1/**
2 * Bun Redis integration backed by Bun's built-in `RedisClient`.
3 *
4 * This module creates scoped Bun `RedisClient` connections and exposes them as
5 * both the portable `Redis` service and the Bun-specific `BunRedis` service for
6 * direct access to the raw client. The `layer` helper accepts Redis options
7 * directly, while `layerConfig` reads them from Effect config. Both close the
8 * underlying client when the layer scope finalizes.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import type { RedisClient, RedisOptions } from "bun"
14import * as Config from "effect/Config"
15import * as Context from "effect/Context"
16import * as Deferred from "effect/Deferred"
17import * as Effect from "effect/Effect"
18import * as Fn from "effect/Function"
19import * as Layer from "effect/Layer"
20import * as Redis from "effect/persistence/Redis"
21import * as Scope from "effect/Scope"
22
23/**
24 * Service tag for Bun Redis integration, exposing the raw `RedisClient` and a `use` helper that maps client promise failures to `RedisError`.
25 *
26 * @stability unstable
27 * @category services
28 * @since 4.0.0
29 */
30export class BunRedis extends Context.Service<BunRedis, {
31 readonly client: RedisClient
32 readonly use: <A>(f: (client: RedisClient) => Promise<A>) => Effect.Effect<A, Redis.RedisError>
33}>()("@effect/platform-bun/BunRedis") {}
34
35const make = Effect.fnUntraced(function*(
36 options?: {
37 readonly url?: string
38 } & RedisOptions
39) {
40 const { RedisClient } = yield* Effect.promise(() => import("bun"))
41 const scope = yield* Effect.scope
42 yield* Scope.addFinalizer(scope, Effect.sync(() => client.close()))
43 const client = new RedisClient(options?.url, options)
44
45 const use = <A>(f: (client: RedisClient) => Promise<A>) =>
46 Effect.tryPromise({
47 try: () => f(client),
48 catch: (cause) => new Redis.RedisError({ cause })
49 })
50
51 const redis = yield* Redis.make({
52 send: <A = unknown>(command: string, ...args: ReadonlyArray<string>) =>
53 Effect.tryPromise({
54 try: () => client.send(command, args as Array<string>) as Promise<A>,
55 catch: (cause) => new Redis.RedisError({ cause })
56 }),
57 subscribe: (channel, onMessage) =>
58 Effect.gen(function*() {
59 const terminal = yield* Deferred.make<void, Redis.RedisError>()
60 yield* Effect.acquireRelease(
61 Effect.tryPromise({
62 try: async () => {
63 const subscriber = new RedisClient(options?.url, {
64 ...options,
65 autoReconnect: false
66 })
67 subscriber.onclose = (cause) => {
68 Deferred.doneUnsafe(terminal, new Redis.RedisError({ cause }))
69 }
70 try {
71 await subscriber.subscribe(channel, (message, channel) => {
72 onMessage({ channel, message })
73 })
74 return subscriber
75 } catch (cause) {
76 subscriber.onclose = null
77 subscriber.close()
78 throw cause
79 }
80 },
81 catch: (cause) => new Redis.RedisError({ cause })
82 }),
83 (subscriber) =>
84 Effect.sync(() => {
85 subscriber.onclose = null
86 subscriber.close()
87 })
88 )
89 return Deferred.await(terminal)
90 })
91 })
92
93 const bunRedis = Fn.identity<BunRedis["Service"]>({
94 client,
95 use
96 })
97
98 return Context.make(BunRedis, bunRedis).pipe(
99 Context.add(Redis.Redis, redis)
100 )
101})
102
103/**
104 * Creates scoped Bun Redis layers for `Redis.Redis` and `BunRedis`, closing the underlying client when the scope finalizes.
105 *
106 * @stability unstable
107 * @category layers
108 * @since 4.0.0
109 */
110export const layer = (
111 options?: ({ readonly url?: string } & RedisOptions) | undefined
112): Layer.Layer<Redis.Redis | BunRedis> => Layer.effectContext(make(options))
113
114/**
115 * Creates scoped Bun Redis layers from configurable Redis options, closing the underlying client when the scope finalizes.
116 *
117 * @stability unstable
118 * @category layers
119 * @since 4.0.0
120 */
121export const layerConfig = (
122 options: Config.Wrap<{ readonly url?: string } & RedisOptions>
123): Layer.Layer<Redis.Redis | BunRedis, Config.ConfigError> =>
124 Layer.effectContext(
125 Config.unwrap(options).pipe(
126 Effect.flatMap(make)
127 )
128 )