@effect/platform-node 4.0.0-rc.108 → 4.0.0-rc.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/NodeRedis.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Node.js Redis integration backed by `ioredis`.
2
+ * Node.js Redis integration backed by `redis` (node-redis).
3
3
  *
4
- * This module creates a scoped `ioredis` client and exposes it in two forms:
4
+ * This module creates a scoped `node-redis` client and exposes it in two forms:
5
5
  * the generic `Redis` service and the {@link NodeRedis} service for direct
6
- * access to the underlying client. `layer` accepts ioredis options directly,
7
- * while `layerConfig` reads them from Effect config. Both layers close the
8
- * client when the layer scope ends.
6
+ * access to the underlying client. `layer` accepts node-redis client options
7
+ * directly, while `layerConfig` reads them from Effect config. `node-redis`
8
+ * connects explicitly, so layer construction can fail with a `RedisError`.
9
+ * Both layers close the client when the layer scope ends.
9
10
  *
10
11
  * @since 4.0.0
11
12
  */
@@ -14,31 +15,68 @@ import * as Context from "effect/Context"
14
15
  import * as Effect from "effect/Effect"
15
16
  import * as Fn from "effect/Function"
16
17
  import * as Layer from "effect/Layer"
17
- import * as Scope from "effect/Scope"
18
18
  import * as Redis from "effect/unstable/persistence/Redis"
19
- import * as IoRedis from "ioredis"
19
+ import { createClient, SocketTimeoutError } from "redis"
20
+
21
+ type NodeRedisClient = ReturnType<typeof createClient>
22
+ type NodeRedisClientOptions = NonNullable<Parameters<typeof createClient>[0]>
20
23
 
21
24
  /**
22
25
  * Service tag for the Node Redis integration, exposing the underlying
23
- * `ioredis` client and a `use` helper that maps client failures to
26
+ * `node-redis` client and a `use` helper that maps client failures to
24
27
  * `RedisError`.
25
28
  *
26
29
  * @category services
27
30
  * @since 4.0.0
28
31
  */
29
32
  export class NodeRedis extends Context.Service<NodeRedis, {
30
- readonly client: IoRedis.Redis
31
- readonly use: <A>(f: (client: IoRedis.Redis) => Promise<A>) => Effect.Effect<A, Redis.RedisError>
33
+ readonly client: NodeRedisClient
34
+ readonly use: <A>(f: (client: NodeRedisClient) => Promise<A>) => Effect.Effect<A, Redis.RedisError>
32
35
  }>()("@effect/platform-node/NodeRedis") {}
33
36
 
34
37
  const make = Effect.fnUntraced(function*(
35
- options?: IoRedis.RedisOptions
38
+ options?: NodeRedisClientOptions
36
39
  ) {
37
- const scope = yield* Effect.scope
38
- yield* Scope.addFinalizer(scope, Effect.promise(() => client.quit()))
39
- const client = new IoRedis.Redis(options ?? {})
40
+ let ready = false
41
+ const socket = options?.socket
42
+ const client = yield* Effect.acquireRelease(
43
+ Effect.sync((): NodeRedisClient =>
44
+ createClient({
45
+ ...options,
46
+ socket: socket?.reconnectStrategy === undefined
47
+ ? {
48
+ ...socket,
49
+ reconnectStrategy: (retries, cause) => {
50
+ if (!ready) return cause
51
+ if (cause instanceof SocketTimeoutError) return false
52
+ const jitter = Math.floor(Math.random() * 200)
53
+ const delay = Math.min(2 ** retries * 50, 2000)
54
+ return delay + jitter
55
+ }
56
+ }
57
+ : socket
58
+ })
59
+ ),
60
+ (client) => Effect.ignoreCause(Effect.promise(() => client.close()))
61
+ )
62
+ client.once("ready", () => {
63
+ ready = true
64
+ })
65
+
66
+ // node-redis rethrows `error` events that have no listener, which would crash
67
+ // the process on a transient socket failure. Command failures are still
68
+ // reported as `RedisError`.
69
+ const runSync = Effect.runSyncWith(yield* Effect.context<never>())
70
+ client.on("error", (cause) => {
71
+ runSync(Effect.logWarning("NodeRedis client error", cause))
72
+ })
73
+
74
+ yield* Effect.tryPromise({
75
+ try: () => client.connect(),
76
+ catch: (cause) => new Redis.RedisError({ cause })
77
+ })
40
78
 
41
- const use = <A>(f: (client: IoRedis.Redis) => Promise<A>) =>
79
+ const use = <A>(f: (client: NodeRedisClient) => Promise<A>) =>
42
80
  Effect.tryPromise({
43
81
  try: () => f(client),
44
82
  catch: (cause) => new Redis.RedisError({ cause })
@@ -47,7 +85,7 @@ const make = Effect.fnUntraced(function*(
47
85
  const redis = yield* Redis.make({
48
86
  send: <A = unknown>(command: string, ...args: ReadonlyArray<string>) =>
49
87
  Effect.tryPromise({
50
- try: () => client.call(command, ...args) as Promise<A>,
88
+ try: () => client.sendCommand([command, ...args]) as Promise<A>,
51
89
  catch: (cause) => new Redis.RedisError({ cause })
52
90
  })
53
91
  })
@@ -63,28 +101,50 @@ const make = Effect.fnUntraced(function*(
63
101
  })
64
102
 
65
103
  /**
66
- * Provides `Redis` and `NodeRedis` services backed by an `ioredis` client
67
- * created with the supplied options and closed when the layer scope ends.
104
+ * Provides `Redis` and `NodeRedis` services backed by a `node-redis` client
105
+ * created with the supplied options, connected when the layer is built and
106
+ * closed when the layer scope ends.
107
+ *
108
+ * **Details**
109
+ *
110
+ * By default, the initial connection fails on its first connection error. A
111
+ * caller-supplied `socket.reconnectStrategy` is used instead when present. Once
112
+ * the client has emitted `ready`, the default reconnect strategy uses
113
+ * node-redis' exponential backoff and stops on socket timeouts.
114
+ *
115
+ * Scope finalization calls `close()`, which waits for in-flight commands,
116
+ * including blocking commands, and can therefore delay scope closure.
68
117
  *
69
118
  * @category layers
70
119
  * @since 4.0.0
71
120
  */
72
121
  export const layer = (
73
- options?: IoRedis.RedisOptions | undefined
74
- ): Layer.Layer<Redis.Redis | NodeRedis> => Layer.effectContext(make(options))
122
+ options?: NodeRedisClientOptions | undefined
123
+ ): Layer.Layer<Redis.Redis | NodeRedis, Redis.RedisError> => Layer.effectContext(make(options))
75
124
 
76
125
  /**
77
- * Provides `Redis` and `NodeRedis` services from `Config`-backed ioredis
78
- * options, closing the client when the layer scope ends.
126
+ * Provides `Redis` and `NodeRedis` services from `Config`-backed node-redis
127
+ * client options, connecting the client when the layer is built and closing it
128
+ * when the layer scope ends.
129
+ *
130
+ * **Details**
131
+ *
132
+ * By default, the initial connection fails on its first connection error. A
133
+ * caller-supplied `socket.reconnectStrategy` is used instead when present. Once
134
+ * the client has emitted `ready`, the default reconnect strategy uses
135
+ * node-redis' exponential backoff and stops on socket timeouts.
136
+ *
137
+ * Scope finalization calls `close()`, which waits for in-flight commands,
138
+ * including blocking commands, and can therefore delay scope closure.
79
139
  *
80
140
  * @category layers
81
141
  * @since 4.0.0
82
142
  */
83
143
  export const layerConfig: (
84
- options: Config.Wrap<IoRedis.RedisOptions>
85
- ) => Layer.Layer<Redis.Redis | NodeRedis, Config.ConfigError> = (
86
- options: Config.Wrap<IoRedis.RedisOptions>
87
- ): Layer.Layer<Redis.Redis | NodeRedis, Config.ConfigError> =>
144
+ options: Config.Wrap<NodeRedisClientOptions>
145
+ ) => Layer.Layer<Redis.Redis | NodeRedis, Redis.RedisError | Config.ConfigError> = (
146
+ options: Config.Wrap<NodeRedisClientOptions>
147
+ ): Layer.Layer<Redis.Redis | NodeRedis, Redis.RedisError | Config.ConfigError> =>
88
148
  Layer.effectContext(
89
149
  Config.unwrap(options).pipe(
90
150
  Effect.flatMap(make)