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 * as Output from "../Output.ts";
6import type { Url } from "../Redis/index.ts";
7import { UrlMissing as RedisUrlMissing } from "../Redis/index.ts";
8import { isRailwayHost } from "./MountVolume.ts";
9import type { Redis } from "./Redis.ts";
10
11export const REDIS_URL_ENV = "REDIS_URL";
12
13/**
14 * Shared scaffolding for Railway Redis bindings.
15 *
16 * Each `{Op}Http.ts` is a thin `Layer.effect` over {@link makeRedisBinding}.
17 * Deploy-time writes `REDIS_URL` onto the host Service as a Railway
18 * reference (`${{RedisName.REDIS_URL}}`). Runtime commands use that URL
19 * internally — callers never read `Config.Redacted`.
20 *
21 * The RESP client lives in `alchemy/Redis`. This file only wires the
22 * Railway host binding.
23 *
24 * NOT exported from `index.ts`.
25 */
26
27const asPlain = (value: unknown): string | undefined => {
28 if (typeof value === "string" && value.length > 0) return value;
29 if (Redacted.isRedacted(value)) return asPlain(Redacted.value(value));
30 return undefined;
31};
32
33const resolveName = (redis: Redis) =>
34 Effect.gen(function* () {
35 const value = redis.name as unknown;
36 const direct = asPlain(value);
37 if (direct !== undefined) return direct;
38 if (Output.isOutput(value)) {
39 const accessor = yield* value as Output.Output<string>;
40 return asPlain(yield* accessor) ?? "";
41 }
42 if (Effect.isEffect(value)) {
43 return asPlain(yield* value as Effect.Effect<unknown>) ?? "";
44 }
45 return redis.LogicalId;
46 });
47
48const redisUrlFromEnv = Config.Redacted(REDIS_URL_ENV).pipe(
49 Effect.map((value) => Redacted.value(value)),
50);
51
52export const makeRedisBinding = <Client>(options: {
53 makeClient: (url: Url) => Client;
54}) =>
55 Effect.succeed(
56 Effect.fn(function* (redis: Redis) {
57 if (!globalThis.__ALCHEMY_RUNTIME__) {
58 const host = yield* Binding.Host;
59 if (isRailwayHost(host)) {
60 // Resolve the physical service name at apply, not at Service
61 // init — LogicalId (`Cache`) is not a Railway variable
62 // reference. Same Output-in-env pattern as ConnectPostgres.
63 yield* host.bind`${redis}`({
64 env: {
65 [REDIS_URL_ENV]: Output.map(
66 Output.asOutput(redis.name as Output.Output<string>),
67 (name) => `\${{${name}.${REDIS_URL_ENV}}}`,
68 ),
69 },
70 });
71 }
72 }
73
74 const url = redisUrlFromEnv.pipe(
75 Effect.mapError(
76 () =>
77 new RedisUrlMissing({
78 name: asPlain(redis.name) ?? redis.LogicalId,
79 }),
80 ),
81 );
82 return options.makeClient(url);
83 }),
84 );