1import * as Config from "effect/Config";
2import * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import * as Binding from "../Binding.ts";
5import type { Url } from "../Redis/index.ts";
6import type { Resource } from "../Resource.ts";
7import type { ServiceBinding } from "./MountVolume.ts";
8import { REDIS_URL_ENV, RedisUrlMissing, type Redis } from "./Redis.ts";
9
10/**
11 * Shared scaffolding for Upstash Redis bindings.
12 *
13 * Each `{Op}Http.ts` is a thin `Layer.effect` over {@link makeRedisBinding}.
14 * Deploy-time registers the Redis add-on on the host so Service reconcile
15 * can attach it. The URL travels as `yield* redis.url` (RuntimeContext
16 * set/get). `REDIS_URL` written as an App secret is a fallback, not the
17 * primary path.
18 *
19 * The RESP client lives in `alchemy/Redis`. This file only wires the
20 * Fly host binding.
21 *
22 * NOT exported from `index.ts`.
23 */
24
25const isFlyHost = (
26 value: unknown,
27): value is Resource<string, any, any, ServiceBinding> =>
28 typeof value === "object" &&
29 value !== null &&
30 ((value as { Type?: string }).Type === "Fly.Service" ||
31 (value as { Type?: string }).Type === "Fly.Machine");
32
33const asPlain = (value: unknown): string | undefined => {
34 if (typeof value === "string" && value.length > 0) return value;
35 if (Redacted.isRedacted(value)) return asPlain(Redacted.value(value));
36 return undefined;
37};
38
39const redisUrlFromEnv = Config.Redacted(REDIS_URL_ENV).pipe(
40 Effect.map((value) => Redacted.value(value)),
41);
42
43export const makeRedisBinding = <Client>(options: {
44 makeClient: (url: Url) => Client;
45}) =>
46 Effect.succeed(
47 Effect.fn(function* (redis: Redis) {
48 if (!globalThis.__ALCHEMY_RUNTIME__) {
49 const host = yield* Binding.Host;
50 if (isFlyHost(host)) {
51 yield* host.bind`${redis}`({
52 redis: { name: redis.name, id: redis.redisId },
53 });
54 }
55 }
56
57 const urlFromResource = yield* redis.url;
58 const missing = new RedisUrlMissing({
59 name: asPlain(redis.name) ?? redis.LogicalId,
60 });
61 const url: Url = Effect.gen(function* () {
62 const fromOutput = asPlain(yield* urlFromResource);
63 if (fromOutput !== undefined) return fromOutput;
64 return yield* redisUrlFromEnv.pipe(Effect.mapError(() => missing));
65 });
66 return options.makeClient(url);
67 }),
68 );