@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/AGENTS.md +14 -0
- package/CLAUDE.md +14 -0
- package/README.md +1 -1
- package/ai-docs/src/40_sql/10_basics.ts +175 -0
- package/ai-docs/src/40_sql/index.md +6 -0
- package/ai-docs/src/51_http-server/10_basics.ts +3 -1
- package/ai-docs/src/51_http-server/20_testing.ts +100 -0
- package/ai-docs/src/51_http-server/fixtures/api/Users.ts +30 -14
- package/ai-docs/src/51_http-server/fixtures/domain/User.ts +15 -6
- package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +11 -10
- package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +41 -32
- package/ai-docs/src/51_http-server/fixtures/server/Users.ts +141 -20
- package/ai-docs/src/70_cli/10_basics.ts +31 -5
- package/dist/NodeRedis.d.ts +40 -15
- package/dist/NodeRedis.d.ts.map +1 -1
- package/dist/NodeRedis.js +66 -16
- package/dist/NodeRedis.js.map +1 -1
- package/package.json +6 -5
- package/src/NodeRedis.ts +86 -26
|
@@ -1,28 +1,142 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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:
|
|
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
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
78
|
+
}
|
|
79
|
+
yield* Effect.annotateCurrentSpan({ search })
|
|
80
|
+
return yield* Effect.orDie(searchUsers(search))
|
|
81
|
+
})
|
|
24
82
|
|
|
25
|
-
const
|
|
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("
|
|
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("
|
|
53
|
-
const
|
|
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
|
-
|
|
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*({
|
|
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 }) {
|
package/dist/NodeRedis.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Node.js Redis integration backed by `
|
|
2
|
+
* Node.js Redis integration backed by `redis` (node-redis).
|
|
3
3
|
*
|
|
4
|
-
* This module creates a scoped `
|
|
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
|
|
7
|
-
* while `layerConfig` reads them from Effect config.
|
|
8
|
-
*
|
|
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
|
|
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:
|
|
20
|
-
readonly use: <A>(f: (client:
|
|
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
|
-
* `
|
|
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
|
|
34
|
-
* created with the supplied options
|
|
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?:
|
|
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
|
|
42
|
-
* options,
|
|
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<
|
|
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
|
package/dist/NodeRedis.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeRedis.d.ts","sourceRoot":"","sources":["../src/NodeRedis.ts"],"names":[],"mappings":"AAAA
|
|
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 `
|
|
2
|
+
* Node.js Redis integration backed by `redis` (node-redis).
|
|
3
3
|
*
|
|
4
|
-
* This module creates a scoped `
|
|
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
|
|
7
|
-
* while `layerConfig` reads them from Effect config.
|
|
8
|
-
*
|
|
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
|
|
19
|
+
import { createClient, SocketTimeoutError } from "redis";
|
|
20
20
|
/**
|
|
21
21
|
* Service tag for the Node Redis integration, exposing the underlying
|
|
22
|
-
* `
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
const client =
|
|
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.
|
|
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
|
|
55
|
-
* created with the supplied options
|
|
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
|
|
63
|
-
* options,
|
|
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
|
package/dist/NodeRedis.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeRedis.js","names":[],"sources":["../src/NodeRedis.ts"],"sourcesContent":[null],"mappings":"AAAA
|
|
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.
|
|
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.
|
|
57
|
+
"@effect/platform-node-shared": "^4.0.0-rc.110"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"
|
|
61
|
-
"effect": "^4.0.0-rc.
|
|
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
|
-
"
|
|
68
|
+
"redis": "^6.2.1",
|
|
69
|
+
"effect": "^4.0.0-rc.110"
|
|
69
70
|
},
|
|
70
71
|
"scripts": {
|
|
71
72
|
"codegen": "effect-utils codegen",
|