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