@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.
@@ -1,28 +1,142 @@
1
- import { Context, Effect, Layer, Ref } from "effect"
2
- import { User, UserId } from "../domain/User.ts"
1
+ import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node"
2
+ import { Context, Effect, Layer, Schema } from "effect"
3
+ import { SqlClient, SqlModel, SqlSchema } from "effect/unstable/sql"
4
+ import { User } from "../domain/User.ts"
5
+ import type { UserId } from "../domain/User.ts"
3
6
  import { SearchQueryTooShort, UserNotFound, UsersError } from "../domain/UserErrors.ts"
4
7
 
8
+ // The SqlClient layer determines which database the SQL implementation talks
9
+ // to. Swap it for another driver package to target a different database.
10
+ const SqlLayer = SqliteClient.layer({ filename: ":memory:" })
11
+
12
+ // Migrations are effects keyed by `<id>_<name>` that run once, in id order. A
13
+ // real application would keep each migration in its own file and load them
14
+ // with `SqliteMigrator.fromFileSystem` instead of an inline record.
15
+ const MigratorLayer = SqliteMigrator.layer({
16
+ loader: SqliteMigrator.fromRecord({
17
+ "0001_create_users": Effect.gen(function*() {
18
+ const sql = yield* SqlClient.SqlClient
19
+ yield* sql`
20
+ CREATE TABLE users (
21
+ id TEXT PRIMARY KEY,
22
+ name TEXT NOT NULL,
23
+ email TEXT NOT NULL,
24
+ createdAt TEXT NOT NULL,
25
+ updatedAt TEXT NOT NULL
26
+ )
27
+ `
28
+ })
29
+ })
30
+ })
31
+
5
32
  export class Users extends Context.Service<Users, {
6
33
  list(search: string | undefined): Effect.Effect<Array<User>, UsersError>
7
34
  getById(id: UserId): Effect.Effect<User, UsersError>
8
- create(input: { readonly name: string; readonly email: string }): Effect.Effect<User, UsersError>
35
+ create(input: typeof User.jsonCreate.Type): Effect.Effect<User, UsersError>
36
+ update(id: UserId, input: typeof User.jsonUpdate.Type): Effect.Effect<User, UsersError>
9
37
  }>()("acme/Users") {
10
- static readonly layer = Layer.effect(
38
+ // The SQL implementation only requires a `SqlClient`, so entrypoints and
39
+ // tests decide how the database is provided.
40
+ static readonly layerNoDeps = Layer.effect(
11
41
  Users,
12
42
  Effect.gen(function*() {
13
- const users = new Map<number, User>([
14
- [
15
- 1,
16
- new User({
17
- id: UserId.make(1),
18
- name: "Admin",
19
- email: "admin@acme.dev"
43
+ const sql = yield* SqlClient.SqlClient
44
+
45
+ // CRUD goes through a repository derived from the `User` model. Each
46
+ // operation uses the matching model variant to encode its input and
47
+ // decodes rows with the full model schema.
48
+ const repo = yield* SqlModel.makeRepository(User, {
49
+ tableName: "users",
50
+ spanPrefix: "Users",
51
+ idColumn: "id"
52
+ })
53
+
54
+ // Queries the repository does not cover are written with the `sql` tag
55
+ // and decoded with the model schema.
56
+ const listAll = SqlSchema.findAll({
57
+ Request: Schema.Void,
58
+ Result: User,
59
+ execute: () => sql`SELECT * FROM users ORDER BY createdAt`
60
+ })
61
+
62
+ const searchUsers = SqlSchema.findAll({
63
+ Request: Schema.String,
64
+ Result: User,
65
+ execute: (search) => {
66
+ const pattern = `%${search}%`
67
+ return sql`SELECT * FROM users WHERE name LIKE ${pattern} OR email LIKE ${pattern}`
68
+ }
69
+ })
70
+
71
+ const list = Effect.fn("Users.list")(function*(search: string | undefined) {
72
+ if (search === undefined || search.length === 0) {
73
+ return yield* Effect.orDie(listAll())
74
+ } else if (search.length < SearchQueryTooShort.minimumLength) {
75
+ return yield* new UsersError({
76
+ reason: new SearchQueryTooShort()
20
77
  })
21
- ]
22
- ])
23
- const nextId = yield* Ref.make(2)
78
+ }
79
+ yield* Effect.annotateCurrentSpan({ search })
80
+ return yield* Effect.orDie(searchUsers(search))
81
+ })
24
82
 
25
- const list = Effect.fn("UsersRepo.list")(function*(search: string | undefined) {
83
+ const getById = Effect.fn("Users.getById")((id: UserId) =>
84
+ repo.findById(id).pipe(
85
+ Effect.catchTags({
86
+ NoSuchElementError: () => new UsersError({ reason: new UserNotFound() }),
87
+ // Database and encoding failures are unexpected, so treat them as
88
+ // defects to keep the service interface focused on domain errors.
89
+ SchemaError: Effect.die,
90
+ SqlError: Effect.die
91
+ })
92
+ )
93
+ )
94
+
95
+ const create = Effect.fn("Users.create")((input: typeof User.jsonCreate.Type) =>
96
+ // `User.insert.makeEffect` fills in the generated id and timestamps
97
+ // using the Effect clock, so tests can control them with `TestClock`.
98
+ User.insert.makeEffect(input).pipe(
99
+ Effect.flatMap(repo.insert),
100
+ Effect.orDie
101
+ )
102
+ )
103
+
104
+ const update = Effect.fn("Users.update")(function*(id: UserId, input: typeof User.jsonUpdate.Type) {
105
+ // Ensure the user exists first, so a missing id fails with the domain
106
+ // error instead of a defect.
107
+ yield* getById(id)
108
+ const update = yield* User.update.makeEffect({ id, ...input }).pipe(Effect.orDie)
109
+ return yield* repo.update(update).pipe(Effect.orDie)
110
+ })
111
+
112
+ return Users.of({ list, getById, create, update })
113
+ })
114
+ )
115
+
116
+ // The fully provided SQL implementation: the database client and migrations
117
+ // are implementation details, so this layer requires nothing.
118
+ static readonly layer: Layer.Layer<Users> = this.layerNoDeps.pipe(
119
+ Layer.provide(MigratorLayer.pipe(Layer.provideMerge(SqlLayer))),
120
+ Layer.orDie
121
+ )
122
+
123
+ // An in-memory implementation for tests, so the HTTP stack can be exercised
124
+ // without a database.
125
+ static readonly layerMemory = Layer.effect(
126
+ Users,
127
+ Effect.gen(function*() {
128
+ const users = new Map<UserId, User>()
129
+
130
+ const makeUser = (input: typeof User.jsonCreate.Type) =>
131
+ User.insert.makeEffect(input).pipe(
132
+ Effect.map((user) => new User(user)),
133
+ Effect.orDie
134
+ )
135
+
136
+ const admin = yield* makeUser({ name: "Admin", email: "admin@acme.dev" })
137
+ users.set(admin.id, admin)
138
+
139
+ const list = Effect.fn("Users.list")(function*(search: string | undefined) {
26
140
  const allUsers = Array.from(users.values())
27
141
  if (search === undefined || search.length === 0) {
28
142
  return allUsers
@@ -38,7 +152,7 @@ export class Users extends Context.Service<Users, {
38
152
  )
39
153
  })
40
154
 
41
- const getById = Effect.fn("UsersRepo.getById")(function*(id: UserId) {
155
+ const getById = Effect.fn("Users.getById")(function*(id: UserId) {
42
156
  yield* Effect.annotateCurrentSpan({ id })
43
157
  const user = users.get(id)
44
158
  if (user === undefined) {
@@ -49,14 +163,21 @@ export class Users extends Context.Service<Users, {
49
163
  return user
50
164
  })
51
165
 
52
- const create = Effect.fn("UsersRepo.create")(function*(input: { readonly name: string; readonly email: string }) {
53
- const id = yield* Ref.getAndUpdate(nextId, (current) => current + 1)
54
- const user = new User({ id: UserId.make(id), ...input })
166
+ const create = Effect.fn("Users.create")(function*(input: typeof User.jsonCreate.Type) {
167
+ const user = yield* makeUser(input)
55
168
  users.set(user.id, user)
56
169
  return user
57
170
  })
58
171
 
59
- return Users.of({ list, getById, create })
172
+ const update = Effect.fn("Users.update")(function*(id: UserId, input: typeof User.jsonUpdate.Type) {
173
+ const existing = yield* getById(id)
174
+ const update = yield* User.update.makeEffect({ id, ...input }).pipe(Effect.orDie)
175
+ const updated = new User({ ...existing, ...update })
176
+ users.set(id, updated)
177
+ return updated
178
+ })
179
+
180
+ return Users.of({ list, getById, create, update })
60
181
  })
61
182
  )
62
183
  }
@@ -5,7 +5,7 @@
5
5
  * handlers into a single executable command.
6
6
  */
7
7
  import { NodeRuntime, NodeServices } from "@effect/platform-node"
8
- import { Console, Effect } from "effect"
8
+ import { Console, Effect, Option, Schema } from "effect"
9
9
  import { Argument, Command, Flag } from "effect/unstable/cli"
10
10
 
11
11
  // You can define flags outside of commands and reuse them across multiple
@@ -23,24 +23,41 @@ const tasks = Command.make("tasks").pipe(
23
23
  workspace,
24
24
  verbose: Flag.boolean("verbose").pipe(
25
25
  Flag.withAlias("v"),
26
- Flag.withDescription("Print diagnostic output")
26
+ Flag.withDescription("Print diagnostic output"),
27
+ Flag.withDefault(false)
27
28
  )
28
29
  }),
29
30
  Command.withDescription("Track and manage tasks")
30
31
  )
31
32
 
33
+ // Arguments and flags parse plain strings; use `withSchema` to validate or
34
+ // transform the parsed value with any schema.
35
+ const Email = Schema.String.pipe(
36
+ Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, {
37
+ message: "Expected a valid email address"
38
+ }))
39
+ )
40
+
32
41
  const create = Command.make(
33
42
  "create",
34
43
  {
35
44
  title: Argument.string("title").pipe(
36
- Argument.withDescription("Task title")
45
+ Argument.withDescription("Task title"),
46
+ // Reject empty titles at parse time, so the handler only ever sees
47
+ // valid input
48
+ Argument.withSchema(Schema.NonEmptyString)
37
49
  ),
38
50
  priority: Flag.choice("priority", ["low", "normal", "high"]).pipe(
39
51
  Flag.withDescription("Priority for the new task"),
40
52
  Flag.withDefault("normal")
53
+ ),
54
+ assignee: Flag.string("assignee").pipe(
55
+ Flag.withDescription("Email address of the person to assign"),
56
+ Flag.withSchema(Email),
57
+ Flag.optional
41
58
  )
42
59
  },
43
- Effect.fn(function*({ title, priority }) {
60
+ Effect.fn(function*({ assignee, priority, title }) {
44
61
  // Subcommands can read parent command input by yielding the parent command.
45
62
  const root = yield* tasks
46
63
 
@@ -49,6 +66,10 @@ const create = Command.make(
49
66
  }
50
67
 
51
68
  yield* Console.log(`Created "${title}" in ${root.workspace} with ${priority} priority`)
69
+
70
+ if (Option.isSome(assignee)) {
71
+ yield* Console.log(`Assigned to ${assignee.value}`)
72
+ }
52
73
  })
53
74
  ).pipe(
54
75
  Command.withDescription("Create a task"),
@@ -56,6 +77,10 @@ const create = Command.make(
56
77
  {
57
78
  command: "tasks create \"Ship 4.0\" --priority high",
58
79
  description: "Create a high-priority task"
80
+ },
81
+ {
82
+ command: "tasks create \"Ship 4.0\" --assignee dev@acme.com",
83
+ description: "Create a task assigned to a team member"
59
84
  }
60
85
  ])
61
86
  )
@@ -68,7 +93,8 @@ const list = Command.make(
68
93
  Flag.withDefault("open")
69
94
  ),
70
95
  json: Flag.boolean("json").pipe(
71
- Flag.withDescription("Print machine-readable output")
96
+ Flag.withDescription("Print machine-readable output"),
97
+ Flag.withDefault(false)
72
98
  )
73
99
  },
74
100
  Effect.fn(function*({ status, json }) {
@@ -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,14 +15,16 @@ import * as Context from "effect/Context";
14
15
  import * as Effect from "effect/Effect";
15
16
  import * as Layer from "effect/Layer";
16
17
  import * as Redis from "effect/unstable/persistence/Redis";
17
- import * as IoRedis from "ioredis";
18
+ import { createClient } from "redis";
19
+ type NodeRedisClient = ReturnType<typeof createClient>;
20
+ type NodeRedisClientOptions = NonNullable<Parameters<typeof createClient>[0]>;
18
21
  declare const NodeRedis_base: Context.ServiceClass<NodeRedis, "@effect/platform-node/NodeRedis", {
19
- readonly client: IoRedis.Redis;
20
- readonly use: <A>(f: (client: IoRedis.Redis) => Promise<A>) => Effect.Effect<A, Redis.RedisError>;
22
+ readonly client: NodeRedisClient;
23
+ readonly use: <A>(f: (client: NodeRedisClient) => Promise<A>) => Effect.Effect<A, Redis.RedisError>;
21
24
  }>;
22
25
  /**
23
26
  * Service tag for the Node Redis integration, exposing the underlying
24
- * `ioredis` client and a `use` helper that maps client failures to
27
+ * `node-redis` client and a `use` helper that maps client failures to
25
28
  * `RedisError`.
26
29
  *
27
30
  * @category services
@@ -30,20 +33,42 @@ declare const NodeRedis_base: Context.ServiceClass<NodeRedis, "@effect/platform-
30
33
  export declare class NodeRedis extends NodeRedis_base {
31
34
  }
32
35
  /**
33
- * Provides `Redis` and `NodeRedis` services backed by an `ioredis` client
34
- * created with the supplied options and closed when the layer scope ends.
36
+ * Provides `Redis` and `NodeRedis` services backed by a `node-redis` client
37
+ * created with the supplied options, connected when the layer is built and
38
+ * closed when the layer scope ends.
39
+ *
40
+ * **Details**
41
+ *
42
+ * By default, the initial connection fails on its first connection error. A
43
+ * caller-supplied `socket.reconnectStrategy` is used instead when present. Once
44
+ * the client has emitted `ready`, the default reconnect strategy uses
45
+ * node-redis' exponential backoff and stops on socket timeouts.
46
+ *
47
+ * Scope finalization calls `close()`, which waits for in-flight commands,
48
+ * including blocking commands, and can therefore delay scope closure.
35
49
  *
36
50
  * @category layers
37
51
  * @since 4.0.0
38
52
  */
39
- export declare const layer: (options?: IoRedis.RedisOptions | undefined) => Layer.Layer<Redis.Redis | NodeRedis>;
53
+ export declare const layer: (options?: NodeRedisClientOptions | undefined) => Layer.Layer<Redis.Redis | NodeRedis, Redis.RedisError>;
40
54
  /**
41
- * Provides `Redis` and `NodeRedis` services from `Config`-backed ioredis
42
- * options, closing the client when the layer scope ends.
55
+ * Provides `Redis` and `NodeRedis` services from `Config`-backed node-redis
56
+ * client options, connecting the client when the layer is built and closing it
57
+ * when the layer scope ends.
58
+ *
59
+ * **Details**
60
+ *
61
+ * By default, the initial connection fails on its first connection error. A
62
+ * caller-supplied `socket.reconnectStrategy` is used instead when present. Once
63
+ * the client has emitted `ready`, the default reconnect strategy uses
64
+ * node-redis' exponential backoff and stops on socket timeouts.
65
+ *
66
+ * Scope finalization calls `close()`, which waits for in-flight commands,
67
+ * including blocking commands, and can therefore delay scope closure.
43
68
  *
44
69
  * @category layers
45
70
  * @since 4.0.0
46
71
  */
47
- export declare const layerConfig: (options: Config.Wrap<IoRedis.RedisOptions>) => Layer.Layer<Redis.Redis | NodeRedis, Config.ConfigError>;
72
+ export declare const layerConfig: (options: Config.Wrap<NodeRedisClientOptions>) => Layer.Layer<Redis.Redis | NodeRedis, Redis.RedisError | Config.ConfigError>;
48
73
  export {};
49
74
  //# sourceMappingURL=NodeRedis.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NodeRedis.d.ts","sourceRoot":"","sources":["../src/NodeRedis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,OAAO,KAAK,KAAK,MAAM,mCAAmC,CAAA;AAC1D,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;;qBAWf,OAAO,CAAC,KAAK;kBAChB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC;;AAVnG;;;;;;;GAOG;AACH,qBAAa,SAAU,SAAQ,cAGQ;CAAG;AAiC1C;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,aACN,OAAO,CAAC,YAAY,GAAG,SAAS,KACzC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAuC,CAAA;AAE7E;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,CACxB,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,KACvC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,EAAE,MAAM,CAAC,WAAW,CAOzD,CAAA"}
1
+ {"version":3,"file":"NodeRedis.d.ts","sourceRoot":"","sources":["../src/NodeRedis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,KAAK,MAAM,mCAAmC,CAAA;AAC1D,OAAO,EAAE,YAAY,EAAsB,MAAM,OAAO,CAAA;AAExD,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAA;AACtD,KAAK,sBAAsB,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;;qBAW1D,eAAe;kBAClB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC;;AAVrG;;;;;;;GAOG;AACH,qBAAa,SAAU,SAAQ,cAGQ;CAAG;AAoE1C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,KAAK,aACN,sBAAsB,GAAG,SAAS,KAC3C,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,EAAE,KAAK,CAAC,UAAU,CAAuC,CAAA;AAE/F;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,WAAW,EAAE,CACxB,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,sBAAsB,CAAC,KACzC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,EAAE,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,WAAW,CAO5E,CAAA"}
package/dist/NodeRedis.js 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,12 +15,11 @@ 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
20
  /**
21
21
  * Service tag for the Node Redis integration, exposing the underlying
22
- * `ioredis` client and a `use` helper that maps client failures to
22
+ * `node-redis` client and a `use` helper that maps client failures to
23
23
  * `RedisError`.
24
24
  *
25
25
  * @category services
@@ -27,9 +27,37 @@ import * as IoRedis from "ioredis";
27
27
  */
28
28
  export class NodeRedis extends /*#__PURE__*/Context.Service()("@effect/platform-node/NodeRedis") {}
29
29
  const make = /*#__PURE__*/Effect.fnUntraced(function* (options) {
30
- const scope = yield* Effect.scope;
31
- yield* Scope.addFinalizer(scope, Effect.promise(() => client.quit()));
32
- const client = new IoRedis.Redis(options ?? {});
30
+ let ready = false;
31
+ const socket = options?.socket;
32
+ const client = yield* Effect.acquireRelease(Effect.sync(() => createClient({
33
+ ...options,
34
+ socket: socket?.reconnectStrategy === undefined ? {
35
+ ...socket,
36
+ reconnectStrategy: (retries, cause) => {
37
+ if (!ready) return cause;
38
+ if (cause instanceof SocketTimeoutError) return false;
39
+ const jitter = Math.floor(Math.random() * 200);
40
+ const delay = Math.min(2 ** retries * 50, 2000);
41
+ return delay + jitter;
42
+ }
43
+ } : socket
44
+ })), client => Effect.ignoreCause(Effect.promise(() => client.close())));
45
+ client.once("ready", () => {
46
+ ready = true;
47
+ });
48
+ // node-redis rethrows `error` events that have no listener, which would crash
49
+ // the process on a transient socket failure. Command failures are still
50
+ // reported as `RedisError`.
51
+ const runSync = Effect.runSyncWith(yield* Effect.context());
52
+ client.on("error", cause => {
53
+ runSync(Effect.logWarning("NodeRedis client error", cause));
54
+ });
55
+ yield* Effect.tryPromise({
56
+ try: () => client.connect(),
57
+ catch: cause => new Redis.RedisError({
58
+ cause
59
+ })
60
+ });
33
61
  const use = f => Effect.tryPromise({
34
62
  try: () => f(client),
35
63
  catch: cause => new Redis.RedisError({
@@ -38,7 +66,7 @@ const make = /*#__PURE__*/Effect.fnUntraced(function* (options) {
38
66
  });
39
67
  const redis = yield* Redis.make({
40
68
  send: (command, ...args) => Effect.tryPromise({
41
- try: () => client.call(command, ...args),
69
+ try: () => client.sendCommand([command, ...args]),
42
70
  catch: cause => new Redis.RedisError({
43
71
  cause
44
72
  })
@@ -51,16 +79,38 @@ const make = /*#__PURE__*/Effect.fnUntraced(function* (options) {
51
79
  return Context.make(NodeRedis, nodeRedis).pipe(Context.add(Redis.Redis, redis));
52
80
  });
53
81
  /**
54
- * Provides `Redis` and `NodeRedis` services backed by an `ioredis` client
55
- * created with the supplied options and closed when the layer scope ends.
82
+ * Provides `Redis` and `NodeRedis` services backed by a `node-redis` client
83
+ * created with the supplied options, connected when the layer is built and
84
+ * closed when the layer scope ends.
85
+ *
86
+ * **Details**
87
+ *
88
+ * By default, the initial connection fails on its first connection error. A
89
+ * caller-supplied `socket.reconnectStrategy` is used instead when present. Once
90
+ * the client has emitted `ready`, the default reconnect strategy uses
91
+ * node-redis' exponential backoff and stops on socket timeouts.
92
+ *
93
+ * Scope finalization calls `close()`, which waits for in-flight commands,
94
+ * including blocking commands, and can therefore delay scope closure.
56
95
  *
57
96
  * @category layers
58
97
  * @since 4.0.0
59
98
  */
60
99
  export const layer = options => Layer.effectContext(make(options));
61
100
  /**
62
- * Provides `Redis` and `NodeRedis` services from `Config`-backed ioredis
63
- * options, closing the client when the layer scope ends.
101
+ * Provides `Redis` and `NodeRedis` services from `Config`-backed node-redis
102
+ * client options, connecting the client when the layer is built and closing it
103
+ * when the layer scope ends.
104
+ *
105
+ * **Details**
106
+ *
107
+ * By default, the initial connection fails on its first connection error. A
108
+ * caller-supplied `socket.reconnectStrategy` is used instead when present. Once
109
+ * the client has emitted `ready`, the default reconnect strategy uses
110
+ * node-redis' exponential backoff and stops on socket timeouts.
111
+ *
112
+ * Scope finalization calls `close()`, which waits for in-flight commands,
113
+ * including blocking commands, and can therefore delay scope closure.
64
114
  *
65
115
  * @category layers
66
116
  * @since 4.0.0
@@ -1 +1 @@
1
- {"version":3,"file":"NodeRedis.js","names":[],"sources":["../src/NodeRedis.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;AAWA,OAAO,KAAK,MAAM,MAAM,eAAe;AACvC,OAAO,KAAK,OAAO,MAAM,gBAAgB;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe;AACvC,OAAO,KAAK,EAAE,MAAM,iBAAiB;AACrC,OAAO,KAAK,KAAK,MAAM,cAAc;AACrC,OAAO,KAAK,KAAK,MAAM,cAAc;AACrC,OAAO,KAAK,KAAK,MAAM,mCAAmC;AAC1D,OAAO,KAAK,OAAO,MAAM,SAAS;AAElC;;;;;;;;AAQA,OAAM,MAAO,SAAU,sBAAQ,OAAO,CAAC,OAAO,EAG1C,CAAC,iCAAiC,CAAC;AAEvC,MAAM,IAAI,gBAAG,MAAM,CAAC,UAAU,CAAC,WAC7B,OAA8B;EAE9B,MAAM,KAAK,GAAG,OAAO,MAAM,CAAC,KAAK;EACjC,OAAO,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;EACrE,MAAM,MAAM,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;EAE/C,MAAM,GAAG,GAAO,CAAwC,IACtD,MAAM,CAAC,UAAU,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC;IACpB,KAAK,EAAG,KAAK,IAAK,IAAI,KAAK,CAAC,UAAU,CAAC;MAAE;IAAK,CAAE;GACjD,CAAC;EAEJ,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAC;IAC9B,IAAI,EAAE,CAAc,OAAe,EAAE,GAAG,IAA2B,KACjE,MAAM,CAAC,UAAU,CAAC;MAChB,GAAG,EAAE,MAAM,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,CAAe;MACtD,KAAK,EAAG,KAAK,IAAK,IAAI,KAAK,CAAC,UAAU,CAAC;QAAE;MAAK,CAAE;KACjD;GACJ,CAAC;EAEF,MAAM,SAAS,GAAG,EAAE,CAAC,QAAQ,CAAuB;IAClD,MAAM;IACN;GACD,CAAC;EAEF,OAAO,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,IAAI,CAC5C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAChC;AACH,CAAC,CAAC;AAEF;;;;;;;AAOA,OAAO,MAAM,KAAK,GAChB,OAA0C,IACD,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAE7E;;;;;;;AAOA,OAAO,MAAM,WAAW,GAGtB,OAA0C,IAE1C,KAAK,CAAC,aAAa,CACjB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CACzB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CACrB,CACF","ignoreList":[]}
1
+ {"version":3,"file":"NodeRedis.js","names":[],"sources":["../src/NodeRedis.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@effect/platform-node",
3
3
  "type": "module",
4
- "version": "4.0.0-rc.108",
4
+ "version": "4.0.0-rc.110",
5
5
  "license": "MIT",
6
6
  "description": "Platform specific implementations for the Node.js runtime",
7
7
  "homepage": "https://effect.website",
@@ -54,18 +54,19 @@
54
54
  "dependencies": {
55
55
  "mime": "^4.1.0",
56
56
  "undici": "^8.7.0",
57
- "@effect/platform-node-shared": "^4.0.0-rc.108"
57
+ "@effect/platform-node-shared": "^4.0.0-rc.110"
58
58
  },
59
59
  "peerDependencies": {
60
- "ioredis": ">=5.7.0 <6.0.0",
61
- "effect": "^4.0.0-rc.108"
60
+ "redis": ">=5.0.0 <7.0.0",
61
+ "effect": "^4.0.0-rc.110"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@testcontainers/mysql": "^12.0.4",
65
65
  "@testcontainers/postgresql": "^12.0.4",
66
66
  "@testcontainers/redis": "^12.0.4",
67
67
  "@types/node": "^26.1.2",
68
- "effect": "^4.0.0-rc.108"
68
+ "redis": "^6.2.1",
69
+ "effect": "^4.0.0-rc.110"
69
70
  },
70
71
  "scripts": {
71
72
  "codegen": "effect-utils codegen",