@oneunit/redis 0.0.0-stage → 1.0.0

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 (79) hide show
  1. package/ARCHITECTURE.md +422 -0
  2. package/CHANGELOG.md +186 -0
  3. package/CONTRIBUTING.md +353 -0
  4. package/LICENSE +21 -0
  5. package/README.md +760 -2
  6. package/dist/client/check.d.ts +19 -0
  7. package/dist/client/check.d.ts.map +1 -0
  8. package/dist/client/check.js +44 -0
  9. package/dist/client/check.js.map +1 -0
  10. package/dist/client/client.d.ts +10 -0
  11. package/dist/client/client.d.ts.map +1 -0
  12. package/dist/client/client.js +25 -0
  13. package/dist/client/client.js.map +1 -0
  14. package/dist/client/events.d.ts +5 -0
  15. package/dist/client/events.d.ts.map +1 -0
  16. package/dist/client/events.js +66 -0
  17. package/dist/client/events.js.map +1 -0
  18. package/dist/client/index.d.ts +6 -0
  19. package/dist/client/index.d.ts.map +1 -0
  20. package/dist/client/index.js +5 -0
  21. package/dist/client/index.js.map +1 -0
  22. package/dist/client/shutdown.d.ts +4 -0
  23. package/dist/client/shutdown.d.ts.map +1 -0
  24. package/dist/client/shutdown.js +78 -0
  25. package/dist/client/shutdown.js.map +1 -0
  26. package/dist/index.d.ts +5 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +5 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/logger.d.ts +23 -0
  31. package/dist/logger.d.ts.map +1 -0
  32. package/dist/logger.js +120 -0
  33. package/dist/logger.js.map +1 -0
  34. package/dist/pipeline/builder.d.ts +111 -0
  35. package/dist/pipeline/builder.d.ts.map +1 -0
  36. package/dist/pipeline/builder.js +197 -0
  37. package/dist/pipeline/builder.js.map +1 -0
  38. package/dist/pipeline/index.d.ts +3 -0
  39. package/dist/pipeline/index.d.ts.map +1 -0
  40. package/dist/pipeline/index.js +2 -0
  41. package/dist/pipeline/index.js.map +1 -0
  42. package/dist/queue/events.d.ts +13 -0
  43. package/dist/queue/events.d.ts.map +1 -0
  44. package/dist/queue/events.js +109 -0
  45. package/dist/queue/events.js.map +1 -0
  46. package/dist/queue/index.d.ts +7 -0
  47. package/dist/queue/index.d.ts.map +1 -0
  48. package/dist/queue/index.js +4 -0
  49. package/dist/queue/index.js.map +1 -0
  50. package/dist/queue/queue.d.ts +13 -0
  51. package/dist/queue/queue.d.ts.map +1 -0
  52. package/dist/queue/queue.js +37 -0
  53. package/dist/queue/queue.js.map +1 -0
  54. package/dist/queue/worker.d.ts +15 -0
  55. package/dist/queue/worker.d.ts.map +1 -0
  56. package/dist/queue/worker.js +20 -0
  57. package/dist/queue/worker.js.map +1 -0
  58. package/examples/README.md +86 -0
  59. package/examples/_setup.js +143 -0
  60. package/examples/cache.js +111 -0
  61. package/examples/pipeline.js +161 -0
  62. package/examples/pubsub.js +101 -0
  63. package/examples/queue-worker.js +189 -0
  64. package/examples/session.js +145 -0
  65. package/examples/standalone.js +58 -0
  66. package/package.json +100 -4
  67. package/src/client/check.ts +69 -0
  68. package/src/client/client.ts +45 -0
  69. package/src/client/events.ts +101 -0
  70. package/src/client/index.ts +5 -0
  71. package/src/client/shutdown.ts +97 -0
  72. package/src/index.ts +4 -0
  73. package/src/logger.ts +159 -0
  74. package/src/pipeline/builder.ts +307 -0
  75. package/src/pipeline/index.ts +7 -0
  76. package/src/queue/events.ts +158 -0
  77. package/src/queue/index.ts +6 -0
  78. package/src/queue/queue.ts +60 -0
  79. package/src/queue/worker.ts +44 -0
@@ -0,0 +1,353 @@
1
+ # Contributing to `@oneunit/redis`
2
+
3
+ Contributions are welcome — bug reports, tests, docs, and code. No CLA to sign
4
+ and no maintainer approval needed to open a pull request. The package is MIT
5
+ licensed, so your work stays yours.
6
+
7
+ Repository-wide conventions live in the [root CONTRIBUTING.md](../../CONTRIBUTING.md).
8
+ This file covers what is specific to a Redis client and queue library.
9
+
10
+ Before changing anything, read [ARCHITECTURE.md](./ARCHITECTURE.md). This
11
+ package is a thin layer over `ioredis` and `bullmq`, and most of the code here
12
+ exists to work around a specific sharp edge in one of them. Several of those
13
+ workarounds look like pointless indirection until you know which bug they
14
+ prevent.
15
+
16
+ ## Reporting a security vulnerability
17
+
18
+ **Do not open a public issue or pull request for a security report.**
19
+
20
+ A connection helper has a wide blast radius: a flaw here reaches every
21
+ application that installs it, and a public write-up with details gives attackers
22
+ a head start while the fix is written. Report privately, in order of preference:
23
+
24
+ 1. **GitHub Security Advisories** — the repository's _Security_ tab →
25
+ _Report a vulnerability_. This opens a private thread only maintainers can
26
+ see.
27
+ 2. **Email** the maintainer at `04mayank09@gmail.com`.
28
+
29
+ Please include the affected version, a minimal reproduction, and the impact you
30
+ expect. You will get an acknowledgement, and you will be credited in the release
31
+ notes unless you would rather not be.
32
+
33
+ Everything else — a confusing error message, a missing test, an API that is
34
+ awkward to use — is a normal issue and welcome as one.
35
+
36
+ ## Getting set up
37
+
38
+ Requires **Node.js 20+**. You can work directly inside `packages/redis` with **npm**, or across the monorepo using **pnpm 11.9.0** (`corepack enable` gets you the right pnpm).
39
+
40
+ ### Working directly in `packages/redis` (standalone)
41
+
42
+ ```bash
43
+ # Inside packages/redis
44
+ npm install
45
+ npm run build
46
+ npm test
47
+ ```
48
+
49
+ ### Working from the monorepo root
50
+
51
+ ```bash
52
+ # From repository root
53
+ pnpm install
54
+ pnpm --filter @oneunit/redis build
55
+ pnpm --filter @oneunit/redis test
56
+ ```
57
+
58
+ The build step is required before the tests, not optional: the suite imports
59
+ `dist`, which is gitignored and does not exist on a fresh checkout.
60
+
61
+ A **Redis on `localhost:6379`** makes the tests mean something. Without one they
62
+ still pass, but the queue, worker, pipeline, and performance cases no-op. Set
63
+ `REDIS_URL` to point somewhere else. Nothing in the suite hangs for want of a
64
+ server, but do not infer that from a green run with nothing listening.
65
+
66
+ The package typechecks, tests, and builds entirely on its own, so you do not
67
+ need the rest of the monorepo to work on it.
68
+
69
+ ## The checks a pull request must pass
70
+
71
+ Run the full verification suite before submitting a pull request:
72
+
73
+ ```bash
74
+ # Inside packages/redis:
75
+ npm run verify
76
+
77
+ # Or from monorepo root:
78
+ pnpm --filter @oneunit/redis verify
79
+ ```
80
+
81
+ `npm run verify` runs the exact five checks in order:
82
+
83
+ ```bash
84
+ npm run build # Clean dist/ and compile TypeScript
85
+ npm run typecheck # Typecheck without emitting files
86
+ npm run lint # ESLint across src/ and test/
87
+ npm test # Run test suite with Node's native test runner via tsx
88
+ npm run pack:check # Dry-run npm pack to catch packaging errors
89
+ ```
90
+
91
+ CI runs these on **Node 20, 22, and 24** against a `redis` service container,
92
+ then installs the packed tarball into a clean project and exercises the public
93
+ API against it. That last step catches the failure mode unit tests miss: an
94
+ export that only resolves inside this repository.
95
+
96
+ Because CI always has a server, it cannot catch a test that quietly requires one.
97
+ The server-backed tests no-op when nothing is listening, so a run without Redis
98
+ is green while covering a good part of nothing — and one that was missing its
99
+ guard hung the suite outright rather than failing. **Start a Redis on
100
+ `localhost:6379` before trusting a local test run**, and see
101
+ [Tests that need Redis](#tests-that-need-redis).
102
+
103
+ **Build comes first, and the order is load-bearing.** `npm test` runs against
104
+ `dist`, not `src`: every test file imports `../dist/index.js`, and `dist/` is
105
+ gitignored, so a fresh checkout cannot load the suite at all until it is built.
106
+ Running `test` before `build` does not fail an assertion — it fails to resolve
107
+ the module, on every file, every run. `npm run verify` (or `pnpm --filter @oneunit/redis verify`)
108
+ orders all five correctly; reach for it rather than the individual scripts.
109
+ `prepublishOnly` builds before testing for the same reason.
110
+
111
+ ## Writing tests
112
+
113
+ Tests use the Node built-in runner (`node:test`) via `tsx`. There is no test
114
+ framework to configure.
115
+
116
+ ```bash
117
+ # Inside packages/redis:
118
+ npm test
119
+
120
+ # Run tests in watch mode:
121
+ npm run test:watch
122
+
123
+ # From monorepo root:
124
+ pnpm --filter @oneunit/redis test
125
+ ```
126
+
127
+ ### Test Suite Structure
128
+
129
+ The test suite in `test/` is organized into focused suites:
130
+
131
+ | Test File | Scope & Responsibilities |
132
+ | :------------------------- | :---------------------------------------------------------------------------------------------------- |
133
+ | `test/redis.test.ts` | Client defaults, URL normalization, BullMQ queue/worker options, health timeout, shutdown idempotency. |
134
+ | `test/pipeline.test.ts` | Batch command execution, 1-to-1 step validation, timeout guarantees, error redaction, `pipelineValues`. |
135
+ | `test/security.test.ts` | Plaintext credential redaction (`AUTH`/`HELLO`), prototype pollution guards, safe key names. |
136
+ | `test/performance.test.ts` | Latency bounds, concurrent health checks, high-volume pipeline throughput. |
137
+ | `test/examples.test.ts` | End-to-end execution smoke tests verifying each runnable script in `examples/`. |
138
+ | `test/helpers.ts` | Shared connectivity probe (`redisAvailable()`, `cachedRedisAvailable()`). |
139
+
140
+ ### A regression test must fail without its fix
141
+
142
+ This is the rule that matters most here, and it is easy to get wrong. Write the
143
+ test, then reintroduce the bug and confirm the suite goes red. If it still
144
+ passes, the test is protecting nothing.
145
+
146
+ Two traps in this package specifically:
147
+
148
+ - **bullmq swallows listener exceptions.** `QueueBase.emit` catches a throwing
149
+ listener and re-emits the event as `error`. If that also throws, it lands on
150
+ `console.error`. So a test that only asserts "did not throw" will pass against
151
+ a broken event handler. Assert on `console.error` instead — see
152
+ `attachQueueEvents tolerates a missing logger on event dispatch`.
153
+ - **Some failures only appear in a fresh process.** The health-check deadline
154
+ bug (an `unref`'d timer letting Node exit before the promise settled) is
155
+ invisible in-process, because the test runner keeps the loop alive for other
156
+ reasons. That test spawns a child process.
157
+
158
+ ### Tests that need Redis
159
+
160
+ Anything constructing a `Queue`, `Worker`, or `QueueEvents` needs a live server:
161
+ bullmq opens a blocking connection and retries a dead port for ~30s after
162
+ `close()`, which makes a suite pointed at nothing listening both slow and
163
+ handle-leaking. Use the existing `redisAvailable()` guard so the test returns
164
+ early instead:
165
+
166
+ ```ts
167
+ test("does the thing", async () => {
168
+ if (!(await redisAvailable())) {
169
+ return;
170
+ }
171
+ // ...
172
+ });
173
+ ```
174
+
175
+ `redisAvailable()` comes from `test/helpers.ts` — import it, do not redeclare
176
+ it. It used to be copied into each test file, and the copies drifted: some had a
177
+ `Promise.race` backstop and some did not. There is now one implementation and
178
+ one `cachedRedisAvailable()` per file, so the guard that decides whether the
179
+ server-backed half of the suite silently does nothing lives in a single place.
180
+
181
+ **The guard is needed for a bare `client.ping()` too, not just for bullmq
182
+ objects.** `createClient` defaults `maxRetriesPerRequest` to `null`, which is
183
+ what bullmq requires, and `null` means ioredis never gives up on a queued
184
+ command. So against a Redis that is not listening, an unguarded `await
185
+ client.ping()` parks in the offline queue and never settles — the test, and the
186
+ whole run, hangs forever rather than failing. That is not hypothetical: three
187
+ tests in `security.test.ts` were missing the guard, one of which hung the suite
188
+ indefinitely on a machine with no server. CI does not catch it, because
189
+ `.github/workflows/redis.yml` starts a Redis service for the `verify` job.
190
+
191
+ Tests for pure functions (`createClient` defaults, `health` against a fake
192
+ client, `shutdown` against a fake client) need no server. Prefer those; they are
193
+ fast and deterministic. `health` accepts any object with a `ping` method, and
194
+ `shutdown` any object with `status` and `quit`, so both are testable without
195
+ ioredis.
196
+
197
+ Two more rules that the suite has broken in the past:
198
+
199
+ - **A test must not rebuild `dist/`.** Every test file imports `../dist/index.js`
200
+ and node's test runner executes files in parallel child processes, so anything
201
+ that mutates the build mid-run takes `dist/` away from its siblings and
202
+ produces `ERR_MODULE_NOT_FOUND` attributed to the wrong file. If a test needs
203
+ the published file list, use `npm pack --dry-run --json --ignore-scripts`;
204
+ without that flag `prepack` runs `build`, whose `clean` step deletes `dist/`.
205
+ - **Proven red before green.** See the rule above; it applies to every new
206
+ regression test, including a fix whose "before" state is a hang.
207
+
208
+ ## Working with ioredis and bullmq defaults
209
+
210
+ The single most common bug in this package's history has been forwarding an
211
+ explicit `undefined` to bullmq, which overrides a default it would otherwise
212
+ have applied. Both factories spread conditionally:
213
+
214
+ ```ts
215
+ ...(concurrency === undefined ? {} : { concurrency }),
216
+ ```
217
+
218
+ Do not "simplify" that to `concurrency,`. If you add an option, follow the
219
+ same pattern. The same applies to `createClient`: `url` must stay the first
220
+ positional argument to the ioredis constructor, because ioredis ignores a `url`
221
+ key inside the options object and silently connects to `localhost`.
222
+
223
+ `createQueue`'s `defaultJobOptions` cannot use the conditional-spread form,
224
+ because it merges into an existing object of defaults. It merges per key instead,
225
+ skipping `undefined`. Do not "simplify" that to `...defaultJobOptions` either: a
226
+ spread writes `undefined` over every default the caller did not set. `null` is
227
+ deliberate and passes through — bullmq reads `removeOnComplete: null` as "keep
228
+ this job".
229
+
230
+ The inverse trap is just as quiet: a `url` passed as a **string** must not be
231
+ destructured as an options object either. Both mistakes end up on
232
+ `localhost:6379` with no error. `createClient` accepts a string for exactly
233
+ that reason.
234
+
235
+ Logger argument order is the other thing to get right. Every call site uses
236
+ `(message, extra)`, matching the `Logger` interface in `src/logger.ts`. pino is
237
+ the exception and takes `(bindings, message)`, so it is detected by the
238
+ `bindings()` method and `levels` map it carries — **not** by `child()`, which
239
+ this package's own interface declares and therefore cannot be a signal.
240
+
241
+ Pipelines have the same two failure modes in a new place. A command that fails
242
+ inside a pipeline does not fail the pipeline: ioredis resolves `EXEC` and puts
243
+ the error in that command's tuple, so anything that reads only the values treats
244
+ a dropped write as a success. And a pipeline `exec()` against an unreachable
245
+ server never settles, because ioredis parks queued commands while reconnecting.
246
+ `runPipeline` exists for both; keep both properties if you extend it. Anything
247
+ reaching a logger or an exception from a pipeline result goes through
248
+ `redactError` first — the step array is meant to be loggable as a whole, so the
249
+ redaction has to have already happened by the time it is returned. That applies
250
+ to a rejected `exec()` as well as to the resolved tuples; a rejection skips the
251
+ tuple mapping entirely, so redacting only the tuples leaves one path unguarded.
252
+
253
+ Results are paired with steps by **position**, so each step must queue exactly
254
+ one command. `runPipeline` enforces it by reading ioredis's own
255
+ `pipeline.length` either side of every step and raising `PipelineStepError`; do
256
+ not relax that check. Two ways of breaking it compile silently, because
257
+ TypeScript allows any value to be returned from a `void`-typed signature:
258
+
259
+ ```ts
260
+ { label: "seed", run: (p) => { p.set("a", "1"); p.set("b", "2"); } },
261
+ { label: "x", run: async (p) => { await something(); void p.get("a"); } },
262
+ ```
263
+
264
+ The first shifts every later result by one, so the caller gets a real value
265
+ against the wrong label. The second queues nothing before `exec()` is sent and
266
+ loses the command with no error anywhere. If you change how steps are queued,
267
+ keep a test that a two-command step and an `async` step both raise — and note
268
+ that a test fake standing in for ioredis has to expose a `length` that tracks
269
+ the queue, or the guard cannot be exercised at all.
270
+
271
+ Anywhere an error reaches the logger, it goes through `redactError(error)`
272
+ first. ioredis attaches the failing command to its errors, and for `AUTH` that
273
+ command's args are the password in plaintext. There are three such call sites —
274
+ the client `error` event, the `QueueEvents` `error` event, and `shutdown`'s
275
+ failure report — and adding a fourth without redacting writes the credential to
276
+ the app's log.
277
+
278
+ ## Things that look like bugs but are not
279
+
280
+ - **`close` logs on every reconnect.** ioredis emits `close` on each failed
281
+ attempt during an outage. That is expected, not a leak.
282
+ - **One client shared by queue and worker is fine.** bullmq duplicates the
283
+ connection for its blocking needs; closing the queue or worker does not close
284
+ your client. That is what `shutdown` is for.
285
+ - **`prefix` is not global.** It is per-call on all three factories. Set it on
286
+ every one, or let `attachQueueEvents` inherit it from the queue.
287
+ - **`maxRetriesPerRequest` is `null` on purpose.** ioredis defaults to `20` and
288
+ bullmq refuses anything else. See ARCHITECTURE.md.
289
+ - **`status` can stay `reconnecting` after a forced close.** `disconnect()` never
290
+ runs ioredis's `closeHandler` from a client in `reconnecting`, so the status
291
+ does not move to `end` even though the retry loop is gone. `shutdown` keeps its
292
+ own record for exactly this reason; do not add a status check expecting
293
+ otherwise.
294
+
295
+ ## Publishing and Releasing
296
+
297
+ `npm run verify` is the whole gate in order: build, typecheck, lint, tests,
298
+ `pack:check`. Run it rather than the individual scripts — the pack check is what
299
+ catches a packaging mistake that no amount of unit testing will.
300
+
301
+ Two packaging rules, both learned the hard way:
302
+
303
+ - **`src` must stay in `files`.** The compiler emits sourcemaps that reference
304
+ `../src/*.ts`. Without it they resolve to nothing in an installed package, and
305
+ consumer stack traces silently degrade.
306
+ - **`build` must clean `dist`.** `tsc` never removes output for a deleted source
307
+ file, so a module you delete keeps shipping its compiled copy. Do not "simplify"
308
+ the clean step away.
309
+
310
+ The package is configured for public npm distribution under the `@oneunit` scope:
311
+
312
+ ```json
313
+ "publishConfig": {
314
+ "access": "public",
315
+ "registry": "https://registry.npmjs.org/"
316
+ }
317
+ ```
318
+
319
+ ### Direct npm CLI Publishing
320
+
321
+ `package.json` defines `"prepublishOnly": "npm run build && npm test"`, ensuring
322
+ a clean build and full test execution precede every publish:
323
+
324
+ ```bash
325
+ # Inside packages/redis:
326
+ npm login
327
+ npm publish
328
+ ```
329
+
330
+ ### Automated CI Release Workflow
331
+
332
+ Releases are also driven by git tags: `git tag redis-v1.0.0 && git push origin redis-v1.0.0`.
333
+ `.github/workflows/redis.yml` verifies on Node 20/22/24, installs
334
+ the real tarball into a clean project and exercises the public API against a
335
+ Redis service container, then checks the tag matches `package.json` before
336
+ uploading. npm will not let you reuse a version number, so a mistyped tag is not
337
+ retryable.
338
+
339
+ ## Docs and naming
340
+
341
+ The package is published as `@oneunit/redis`. Internal links, install commands,
342
+ and the repository URL should all say `oneunit`; the old `bootstrap-framework`
343
+ name is retired. Anything in this monorepo that still imports the redis plugin
344
+ by the old name will not resolve, and the server's optional redis plugin reports
345
+ that as "package not installed" rather than naming the fault. Update
346
+ `CHANGELOG.md` under an `Unreleased` heading as part of your change — a bug fix
347
+ without a changelog entry will be asked for.
348
+
349
+ ## Examples
350
+
351
+ `examples/` is shipped in the tarball and exercised by CI, so an example that
352
+ does not run is a broken build. Each script reads `REDIS_URL` and honours
353
+ `REDIS_SILENT=true`. Check yours against a local Redis before sending it.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mayank
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.