enqiu 0.1.2 → 0.4.0-beta.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,297 @@
3
3
  All notable changes to this project are documented here. The project follows
4
4
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## Unreleased
7
+
8
+ ## [0.4.0-beta.0] - 2026-08-07
9
+
10
+ Rewritten from a blank file. The idea is unchanged — define a job once, call it
11
+ like a function — but the previous version was the accumulated result of a
12
+ queue implementation that became a wrapper, and it showed. This is the shape it
13
+ would have had if it had been a wrapper from the start.
14
+
15
+ ### Breaking
16
+
17
+ - **`enqiu()` returns `{ jobs, queue, worker, bull, close }`**, meant to be
18
+ destructured. Your jobs live under `jobs`, so **no job name is reserved any
19
+ more** — a job called `queue` is `jobs.queue`. The runtime check that used to
20
+ throw on `queue`, `worker` and `bull` is gone, along with the two casts that
21
+ were needed to build the old combined object at all.
22
+ - **`close()` is returned by `enqiu()`** rather than sitting on `worker`. It
23
+ ends the queue, the worker and the event stream, which is what it always did
24
+ — including on a producer-only queue that has no worker to close.
25
+ - **`worker.onIdle()` is now `queue.onIdle()`.** It measures the queue, and
26
+ every process sharing that queue gets the same answer.
27
+ - **`queue.list()` requires a `status`,** and its cursor carries one offset per
28
+ underlying BullMQ state. A single number meant "position within each state",
29
+ which skipped and repeated jobs whenever a status spanned more than one.
30
+ - **`JobHandle.status` is gone.** It could only report what was true when the
31
+ handle was made, and went quietly stale while you held it. Use `refresh()`.
32
+ - **`validatePayloads` is gone** and the check always runs. Measured honestly it
33
+ is worth about a point of a two-point overhead, which is not a trade worth
34
+ offering.
35
+ - **Telemetry is removed.** Every event it emitted was either BullMQ's own —
36
+ available on `bull.worker.on(...)` — or reconstructible from one. A hook that
37
+ forwards another library's events under new names is a second vocabulary for
38
+ the same thing.
39
+ - **`queue.pause`, `queue.resume`, `queue.setConcurrency`, `worker.pause` and
40
+ `worker.resume` are removed.** All five forwarded to BullMQ and added
41
+ nothing. They are `bull.queue.pause()`, `.resume()`,
42
+ `.setGlobalConcurrency(n)`, `bull.worker.pause()` and `.resume()`. What
43
+ remains on `queue` and `worker` is what Enqiu types or computes.
44
+ - Dead surface removed: `encodeJobValue`, `decodeJobValue`, `cloneJobValue`,
45
+ `serializeError`, `SubmitOptions.timeout`/`expiresIn`, the `"expired"` status,
46
+ `JobSnapshot.logs`, `TelemetryEvent.job`, `SerializedError.stack`,
47
+ `ScheduleHandle.nextRunAt`, and `BulkOptions.idempotencyKey` — which applied
48
+ one key to a whole batch and collapsed it into a single job.
49
+
50
+ ### Fixed
51
+
52
+ - **A job's failure is no longer reconstructed by reading its error message.**
53
+ `awaitResult` matched on the strings `"timed out after"` and `"expired before
54
+ it could start"`, both produced elsewhere in this repo — so rewording either
55
+ message silently changed which class callers caught, with no test to notice.
56
+ The kind is now written into `failedReason` and read back. Snapshots gained
57
+ real error names as a side effect, instead of every failure being `"Error"`.
58
+ - **A cancelled job keeps its own name and input.** The marker stored only a
59
+ reason, so once the job was removed the snapshot was invented: `name:
60
+ "unknown"`, `input: undefined`, `attempt: 0`. It now stores the snapshot taken
61
+ before removal.
62
+ - **`cleanup({ status })` cleaned the wrong jobs** for every status but
63
+ `"failed"`, so `{ status: "running" }` deleted successes. It now cleans every
64
+ state a status is made of — cleaning only the first left prioritized jobs
65
+ behind and reported success.
66
+ - **`EnqiuOptions.retry` had no effect**, though its neighbour `timeout` did.
67
+ - **Cancellation markers used ioredis' positional `HSET`**, which would throw on
68
+ the node-redis and Bun clients BullMQ 6 adapts.
69
+ - **`worker.running` and the closed check were mirrored state**, so pausing or
70
+ closing through `bull` left them claiming otherwise. Both read from BullMQ.
71
+ - **`worker.start()` left a paused worker resumed but not running.** BullMQ's
72
+ `resume()` restarts the main loop only if it has already exited, and right
73
+ after a pause it is still unwinding; the resume was not awaited, so the
74
+ `isRunning()` check saw a live loop that was about to die. A wrapper that has
75
+ since been deleted was adding the microtask that hid this.
76
+ - **`test/**` was never type-checked.** The base config excludes `test`, and
77
+ `exclude` is inherited rather than replaced by a child's `include` — so
78
+ `typecheck:test` silently skipped the suite. Every `expectTypeOf` and
79
+ `@ts-expect-error` in it was inert. They are checked now.
80
+
81
+ ### Packaging
82
+
83
+ - **`dist` is cleaned before every build.** `tsc` never removes files from its
84
+ `outDir`, so every module deleted since 0.1.x was still sitting there —
85
+ `dist/memory/`, `dist/drivers/`, `dist/redis/`, `dist/cron.*` — and would have
86
+ shipped. The tarball went from 86 files / 84.5 kB to 26 / 31.5 kB.
87
+ - **The beta dist-tag lives in a `release:beta` script.** npm 11 does not honour
88
+ `publishConfig.tag`: a plain `npm publish` resolves to `latest`, which would
89
+ have handed a beta to every `npm install enqiu`.
90
+
91
+ ### Changed
92
+
93
+ - `src/` is ten modules by concern rather than one 1,000-line facade. The
94
+ BullMQ vocabulary mapping is pure and now has its own suite: a run without
95
+ `ENQIU_TEST_REDIS_URL` verifies something instead of nothing, and holds
96
+ `mapping.ts` and `serialize.ts` to their own coverage thresholds.
97
+ - 92 tests, 99% statements and 91% branches against a real Redis; 45 of them
98
+ need no server at all.
99
+ - The cancellation marker stores the finished snapshot itself rather than a
100
+ reason beside a copy of one. It was three records of one event that had to
101
+ agree, reassembled differently by each of the two readers.
102
+ - `AbortSignal.any` replaces twelve lines of hand-rolled signal forwarding.
103
+ - `queue.get()` and `handle.refresh()` fetch the job and its state together
104
+ instead of one after the other — `getJobState` needs only the id, so waiting
105
+ for the job first spent a round trip for nothing (~300µs on a local Redis).
106
+ - `handle.result` checks the stored failure envelope before reading the
107
+ cancellation marker: the envelope is free and the marker is a round trip.
108
+ - The benchmark uses BullMQ's own `removeAllQueueData` rather than `KEYS`,
109
+ which is O(keyspace) and blocks the server being timed.
110
+ - Telemetry has one module that owns the event vocabulary and the envelope.
111
+ The names were inline string literals at three sites across two files, with
112
+ `{ type, queue, timestamp, fields }` rebuilt by hand at each — adding an
113
+ event meant knowing which file to open. Where events fire is unchanged;
114
+ telemetry is cross-cutting and centralising that would be worse.
115
+ - The paging arithmetic behind `queue.list()` is a pure function, so the
116
+ calculation a single-number cursor got wrong is now covered without a server.
117
+ It takes each state's offset attached to its items rather than as a second
118
+ array, which makes a length mismatch between the two unrepresentable.
119
+ - `queue.on()` no longer asks Redis for a state its own event already carried.
120
+ - `close()` shuts the three connections down concurrently; `queue.onIdle()`
121
+ backs its poll off from 20ms to 250ms.
122
+ - Telemetry now receives the worker's completions, failures and stalls, not
123
+ only what Enqiu itself does.
124
+ - The overhead benchmark interleaves its contestants and gives raw BullMQ the
125
+ same connection handling as Enqiu. Sharing one ioredis between Queue and
126
+ Worker had made the floor read slower than the wrapper built on it, inflating
127
+ every previously published figure. The measured cost is about 2%, or 3% with
128
+ Zod validation.
129
+
130
+ ## [0.3.0-alpha.0] - 2026-08-06
131
+
132
+ Enqiu is now a typed layer over [BullMQ](https://bullmq.io) rather than a queue
133
+ implementation. BullMQ owns storage, scheduling and execution; Enqiu owns the
134
+ developer experience — inferred job names, schema-validated input, and one
135
+ object you call like a function.
136
+
137
+ This reverses the direction taken in 0.1.0, which replaced an earlier BullMQ
138
+ wrapper with first-party drivers.
139
+
140
+ ### Breaking
141
+
142
+ - **`connection` is required.** `enqiu()` now takes a BullMQ connection instead
143
+ of a driver. `driver`, `redis()` and `DriverEnqiuOptions` are gone.
144
+ - **The in-memory driver is removed, and with it browser support.** BullMQ
145
+ requires Redis and Node, so the `browser` export has been dropped. An in-tab,
146
+ non-durable queue is no longer possible.
147
+ - **Zero runtime dependencies is no longer true.** `bullmq` and `ioredis` are
148
+ peer dependencies.
149
+ - **Per-key concurrency, per-key throttling and debounce are removed.** BullMQ's
150
+ open-source tier offers one global `{ max, duration }` limiter per worker;
151
+ per-key grouping is a BullMQ Pro feature. These are absent rather than faked.
152
+ - **`historyLimit`, `rateLimit` and `catchUp` schedules are removed**, having no
153
+ open-source equivalent. `logLimit` maps onto BullMQ's `keepLogs`.
154
+ - `JobSnapshot` is reshaped around BullMQ's job model: no `priority`, `retries`,
155
+ `runAt` or `expiresAt`; `logs` are strings rather than structured entries.
156
+
157
+ ### Kept
158
+
159
+ - `job()`, full type inference, and Standard Schema validation.
160
+ - Per-attempt `timeout` with an `AbortSignal`, and `expiresIn`. BullMQ has
161
+ neither; Enqiu enforces both around the handler.
162
+ - The serialization guard, which still reports the exact path of an
163
+ unserialisable value.
164
+ - `queue` and `worker` control surfaces, mapped onto BullMQ.
165
+
166
+ ### Removed
167
+
168
+ - `src/memory/*`, `src/redis/*`, `src/drivers/*`, the `QueueDriver` seam, the
169
+ cron parser and 728 lines of Lua — roughly 4,000 lines.
170
+ - Five of the ten scenarios, which demonstrated features that no longer exist.
171
+ - The BullMQ comparison benchmark, which no longer has two things to compare.
172
+
173
+ ## [0.2.0-alpha.0] - 2026-08-06
174
+
175
+ Enqiu is now labelled alpha and published under the `alpha` dist-tag, so
176
+ `npm install enqiu` will not resolve to it. It is not suitable for production:
177
+ the API will keep breaking without a deprecation period, and neither the
178
+ durability guarantees nor the Redis driver have been validated under sustained
179
+ real-world load.
180
+
181
+ ### Breaking
182
+
183
+ - `RedisEnqiuOptions` is now `DriverEnqiuOptions`, and its `driver` field is
184
+ typed as `DriverFactory` rather than `RedisDriver`. Any driver factory is
185
+ accepted, which is what makes the backend an extension point rather than a
186
+ hard-coded pair.
187
+ - The Redis driver now rejects a `historyLimit` below 1 instead of silently
188
+ clamping it. The in-memory driver still accepts 0; Redis trims its terminal
189
+ lists with `LTRIM`, which cannot express "retain nothing".
190
+
191
+ ### Added
192
+
193
+ - A `QueueDriver` seam (`src/driver.ts`) that both backends implement, with
194
+ `DriverFactory` carrying its own queue constructor.
195
+ - Cursor pagination for `queue.list()` on the in-memory driver, matching the
196
+ Redis driver's offset semantics.
197
+ - `pnpm run test:coverage`, with thresholds enforced in `vitest.config.ts`.
198
+
199
+ ### Changed
200
+
201
+ - The package is tree-shakable. `api.ts` no longer imports the Redis driver, so
202
+ a memory-only bundle drops from 137,786 to 72,562 bytes (-47%), losing all 215
203
+ lines of Lua and the `RedisQueue` class. Importing `redis()` still pulls them
204
+ in, as it should.
205
+ - `MemoryQueue` keeps live per-status counters instead of scanning its record
206
+ map, so `size` and `stats` are O(1). Pushing 20,000 jobs through a
207
+ concurrency-64 queue went from 3,102ms to 144ms (6,447 to 139,370 jobs/sec).
208
+ - Validators, error serialization, and backoff arithmetic moved into
209
+ `src/internal/`, replacing per-driver copies. Both drivers now reject a
210
+ non-finite or negative backoff delay rather than the Redis driver clamping it.
211
+
212
+ ### Fixed
213
+
214
+ - **`enqiu()` did not type-check with a real schema.** Its copy of the Standard
215
+ Schema types omitted the explicit `| undefined` the spec puts on every
216
+ optional property, so a Zod, Valibot or ArkType schema failed to assign under
217
+ `exactOptionalPropertyTypes`. `JobDefinition` also pinned the schema to
218
+ `StandardSchemaV1<unknown, unknown>`, and since `JobHandler` is contravariant
219
+ in its input — and the generic is invariant in `Schema` — a handler typed to
220
+ its own schema was rejected. The result was that the README's headline
221
+ example did not compile: `jobs.sendEmail` came out `possibly undefined` and
222
+ its result `unknown`. No test caught it because every test used a hand-rolled
223
+ schema with `types: undefined`.
224
+ - **`when` retry predicates and function backoffs were silently dropped by the
225
+ Redis driver.** They were treated as data to be serialised, when they are
226
+ code: a worker always holds the definition of the job it runs, so it can
227
+ resolve them locally. It now does, and a function backoff is no longer
228
+ written to Redis at all.
229
+ - **`historyLimit: 0` was rejected by the Redis driver.** `LTRIM` cannot express
230
+ an empty window (`LTRIM k 0 -1` keeps everything), so the scripts now delete
231
+ the list instead. Both drivers accept 0 and retain nothing.
232
+ - **`close({ drain: true })` abandoned queued work on Redis.** It stopped the
233
+ worker before draining, so only already-claimed jobs finished while the
234
+ in-memory driver finished the whole backlog. The Redis driver now keeps its
235
+ worker running until the backlog clears, ignoring other workers' in-flight
236
+ jobs so a deploy does not block on them.
237
+ - **`worker.onIdle()` was effectively a no-op on the Redis driver.** It waited
238
+ only on work this process had already claimed, so calling it right after
239
+ submitting — before the poll loop had claimed anything — returned immediately
240
+ with a full backlog. The in-memory driver waited on queued work too, so the
241
+ same code behaved differently on each driver. `onIdle()` now waits for the
242
+ queue to be drained; `close()` still drains only in-flight work, because the
243
+ worker is already stopped by then.
244
+ - **Queue events never fired on the Redis driver.** `node-redis` returns `XREAD`
245
+ as an object keyed by stream name (and a `Map` under RESP3), while RESP2
246
+ clients and Bun's return a nested array. The stream parser bailed on anything
247
+ that was not an array, so it always produced zero entries — `queue.on(...)`
248
+ and telemetry forwarding subscribed successfully and then stayed silent
249
+ forever. `XREVRANGE` does return an array, which is why cursor setup worked
250
+ and hid the fault.
251
+ - A bad `logLimit` was reported as `historyLimit must be a non-negative integer`,
252
+ because the validator hardcoded the wrong field name.
253
+ - `queue.list({ cursor })` was honoured by Redis and silently ignored in memory,
254
+ so code that paginated correctly against Redis re-read page one forever
255
+ against the in-memory driver.
256
+ - `list.limit` was range-checked by Redis but not by the in-memory driver.
257
+ - `MemoryQueue.cleanup` kept scanning every remaining record after it reached
258
+ the requested limit.
259
+ - Removed two unused functions from the Redis driver that `noUnusedLocals`,
260
+ being disabled, never flagged.
261
+
262
+ ### Internal
263
+
264
+ - Test count rose from 36 to 136, all of which now run: the Redis suite was
265
+ verified against Redis 7.4.10 through `node-redis`. Coverage is 96.65% of
266
+ statements without Redis and 95.26% with it. `src/internal/` is fully covered.
267
+ - Added ten runnable, self-asserting usage scenarios under `examples/scenarios/`,
268
+ chosen from documented workloads rather than intuition. The reasoning and its
269
+ evidence grades are in `docs/use-case-research.md`. Running them against a
270
+ live Redis is what surfaced the `onIdle()` defect above.
271
+ - Fixed test isolation in the Redis suite. It called `flushDb()` before every
272
+ test, wiping a database that Vitest's parallel test files were using at the
273
+ same time, which made `examples/testing/jobs.redis.test.ts` fail roughly one
274
+ run in three. Each test now owns a namespace and cleans up only its own keys,
275
+ which is what the README already claimed. Doing that exposed a second latent
276
+ coupling: the cron test hardcoded the default `enqiu:` key prefix.
277
+ - Resolved Airbnb style-guide violations in `src`: `no-plusplus`,
278
+ `no-underscore-dangle`, `no-nested-ternary`, and an unused import.
279
+
280
+ ## [0.1.3] - 2026-08-03
281
+
282
+ ### Added
283
+
284
+ - Added an explicit browser export for the in-memory queue, with browser usage
285
+ guidance and a live React landing example powered by the packaged Enqiu runtime.
286
+ Browser queues run in the current tab and are intentionally non-durable;
287
+ Redis remains a server-runtime driver.
288
+ - Added a React queue playground at `/playground` for composing, running,
289
+ inspecting, cancelling, retrying, and redriving real in-browser jobs.
290
+
291
+ ### Fixed
292
+
293
+ - Allowed strict TypeScript handlers to combine typed inputs with `JobContext`.
294
+ - Made memory-driver cleanup honor requested terminal statuses instead of
295
+ removing every terminal job.
296
+
6
297
  ## [0.1.2] - 2026-08-02
7
298
 
8
299
  ### Added