queasy 0.2.0 → 0.3.1

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.
Files changed (98) hide show
  1. package/.github/workflows/check.yml +3 -0
  2. package/.github/workflows/publish.yml +3 -0
  3. package/CLAUDE.md +5 -4
  4. package/Readme.md +9 -4
  5. package/biome.json +5 -1
  6. package/dist/client.d.ts +33 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +199 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/constants.d.ts +10 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/{src → dist}/constants.js +2 -10
  13. package/dist/constants.js.map +1 -0
  14. package/dist/errors.d.ts +7 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/{src → dist}/errors.js +1 -13
  17. package/dist/errors.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +3 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/manager.d.ts +19 -0
  23. package/dist/manager.d.ts.map +1 -0
  24. package/dist/manager.js +67 -0
  25. package/dist/manager.js.map +1 -0
  26. package/dist/pool.d.ts +29 -0
  27. package/dist/pool.d.ts.map +1 -0
  28. package/{src → dist}/pool.js +23 -82
  29. package/dist/pool.js.map +1 -0
  30. package/dist/queasy.lua +390 -0
  31. package/dist/queue.d.ts +22 -0
  32. package/dist/queue.d.ts.map +1 -0
  33. package/dist/queue.js +81 -0
  34. package/dist/queue.js.map +1 -0
  35. package/dist/types.d.ts +92 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +2 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/utils.d.ts +4 -0
  40. package/dist/utils.d.ts.map +1 -0
  41. package/dist/utils.js +24 -0
  42. package/dist/utils.js.map +1 -0
  43. package/dist/worker.d.ts +2 -0
  44. package/dist/worker.d.ts.map +1 -0
  45. package/dist/worker.js +42 -0
  46. package/dist/worker.js.map +1 -0
  47. package/docker-compose.yml +0 -2
  48. package/fuzztest/Readme.md +185 -0
  49. package/fuzztest/fuzz.ts +356 -0
  50. package/fuzztest/handlers/cascade-a.ts +90 -0
  51. package/fuzztest/handlers/cascade-b.ts +71 -0
  52. package/fuzztest/handlers/fail-handler.ts +47 -0
  53. package/fuzztest/handlers/periodic.ts +89 -0
  54. package/fuzztest/process.ts +100 -0
  55. package/fuzztest/shared/chaos.ts +29 -0
  56. package/fuzztest/shared/stream.ts +40 -0
  57. package/package.json +8 -7
  58. package/plans/redis-options.md +279 -0
  59. package/src/client.ts +246 -0
  60. package/src/constants.ts +33 -0
  61. package/src/errors.ts +13 -0
  62. package/src/index.ts +2 -0
  63. package/src/manager.ts +78 -0
  64. package/src/pool.ts +129 -0
  65. package/src/queasy.lua +2 -3
  66. package/src/queue.ts +108 -0
  67. package/src/types.ts +16 -0
  68. package/src/{utils.js → utils.ts} +3 -20
  69. package/src/{worker.js → worker.ts} +5 -12
  70. package/test/{client.test.js → client.test.ts} +6 -7
  71. package/test/{errors.test.js → errors.test.ts} +1 -1
  72. package/test/fixtures/always-fail-handler.ts +5 -0
  73. package/test/fixtures/data-logger-handler.ts +11 -0
  74. package/test/fixtures/failure-handler.ts +6 -0
  75. package/test/fixtures/permanent-error-handler.ts +6 -0
  76. package/test/fixtures/slow-handler.ts +6 -0
  77. package/test/fixtures/success-handler.js +0 -5
  78. package/test/fixtures/success-handler.ts +6 -0
  79. package/test/fixtures/with-failure-handler.ts +5 -0
  80. package/test/{guards.test.js → guards.test.ts} +21 -34
  81. package/test/{manager.test.js → manager.test.ts} +26 -34
  82. package/test/{pool.test.js → pool.test.ts} +14 -16
  83. package/test/{queue.test.js → queue.test.ts} +21 -21
  84. package/test/{redis-functions.test.js → redis-functions.test.ts} +14 -20
  85. package/test/{utils.test.js → utils.test.ts} +1 -1
  86. package/tsconfig.json +20 -0
  87. package/jsconfig.json +0 -17
  88. package/src/client.js +0 -258
  89. package/src/index.js +0 -2
  90. package/src/manager.js +0 -94
  91. package/src/queue.js +0 -154
  92. package/test/fixtures/always-fail-handler.js +0 -8
  93. package/test/fixtures/data-logger-handler.js +0 -19
  94. package/test/fixtures/failure-handler.js +0 -9
  95. package/test/fixtures/permanent-error-handler.js +0 -10
  96. package/test/fixtures/slow-handler.js +0 -9
  97. package/test/fixtures/with-failure-handler.js +0 -8
  98. /package/test/fixtures/{no-handle-handler.js → no-handle-handler.ts} +0 -0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Fuzz test child process entry point.
3
+ *
4
+ * Each child process creates one queasy Client (with worker threads) and
5
+ * calls listen() on all three queues. Handlers run inside queasy's internal
6
+ * worker threads and communicate crash signals via BroadcastChannel.
7
+ */
8
+
9
+ import { dirname, join } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { BroadcastChannel } from 'node:worker_threads';
12
+ import { Client } from '../src/index.ts';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+
16
+ // ── Configuration ─────────────────────────────────────────────────────────────
17
+
18
+ const WORKER_THREADS = 2; // worker threads per child process
19
+
20
+ const BASE_OPTIONS = {
21
+ maxRetries: 3,
22
+ maxStalls: 2,
23
+ minBackoff: 200,
24
+ maxBackoff: 2000,
25
+ timeout: 3000,
26
+ size: 10,
27
+ };
28
+
29
+ const FAIL_RETRY_OPTIONS = {
30
+ maxRetries: 5,
31
+ minBackoff: 200,
32
+ };
33
+
34
+ // ── Handler paths ──────────────────────────────────────────────────────────────
35
+
36
+ const failHandlerPath = join(__dirname, 'handlers', 'fail-handler.ts');
37
+ const periodicPath = join(__dirname, 'handlers', 'periodic.ts');
38
+ const cascadeAPath = join(__dirname, 'handlers', 'cascade-a.ts');
39
+ const cascadeBPath = join(__dirname, 'handlers', 'cascade-b.ts');
40
+
41
+ // ── Crash signal ───────────────────────────────────────────────────────────────
42
+
43
+ // Handlers running in worker threads post to this channel to trigger a crash.
44
+ const crashChannel = new BroadcastChannel('fuzz-crash');
45
+ crashChannel.onmessage = () => {
46
+ process.exit(1);
47
+ };
48
+
49
+ // ── Redis + queasy client ──────────────────────────────────────────────────────
50
+
51
+ const client = await new Promise<Client>((resolve) => new Client({}, WORKER_THREADS, resolve));
52
+
53
+ client.on('disconnected', (reason: string) => {
54
+ console.error(`[process ${process.pid}] Client disconnected: ${reason}`);
55
+ process.exit(1);
56
+ });
57
+
58
+ // Forward client lifecycle events to the orchestrator via IPC.
59
+ // The orchestrator uses these to authoritatively track active jobs.
60
+ client.on('dequeue', (queue: string, job: { id: string; runAt: number }) => {
61
+ process.send!({ type: 'dequeue', queue, jobId: job.id, runAt: job.runAt });
62
+ });
63
+ client.on('finish', (queue: string, jobId: string) => {
64
+ process.send!({ type: 'finish', queue, jobId });
65
+ });
66
+ client.on('retry', (queue: string, jobId: string) => {
67
+ process.send!({ type: 'retry', queue, jobId });
68
+ });
69
+ client.on('fail', (queue: string, jobId: string) => {
70
+ process.send!({ type: 'fail', queue, jobId });
71
+ });
72
+
73
+ // ── Queue setup ────────────────────────────────────────────────────────────────
74
+
75
+ const periodicQueue = client.queue('{fuzz}:periodic', true);
76
+ const cascadeAQueue = client.queue('{fuzz}:cascade-a', true);
77
+ const cascadeBQueue = client.queue('{fuzz}:cascade-b', true);
78
+
79
+ await Promise.all([
80
+ periodicQueue.listen(periodicPath, {
81
+ ...BASE_OPTIONS,
82
+ priority: 300,
83
+ failHandler: failHandlerPath,
84
+ failRetryOptions: FAIL_RETRY_OPTIONS,
85
+ }),
86
+ cascadeAQueue.listen(cascadeAPath, {
87
+ ...BASE_OPTIONS,
88
+ priority: 200,
89
+ failHandler: failHandlerPath,
90
+ failRetryOptions: FAIL_RETRY_OPTIONS,
91
+ }),
92
+ cascadeBQueue.listen(cascadeBPath, {
93
+ ...BASE_OPTIONS,
94
+ priority: 100,
95
+ failHandler: failHandlerPath,
96
+ failRetryOptions: FAIL_RETRY_OPTIONS,
97
+ }),
98
+ ]);
99
+
100
+ console.log(`[process ${process.pid}] Ready`);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Weighted random chaos behavior picker.
3
+ * All handlers apply the same set of chaos behaviors.
4
+ */
5
+
6
+ type ChaosAction = 'normal' | 'retriable' | 'permanent' | 'stall' | 'spin' | 'crash';
7
+
8
+ const BEHAVIORS: { action: ChaosAction; weight: number }[] = [
9
+ { action: 'normal', weight: 65 },
10
+ { action: 'retriable', weight: 15 },
11
+ { action: 'permanent', weight: 5 },
12
+ { action: 'stall', weight: 10 },
13
+ { action: 'spin', weight: 3 },
14
+ { action: 'crash', weight: 2 },
15
+ ];
16
+
17
+ const TOTAL_WEIGHT = BEHAVIORS.reduce((sum, b) => sum + b.weight, 0);
18
+
19
+ /**
20
+ * Pick a chaos action based on weighted probability.
21
+ */
22
+ export function pickChaos(): ChaosAction {
23
+ let r = Math.random() * TOTAL_WEIGHT;
24
+ for (const { action, weight } of BEHAVIORS) {
25
+ r -= weight;
26
+ if (r <= 0) return action;
27
+ }
28
+ return 'normal';
29
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Redis stream helpers for the fuzz test event log.
3
+ * All events are written to the 'fuzz:events' stream.
4
+ */
5
+
6
+ import type { RedisClientType } from 'redis';
7
+
8
+ export const STREAM_KEY = 'fuzz:events';
9
+
10
+ /**
11
+ * Emit a structured event to the fuzz:events stream.
12
+ */
13
+ export async function emitEvent(redis: RedisClientType, fields: Record<string, string>): Promise<void> {
14
+ try {
15
+ await redis.xAdd(STREAM_KEY, '*', fields);
16
+ } catch {
17
+ // Never let event emission crash a handler
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Async generator that yields parsed event objects from the fuzz:events stream.
23
+ * Blocks for up to 1 second waiting for new events, then yields control back.
24
+ */
25
+ export async function* readEvents(
26
+ redis: RedisClientType,
27
+ lastId = '0'
28
+ ): AsyncGenerator<Record<string, string>> {
29
+ let id = lastId;
30
+ while (true) {
31
+ const results = await redis.xRead({ key: STREAM_KEY, id }, { BLOCK: 1000, COUNT: 100 });
32
+ if (!results) continue;
33
+ for (const { messages } of results) {
34
+ for (const { id: msgId, message } of messages) {
35
+ id = msgId;
36
+ yield message;
37
+ }
38
+ }
39
+ }
40
+ }
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "queasy",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A simple Redis-backed queue library for Node.js",
5
- "main": "src/index.js",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
6
7
  "type": "module",
7
8
  "scripts": {
9
+ "build": "tsc -p tsconfig.json && cp src/queasy.lua dist/queasy.lua",
8
10
  "test": "node --test",
9
- "test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=96 --test-coverage-include 'src/**/*.js' 'test/**/*.test.js'",
11
+ "test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=96 --test-coverage-include 'dist/**/*.js' 'test/**/*.test.js'",
10
12
  "test:watch": "node --test --watch",
13
+ "test:fuzz": "node fuzztest/fuzz.ts",
11
14
  "lint": "biome check .",
12
15
  "lint:fix": "biome check --write .",
13
- "format": "biome format --write .",
14
- "typecheck": "tsc --noEmit -p jsconfig.json",
16
+ "typecheck": "tsc --noEmit -p tsconfig.json",
15
17
  "docker:up": "docker compose up -d",
16
18
  "docker:down": "docker compose down",
17
19
  "docker:logs": "docker compose logs -f"
@@ -29,13 +31,12 @@
29
31
  ],
30
32
  "author": "",
31
33
  "license": "ISC",
32
- "peerDependencies": {
34
+ "dependencies": {
33
35
  "redis": "^5.10.0"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@biomejs/biome": "^2.3.14",
37
39
  "@types/node": "^25.2.0",
38
- "redis": "^5.10.0",
39
40
  "typescript": "^5.9.3"
40
41
  }
41
42
  }
@@ -0,0 +1,279 @@
1
+ # Plan: Client constructs its own Redis connection
2
+
3
+ ## Goal
4
+
5
+ Change the `Client` constructor so that instead of accepting a pre-built node-redis
6
+ connection as its first argument, it accepts an options object and constructs the
7
+ connection itself (calling either `createClient` or `createCluster` from the `redis`
8
+ package). This gives Queasy full control over connection lifecycle.
9
+
10
+ ---
11
+
12
+ ## 1. Options object design
13
+
14
+ The first argument changes from `RedisClient` to a `RedisOptions` object:
15
+
16
+ ```ts
17
+ import type { RedisClientOptions, RedisClusterOptions } from 'redis';
18
+
19
+ type SingleNodeOptions = Pick<RedisClientOptions, 'url' | 'socket' | 'username' | 'password' | 'database'>;
20
+
21
+ type RedisOptions =
22
+ | SingleNodeOptions
23
+ | {
24
+ rootNodes: SingleNodeOptions[];
25
+ defaults?: Partial<SingleNodeOptions>;
26
+ nodeAddressMap?: RedisClusterOptions['nodeAddressMap'];
27
+ }
28
+ ```
29
+
30
+ `RedisClientOptions` and `RedisClusterOptions` are both exported from the top-level
31
+ `redis` package and can be imported directly. `RedisClusterClientOptions` (the type
32
+ node-redis uses for `rootNodes` elements and `defaults`) is **not** exported at the
33
+ top level, so we define the cluster form inline, reusing `SingleNodeOptions` to
34
+ constrain both `rootNodes` elements and `defaults` to the same permitted fields as
35
+ the single-node case. `nodeAddressMap` is taken via an index type from
36
+ `RedisClusterOptions` directly to avoid depending on the unexported `NodeAddressMap`
37
+ type.
38
+
39
+ The dispatch rule: **if `options.rootNodes` exists, use `createCluster`; otherwise
40
+ use `createClient`.**
41
+
42
+ - If `options.rootNodes` is present: call `createCluster(options)`, passing the
43
+ object through directly. The caller may also provide `defaults` (shared auth/TLS
44
+ for all nodes) and `nodeAddressMap` — all standard `createCluster` options,
45
+ named consistently with the node-redis API.
46
+ - Otherwise: call `createClient(options)`, passing the object through directly.
47
+ - The constructed connection is stored as `this.redis`.
48
+ - `connect()` is called internally in the constructor.
49
+ - `close()` calls `this.redis.destroy()` to disconnect.
50
+
51
+ ### Constructor signature
52
+
53
+ All three arguments default so that `new Client()` works (connects to `localhost:6379`):
54
+
55
+ ```js
56
+ constructor(options = {}, workerCount = os.cpus().length, callback = undefined)
57
+ ```
58
+
59
+ ---
60
+
61
+ ## 2. Changes to `src/client.js`
62
+
63
+ 1. Add imports:
64
+ ```js
65
+ import { createClient, createCluster } from 'redis';
66
+ ```
67
+
68
+ 2. Add a helper at module level:
69
+ ```js
70
+ function buildRedisConnection(options) {
71
+ if (options.rootNodes) {
72
+ return createCluster(options);
73
+ }
74
+ return createClient(options);
75
+ }
76
+ ```
77
+
78
+ 3. Update constructor:
79
+ - Parameter `redis` → `options` (with JSDoc type `RedisOptions`)
80
+ - Replace `this.redis = redis;` with:
81
+ ```js
82
+ this.redis = buildRedisConnection(options);
83
+ ```
84
+ - Replace the bare `installLuaFunctions(this.redis).then(...)` call with:
85
+ ```js
86
+ this.redis.connect()
87
+ .then(() => installLuaFunctions(this.redis))
88
+ .then((disconnect) => { ... })
89
+ .catch((err) => {
90
+ this.disconnected = true;
91
+ this.emit('disconnected', err.message);
92
+ });
93
+ ```
94
+ - Apply defaults to all parameters:
95
+ ```js
96
+ constructor(options = {}, workerCount = os.cpus().length, callback)
97
+ ```
98
+ Add `import os from 'node:os';` (check if already imported; if so, reuse).
99
+
100
+ 4. Update `close()`:
101
+ - After `this.disconnected = true;`, add:
102
+ ```js
103
+ await this.redis.destroy().catch(() => {});
104
+ ```
105
+ `destroy()` is the documented disconnect method for both `RedisClientType`
106
+ and `RedisClusterType` in node-redis v5.
107
+
108
+ 5. Update JSDoc typedef:
109
+ ```js
110
+ /** @typedef {import('./types').RedisOptions} RedisOptions */
111
+ ```
112
+ Point to `types.ts` rather than duplicating the type inline in JS, since the
113
+ full `Pick<>` construction is cleaner to express in TypeScript. Remove the old
114
+ `RedisClient` typedef.
115
+
116
+ ---
117
+
118
+ ## 3. Changes to `src/types.ts`
119
+
120
+ Import the relevant node-redis types and define `RedisOptions` using `Pick<>` and
121
+ an inline cluster shape:
122
+
123
+ ```ts
124
+ import type { RedisClientOptions, RedisClusterOptions } from 'redis';
125
+
126
+ type SingleNodeOptions = Pick<RedisClientOptions, 'url' | 'socket' | 'username' | 'password' | 'database'>;
127
+
128
+ export type RedisOptions =
129
+ | SingleNodeOptions
130
+ | {
131
+ rootNodes: SingleNodeOptions[];
132
+ defaults?: Partial<SingleNodeOptions>;
133
+ nodeAddressMap?: RedisClusterOptions['nodeAddressMap'];
134
+ }
135
+ ```
136
+
137
+ `SingleNodeOptions` is not exported — it's an internal building block. Only
138
+ `RedisOptions` is exported. Update the `Client` constructor signature to accept
139
+ `RedisOptions` instead of `RedisClientType`.
140
+
141
+ ---
142
+
143
+ ## 4. Changes to `package.json`
144
+
145
+ - Move `redis` from `peerDependencies` + `devDependencies` to **`dependencies`**
146
+ (queasy now owns the connection; callers no longer need to install it themselves).
147
+ - Remove the `peerDependencies` section entirely.
148
+
149
+ Before:
150
+ ```json
151
+ "peerDependencies": { "redis": "^5.10.0" },
152
+ "devDependencies": { "redis": "^5.10.0", ... }
153
+ ```
154
+
155
+ After:
156
+ ```json
157
+ "dependencies": { "redis": "^5.10.0" },
158
+ "devDependencies": { ... }
159
+ ```
160
+
161
+ ---
162
+
163
+ ## 5. Changes to `test/queue.test.js`
164
+
165
+ Currently each `beforeEach` creates and connects a Redis client, passes it to
166
+ `Client`, and each `afterEach` calls `redis.quit()`.
167
+
168
+ Changes:
169
+ - Remove the `createClient` import and the `redis` variable.
170
+ - Change `new Client(redis, 1)` → `new Client({}, 1)`.
171
+ - Remove `await redis.quit()` — `client.close()` now handles disconnection.
172
+ - Any direct Redis inspection calls (`redis.zScore`, `redis.hGetAll`, `redis.keys`,
173
+ `redis.del`) still need a Redis connection. Add a dedicated `redisInspect` client
174
+ used only for test-side assertions:
175
+ ```js
176
+ // beforeEach
177
+ redisInspect = createClient();
178
+ await redisInspect.connect();
179
+ // afterEach
180
+ await redisInspect.quit();
181
+ ```
182
+
183
+ ---
184
+
185
+ ## 6. Changes to `test/client.test.js`
186
+
187
+ Same pattern as `queue.test.js`:
188
+ - Remove `createClient` import and `redis` variable.
189
+ - `new Client(redis, 1)` → `new Client({}, 1)`.
190
+ - Direct Redis calls (`redis.zScore`, `redis.keys`, `redis.del`) move to a
191
+ separate `redisInspect` client.
192
+ - Remove `await redis.quit()` from `afterEach`; `client.close()` handles it.
193
+
194
+ ---
195
+
196
+ ## 7. Changes to `test/redis-functions.test.js`
197
+
198
+ This test file does **not** use the `Client` class — it builds its own `redis`
199
+ connection to test Lua functions directly. **No changes needed.**
200
+
201
+ ---
202
+
203
+ ## 8. Changes to `fuzztest/process.js`
204
+
205
+ Currently:
206
+ ```js
207
+ import { createClient } from 'redis';
208
+ const redis = createClient();
209
+ await redis.connect();
210
+ const client = await new Promise((resolve) => new Client(redis, WORKER_THREADS, resolve));
211
+ ```
212
+
213
+ Change to:
214
+ ```js
215
+ const client = await new Promise((resolve) => new Client({}, WORKER_THREADS, resolve));
216
+ ```
217
+
218
+ Remove the `import { createClient } from 'redis'` line and the three lines that
219
+ create and connect `redis`.
220
+
221
+ ---
222
+
223
+ ## 9. Changes to `Readme.md`
224
+
225
+ Update the `client()` API section:
226
+
227
+ **Before:**
228
+ ```
229
+ ### `client(redisConnection, workerCount)`
230
+ Returns a Queasy client.
231
+ - `redisConnection`: a node-redis connection object.
232
+ - `workerCount`: number; Size of the worker pool. ...
233
+ ```
234
+
235
+ **After:**
236
+ ```
237
+ ### `new Client(options, workerCount)`
238
+ Returns a Queasy client. Queasy creates and manages its own Redis connection internally.
239
+ - `options`: connection options. Two forms are accepted:
240
+ - **Single node** (plain object): passed to node-redis `createClient`. Accepts
241
+ `url`, `socket`, `username`, `password`, and `database`. Defaults to `{}`
242
+ (connects to `localhost:6379`).
243
+ - **Cluster** (object with `rootNodes`): passed to node-redis `createCluster`.
244
+ Accepts `rootNodes` (required — array of per-node connection options, at least
245
+ three recommended), `defaults` (options shared across all nodes, e.g. auth and
246
+ TLS), and `nodeAddressMap` (for address translation in NAT environments).
247
+ - `workerCount`: number; size of the worker pool. Defaults to number of CPUs.
248
+ ```
249
+
250
+ Also update the terminology section to reflect that the client manages its own
251
+ connection rather than accepting one from the caller.
252
+
253
+ ---
254
+
255
+ ## 10. Changes to `CLAUDE.md`
256
+
257
+ In the Architecture section, update the JS layer description:
258
+ - "On construction, it calls `createClient` or `createCluster` (based on whether
259
+ `options.rootNodes` is present), connects, then uploads the Lua script via
260
+ `FUNCTION LOAD REPLACE`."
261
+ - Remove the mention of `WeakSet (initializedClients)` tracking which Redis clients
262
+ have had functions loaded (no longer relevant since the connection is internal and
263
+ only created once per `Client` instance).
264
+
265
+ ---
266
+
267
+ ## Summary of file changes
268
+
269
+ | File | Change |
270
+ |---|---|
271
+ | `src/client.js` | Accept `RedisOptions`; call `createClient`/`createCluster`; connect in constructor; `destroy()` in `close()`; emit `'disconnected'` on connect error; default all args |
272
+ | `src/types.ts` | Add `RedisOptions` type using `Pick<RedisClientOptions, ...> \| Pick<RedisClusterOptions, ...>`; remove `RedisClientType` reference |
273
+ | `package.json` | Move `redis` from `peerDependencies`+`devDependencies` to `dependencies` |
274
+ | `test/queue.test.js` | Remove external `redis` client; pass `{}` to `Client`; add `redisInspect` for assertions |
275
+ | `test/client.test.js` | Same as above |
276
+ | `test/redis-functions.test.js` | No changes |
277
+ | `fuzztest/process.js` | Remove `createClient`/`redis`; pass `{}` to `Client` |
278
+ | `Readme.md` | Update `client()` API docs |
279
+ | `CLAUDE.md` | Update architecture description |