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/README.md
CHANGED
|
@@ -1,286 +1,183 @@
|
|
|
1
1
|
# Enqiu
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/enqiu)
|
|
4
|
+
[](https://bullmq.io)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A type-safe job API on top of [BullMQ](https://bullmq.io). Define each job once
|
|
8
|
+
with a schema, then call it like a function — the name, input and result types
|
|
9
|
+
are inferred, so there is no separate registry and no string-keyed dispatch.
|
|
10
|
+
|
|
11
|
+
BullMQ owns storage, scheduling and execution. Enqiu owns the developer
|
|
12
|
+
experience.
|
|
13
|
+
|
|
14
|
+
[npm](https://www.npmjs.com/package/enqiu) ·
|
|
15
|
+
[Issues](https://github.com/moji2002/enqiu/issues)
|
|
16
|
+
|
|
17
|
+
## Stability
|
|
18
|
+
|
|
19
|
+
> [!NOTE]
|
|
20
|
+
> The hard parts — storage, retries, scheduling and crash recovery — are
|
|
21
|
+
> BullMQ's, which is mature and widely deployed. Enqiu is the typed layer in
|
|
22
|
+
> between: covered at 99% of statements and 91% of branches against a real
|
|
23
|
+
> Redis, with the two parts that need no server — the BullMQ vocabulary mapping
|
|
24
|
+
> and the serialization check — held to their own thresholds in either mode.
|
|
5
25
|
|
|
6
26
|
```bash
|
|
7
|
-
|
|
27
|
+
npm install enqiu bullmq ioredis
|
|
8
28
|
```
|
|
9
29
|
|
|
10
|
-
|
|
11
|
-
|
|
30
|
+
`bullmq` and `ioredis` are peer dependencies — Enqiu does not pick versions or
|
|
31
|
+
open connections for you.
|
|
12
32
|
|
|
13
33
|
## Quick start
|
|
14
34
|
|
|
15
|
-
Define each job once, then call it like a function. The name, input, and result
|
|
16
|
-
types are inferred.
|
|
17
|
-
|
|
18
35
|
```ts
|
|
19
36
|
import { enqiu, job } from "enqiu";
|
|
20
37
|
import { z } from "zod";
|
|
21
38
|
|
|
22
|
-
const jobs = enqiu(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
to: z.
|
|
26
|
-
|
|
39
|
+
const { jobs, queue, worker, close } = enqiu(
|
|
40
|
+
{
|
|
41
|
+
sendEmail: job({
|
|
42
|
+
input: z.object({ to: z.string(), subject: z.string() }),
|
|
43
|
+
retry: { attempts: 3, backoff: { type: "exponential", delay: 500 } },
|
|
44
|
+
timeout: 30_000,
|
|
45
|
+
run: async (input, { log }) => {
|
|
46
|
+
log.info("sending", { to: input.to });
|
|
47
|
+
return { delivered: true, subject: input.subject };
|
|
48
|
+
},
|
|
27
49
|
}),
|
|
28
|
-
run: async (email, { signal, log }) => {
|
|
29
|
-
log.info("Sending email", { to: email.to });
|
|
30
|
-
|
|
31
|
-
const response = await fetch("https://example.com/email", {
|
|
32
|
-
method: "POST",
|
|
33
|
-
body: JSON.stringify(email),
|
|
34
|
-
signal,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
return { delivered: response.ok };
|
|
38
|
-
},
|
|
39
|
-
}),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const delivery = await jobs.sendEmail({
|
|
43
|
-
to: "hello@example.com",
|
|
44
|
-
subject: "Welcome",
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const result = await delivery.result;
|
|
48
|
-
console.log(result.delivered);
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
`await jobs.sendEmail(input)` waits until the queue accepts the job and returns
|
|
52
|
-
a handle. It does not wait for the handler. Await `handle.result` only when the
|
|
53
|
-
caller needs the result. Ignoring a handle is safe and does not create an
|
|
54
|
-
unhandled rejected promise.
|
|
55
|
-
|
|
56
|
-
Schemas are optional. A plain handler also infers its result:
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
const jobs = enqiu({
|
|
60
|
-
resizeImage: async (input: { key: string; width: number }) => {
|
|
61
|
-
return { key: input.key, width: input.width };
|
|
62
50
|
},
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
between cases. This Vitest example uses the in-memory driver, awaits the public
|
|
70
|
-
job handle, checks the persisted status, and always closes the worker:
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
74
|
-
import { createJobs } from "./jobs.js";
|
|
75
|
-
|
|
76
|
-
let jobs: ReturnType<typeof createJobs> | undefined;
|
|
77
|
-
|
|
78
|
-
afterEach(async () => {
|
|
79
|
-
await jobs?.worker.close();
|
|
80
|
-
jobs = undefined;
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe("email jobs", () => {
|
|
84
|
-
it("returns and stores the handler result", async () => {
|
|
85
|
-
jobs = createJobs();
|
|
86
|
-
const handle = await jobs.sendWelcome({ name: "Ada" });
|
|
87
|
-
|
|
88
|
-
await expect(handle.result).resolves.toEqual({ subject: "Welcome, Ada" });
|
|
89
|
-
expect((await handle.refresh()).status).toBe("succeeded");
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Install and run it with:
|
|
51
|
+
{
|
|
52
|
+
name: "notifications",
|
|
53
|
+
connection: { host: "localhost", port: 6379 },
|
|
54
|
+
worker: { concurrency: 10 },
|
|
55
|
+
},
|
|
56
|
+
);
|
|
95
57
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
pnpm add -D vitest
|
|
99
|
-
pnpm vitest run
|
|
58
|
+
const handle = await jobs.sendEmail({ to: "a@b.c", subject: "Welcome" });
|
|
59
|
+
const result = await handle.result; // { delivered: boolean; subject: string }
|
|
100
60
|
```
|
|
101
61
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
namespace instead of flushing the database, and closes the worker before the
|
|
106
|
-
injected Redis client.
|
|
62
|
+
`await jobs.sendEmail(input)` resolves once BullMQ accepts the job and returns a
|
|
63
|
+
handle. It does not wait for the handler. Await `handle.result` only when the
|
|
64
|
+
caller needs the result.
|
|
107
65
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
library. It accepts Bun's `send(command, args)` client shape and node-redis'
|
|
112
|
-
`sendCommand(args)` shape.
|
|
113
|
-
|
|
114
|
-
```ts
|
|
115
|
-
import { createClient } from "redis";
|
|
116
|
-
import { enqiu, redis } from "enqiu";
|
|
117
|
-
|
|
118
|
-
const client = createClient({ url: process.env.REDIS_URL });
|
|
119
|
-
await client.connect();
|
|
120
|
-
|
|
121
|
-
const jobs = enqiu(definitions, {
|
|
122
|
-
name: "notifications",
|
|
123
|
-
driver: redis(client),
|
|
124
|
-
worker: { concurrency: 20 },
|
|
125
|
-
});
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Use the same definitions in a producer-only process:
|
|
66
|
+
`jobs` holds your jobs and nothing else, which is why no job name is reserved —
|
|
67
|
+
`jobs.queue` is a job you called `queue`. The queue and worker controls sit
|
|
68
|
+
beside it:
|
|
129
69
|
|
|
130
70
|
```ts
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
worker: false,
|
|
135
|
-
});
|
|
71
|
+
await queue.stats(); // counts by status
|
|
72
|
+
await queue.onIdle(); // resolves when nothing is outstanding
|
|
73
|
+
await close(); // queue, worker and event stream
|
|
136
74
|
```
|
|
137
75
|
|
|
138
|
-
|
|
139
|
-
|
|
76
|
+
Only what Enqiu types or computes is here. Pausing a queue or a worker, setting
|
|
77
|
+
global concurrency and anything else BullMQ already exposes is `bull.queue.*`
|
|
78
|
+
and `bull.worker.*` — a second name for the same call would be one more thing
|
|
79
|
+
to learn and nothing else.
|
|
140
80
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Policies live beside the handler and keep call sites clean. Durations are
|
|
144
|
-
numbers in milliseconds, so applications may use plain numbers or a helper
|
|
145
|
-
such as `ms("30s")` without making it an Enqiu dependency.
|
|
81
|
+
A plain handler works too, with input and output still inferred:
|
|
146
82
|
|
|
147
83
|
```ts
|
|
148
|
-
const jobs = enqiu(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
accountId: z.string(),
|
|
153
|
-
}),
|
|
154
|
-
retry: {
|
|
155
|
-
attempts: 5,
|
|
156
|
-
backoff: { type: "exponential", delay: 250, jitter: 0.2 },
|
|
157
|
-
},
|
|
158
|
-
timeout: 30_000,
|
|
159
|
-
expiresIn: 5 * 60_000,
|
|
160
|
-
concurrency: {
|
|
161
|
-
limit: 2,
|
|
162
|
-
by: (input) => input.tenantId,
|
|
163
|
-
},
|
|
164
|
-
throttle: {
|
|
165
|
-
limit: 100,
|
|
166
|
-
per: 60_000,
|
|
167
|
-
burst: 10,
|
|
168
|
-
by: (input) => input.tenantId,
|
|
169
|
-
},
|
|
170
|
-
run: async (input, context) => {
|
|
171
|
-
return syncAccount(input, { signal: context.signal });
|
|
172
|
-
},
|
|
173
|
-
}),
|
|
174
|
-
});
|
|
84
|
+
const { jobs } = enqiu(
|
|
85
|
+
{ resizeImage: async (input: { key: string; width: number }) => input },
|
|
86
|
+
{ connection },
|
|
87
|
+
);
|
|
175
88
|
```
|
|
176
89
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
-
|
|
180
|
-
|
|
181
|
-
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Per-
|
|
90
|
+
## What Enqiu adds
|
|
91
|
+
|
|
92
|
+
- **Inferred types end to end.** Job names come from the object keys; input and
|
|
93
|
+
result types come from the schema and handler. No generics to write.
|
|
94
|
+
- **Standard Schema validation at the boundary.** Zod, Valibot, ArkType or
|
|
95
|
+
anything else implementing the spec. Invalid input is rejected before a job
|
|
96
|
+
is queued.
|
|
97
|
+
- **Per-attempt `timeout`,** with an `AbortSignal` handed to the handler. BullMQ
|
|
98
|
+
has no job timeout; Enqiu enforces this itself.
|
|
99
|
+
- **`expiresIn`,** which fails a job that waited too long without running it.
|
|
100
|
+
Also enforced by Enqiu.
|
|
101
|
+
- **A serialization guard** that rejects functions, symbols, cycles and sparse
|
|
102
|
+
arrays with the exact path, instead of failing later inside the queue.
|
|
103
|
+
- **A `cancelled` status,** which BullMQ has no state for: cancelling a job that
|
|
104
|
+
has not started removes it, so Enqiu records the finished snapshot and
|
|
105
|
+
`refresh()` can still tell "cancelled" from "never existed".
|
|
106
|
+
- **Failures that survive as classes.** BullMQ hands a failure to another
|
|
107
|
+
process as one string; a timeout or an expiry writes its kind down, so
|
|
108
|
+
`handle.result` rejects with `JobTimeoutError` rather than a bare `Error`.
|
|
109
|
+
|
|
110
|
+
## Escaping the layer
|
|
111
|
+
|
|
112
|
+
Enqiu models a deliberate subset. Everything else BullMQ can do — flows, Pro
|
|
113
|
+
groups, metrics, raw job options — is one property away, with no wrapper in
|
|
114
|
+
between and no fork required:
|
|
185
115
|
|
|
186
116
|
```ts
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
priority: "high",
|
|
192
|
-
});
|
|
117
|
+
const { bull } = enqiu(definitions, { connection });
|
|
118
|
+
|
|
119
|
+
bull.queue // the real BullMQ Queue
|
|
120
|
+
bull.worker // the real BullMQ Worker, or undefined for a producer
|
|
193
121
|
```
|
|
194
122
|
|
|
195
|
-
|
|
123
|
+
Enqiu reads its own state from those objects rather than mirroring it, so
|
|
124
|
+
pausing `bull.worker` or closing `bull.queue` is seen on the Enqiu side too —
|
|
125
|
+
the two cannot drift apart.
|
|
196
126
|
|
|
197
|
-
|
|
127
|
+
Measured against raw BullMQ on the same Redis — 10,000 jobs, concurrency 32,
|
|
128
|
+
contestants interleaved, median of 7 — the typed path costs about 2%, and Zod
|
|
129
|
+
validation about 3%, varying by a point between runs. Calls through `bull` cost
|
|
130
|
+
nothing, because nothing is in the way.
|
|
198
131
|
|
|
199
|
-
|
|
200
|
-
const jobs = enqiu({
|
|
201
|
-
importRows: async (rows: string[], context) => {
|
|
202
|
-
for (let index = 0; index < rows.length; index += 1) {
|
|
203
|
-
await importRow(rows[index]);
|
|
204
|
-
await context.reportProgress({
|
|
205
|
-
completed: index + 1,
|
|
206
|
-
total: rows.length,
|
|
207
|
-
message: "Importing rows",
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
context.log.info("Import complete", { rows: rows.length });
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
```
|
|
132
|
+
Reproduce with `pnpm tsx bench/overhead.ts`.
|
|
215
133
|
|
|
216
|
-
|
|
217
|
-
inside the process; Redis events are shared between producers and workers.
|
|
134
|
+
## What BullMQ provides
|
|
218
135
|
|
|
219
|
-
|
|
136
|
+
Retries and backoff, priorities, delays, cron schedules, deduplication, bulk
|
|
137
|
+
submission, progress, logs, events and cleanup are BullMQ's, surfaced through
|
|
138
|
+
Enqiu's API.
|
|
220
139
|
|
|
221
|
-
|
|
140
|
+
## Compatibility notes
|
|
222
141
|
|
|
223
|
-
|
|
224
|
-
const schedule = await jobs.sendDigest.schedule({
|
|
225
|
-
id: "weekday-digest",
|
|
226
|
-
cron: "0 9 * * 1-5",
|
|
227
|
-
timezone: "Europe/Nicosia",
|
|
228
|
-
input: { audience: "daily" },
|
|
229
|
-
catchUp: true,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
await schedule.pause();
|
|
233
|
-
await schedule.resume();
|
|
234
|
-
await schedule.remove();
|
|
235
|
-
```
|
|
142
|
+
Enqiu deliberately does not paper over gaps in BullMQ's open-source tier:
|
|
236
143
|
|
|
237
|
-
|
|
238
|
-
|
|
144
|
+
| Not available | Why |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| Per-key concurrency (`concurrency: { by }`) | BullMQ groups are a **BullMQ Pro** feature. |
|
|
147
|
+
| Per-key rate limiting (`throttle: { by }`) | The OSS limiter is one global `{ max, duration }` per worker. |
|
|
148
|
+
| Debounce | No open-source equivalent. |
|
|
149
|
+
| In-browser queues | BullMQ requires Redis and Node. |
|
|
239
150
|
|
|
240
|
-
|
|
151
|
+
If you need any of those, use BullMQ Pro directly, or pin Enqiu 0.2.x, which
|
|
152
|
+
shipped first-party memory and Redis drivers that implemented them.
|
|
241
153
|
|
|
242
|
-
|
|
243
|
-
schema can validate an HTTP route without redefining a type:
|
|
154
|
+
## Testing
|
|
244
155
|
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
"/emails",
|
|
250
|
-
sValidator("json", jobs.sendEmail.input),
|
|
251
|
-
async (c) => {
|
|
252
|
-
const handle = await jobs.sendEmail(c.req.valid("json"));
|
|
253
|
-
return c.json({ id: handle.id }, 202);
|
|
254
|
-
},
|
|
255
|
-
);
|
|
156
|
+
```bash
|
|
157
|
+
docker run -d -p 6379:6379 redis:7-alpine
|
|
158
|
+
ENQIU_TEST_REDIS_URL=redis://localhost:6379 pnpm run check
|
|
159
|
+
ENQIU_TEST_REDIS_URL=redis://localhost:6379 pnpm run scenarios
|
|
256
160
|
```
|
|
257
161
|
|
|
258
|
-
|
|
162
|
+
Most tests need a real Redis, because most code paths go through BullMQ. The
|
|
163
|
+
exceptions are the vocabulary mapping and the serialization check, which are
|
|
164
|
+
pure and stay covered without a server — so a run without
|
|
165
|
+
`ENQIU_TEST_REDIS_URL` still verifies something rather than nothing.
|
|
259
166
|
|
|
260
|
-
|
|
167
|
+
Five runnable, self-asserting scenarios live in
|
|
168
|
+
[`examples/scenarios/`](examples/scenarios): webhook ingestion, notification
|
|
169
|
+
campaigns, report progress, a transcoding pool, and failure triage. The
|
|
170
|
+
reasoning behind the workload choices is in
|
|
171
|
+
[`docs/use-case-research.md`](docs/use-case-research.md).
|
|
261
172
|
|
|
262
|
-
|
|
263
|
-
await jobs.queue.pause();
|
|
264
|
-
await jobs.queue.resume();
|
|
265
|
-
await jobs.queue.setConcurrency(50);
|
|
266
|
-
|
|
267
|
-
const page = await jobs.queue.list({ status: "failed", limit: 100 });
|
|
268
|
-
const snapshot = await jobs.queue.get(handle.id);
|
|
269
|
-
await jobs.queue.redrive(handle.id);
|
|
270
|
-
await jobs.queue.cleanup({ olderThan: Date.now() - 7 * 24 * 60 * 60_000 });
|
|
271
|
-
|
|
272
|
-
await jobs.worker.pause();
|
|
273
|
-
await jobs.worker.resume();
|
|
274
|
-
await jobs.worker.onIdle();
|
|
275
|
-
await jobs.worker.close();
|
|
276
|
-
```
|
|
173
|
+
## Releasing
|
|
277
174
|
|
|
278
|
-
|
|
175
|
+
```bash
|
|
176
|
+
pnpm run release # npm publish
|
|
177
|
+
```
|
|
279
178
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
- Memory and Redis drivers
|
|
283
|
-
- ESM and TypeScript declarations
|
|
179
|
+
`prepack` runs the full check first, and `build` cleans `dist` before compiling,
|
|
180
|
+
since `tsc` leaves deleted modules behind and they would otherwise ship.
|
|
284
181
|
|
|
285
182
|
## License
|
|
286
183
|
|
package/dist/api.d.ts
CHANGED
|
@@ -1,236 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
readonly types?: {
|
|
18
|
-
readonly input: Input;
|
|
19
|
-
readonly output: Output;
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
export interface StandardSchemaIssue {
|
|
24
|
-
readonly message: string;
|
|
25
|
-
readonly path?: ReadonlyArray<PropertyKey | {
|
|
26
|
-
readonly key: PropertyKey;
|
|
27
|
-
}>;
|
|
28
|
-
}
|
|
29
|
-
export type InferSchemaInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];
|
|
30
|
-
export type InferSchemaOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
|
|
31
|
-
export interface Progress {
|
|
32
|
-
readonly completed: number;
|
|
33
|
-
readonly total: number;
|
|
34
|
-
readonly message?: string;
|
|
35
|
-
readonly details?: Readonly<Record<string, unknown>>;
|
|
36
|
-
}
|
|
37
|
-
export interface JobLogger {
|
|
38
|
-
debug(message: string, fields?: Readonly<Record<string, unknown>>): void;
|
|
39
|
-
info(message: string, fields?: Readonly<Record<string, unknown>>): void;
|
|
40
|
-
warn(message: string, fields?: Readonly<Record<string, unknown>>): void;
|
|
41
|
-
error(message: string, fields?: Readonly<Record<string, unknown>>): void;
|
|
42
|
-
}
|
|
43
|
-
export interface JobContext<Name extends string = string> {
|
|
44
|
-
readonly id: string;
|
|
45
|
-
readonly name: Name;
|
|
46
|
-
readonly attempt: number;
|
|
47
|
-
readonly signal: AbortSignal;
|
|
48
|
-
reportProgress(progress: Progress): Promise<void>;
|
|
49
|
-
readonly log: JobLogger;
|
|
50
|
-
}
|
|
51
|
-
export type JobHandler<Input = unknown, Output = unknown, Name extends string = string> = (input: Input, context: JobContext<Name>) => MaybePromise<Output>;
|
|
52
|
-
export interface RetryPolicy extends Omit<RetryOptions, "retries"> {
|
|
53
|
-
/** Total number of attempts, including the first. */
|
|
54
|
-
attempts: number;
|
|
55
|
-
}
|
|
56
|
-
export interface ConcurrencyPolicy<Input> {
|
|
57
|
-
limit: number;
|
|
58
|
-
by?: (input: Input) => string;
|
|
59
|
-
}
|
|
60
|
-
export interface ThrottlePolicy<Input> {
|
|
61
|
-
limit: number;
|
|
62
|
-
per: number;
|
|
63
|
-
burst?: number;
|
|
64
|
-
by?: (input: Input) => string;
|
|
65
|
-
}
|
|
66
|
-
export interface DebouncePolicy<Input> {
|
|
67
|
-
wait: number;
|
|
68
|
-
mode: "leading" | "trailing";
|
|
69
|
-
by: (input: Input) => string;
|
|
70
|
-
}
|
|
71
|
-
export interface JobPolicyOptions<Input> {
|
|
72
|
-
retry?: number | RetryPolicy;
|
|
73
|
-
timeout?: number;
|
|
74
|
-
expiresIn?: number;
|
|
75
|
-
concurrency?: number | ConcurrencyPolicy<Input>;
|
|
76
|
-
throttle?: ThrottlePolicy<Input>;
|
|
77
|
-
debounce?: DebouncePolicy<Input>;
|
|
78
|
-
}
|
|
79
|
-
export interface SchemaJobDefinition<Schema extends StandardSchemaV1 = StandardSchemaV1, Output = unknown> extends JobPolicyOptions<InferSchemaOutput<Schema>> {
|
|
80
|
-
readonly [definitionMarker]: true;
|
|
81
|
-
readonly input: Schema;
|
|
82
|
-
readonly run: JobHandler<InferSchemaOutput<Schema>, Output>;
|
|
83
|
-
}
|
|
84
|
-
export type HandlerJobDefinition<Input = unknown, Output = unknown> = JobHandler<Input, Output>;
|
|
85
|
-
export type JobDefinition = SchemaJobDefinition<StandardSchemaV1<unknown, unknown>, unknown> | HandlerJobDefinition<unknown, unknown>;
|
|
86
|
-
export type JobDefinitions = Record<string, JobDefinition>;
|
|
87
|
-
export declare function job<const Schema extends StandardSchemaV1, Output>(definition: Omit<SchemaJobDefinition<Schema, Output>, typeof definitionMarker>): SchemaJobDefinition<Schema, Output>;
|
|
88
|
-
type DefinitionInput<Definition> = Definition extends SchemaJobDefinition<infer Schema, unknown> ? InferSchemaInput<Schema> : Definition extends JobHandler<infer Input, unknown, string> ? Input : never;
|
|
89
|
-
type DefinitionRunInput<Definition> = Definition extends SchemaJobDefinition<infer Schema, unknown> ? InferSchemaOutput<Schema> : Definition extends JobHandler<infer Input, unknown, string> ? Input : never;
|
|
90
|
-
type DefinitionOutput<Definition> = Definition extends SchemaJobDefinition<StandardSchemaV1, infer Output> ? Awaited<Output> : Definition extends JobHandler<unknown, infer Output, string> ? Awaited<Output> : never;
|
|
91
|
-
export interface SubmitOptions {
|
|
92
|
-
id?: string;
|
|
93
|
-
idempotencyKey?: string;
|
|
94
|
-
/** Keep returning the same completed job for this duration. @default 24h */
|
|
95
|
-
idempotencyTtl?: number;
|
|
96
|
-
delay?: number | Date;
|
|
97
|
-
priority?: number | "low" | "normal" | "high";
|
|
98
|
-
retry?: number | RetryPolicy;
|
|
99
|
-
timeout?: number;
|
|
100
|
-
expiresIn?: number;
|
|
101
|
-
signal?: AbortSignal;
|
|
102
|
-
}
|
|
103
|
-
export interface BulkOptions extends Omit<SubmitOptions, "id"> {
|
|
104
|
-
ids?: readonly string[];
|
|
105
|
-
}
|
|
106
|
-
export interface ScheduleOptions<Input> {
|
|
107
|
-
id?: string;
|
|
108
|
-
cron: string;
|
|
109
|
-
timezone?: string;
|
|
110
|
-
input: Input;
|
|
111
|
-
catchUp?: boolean;
|
|
112
|
-
}
|
|
113
|
-
export interface ScheduleHandle {
|
|
114
|
-
readonly id: string;
|
|
115
|
-
readonly nextRunAt: number;
|
|
116
|
-
pause(): Promise<void>;
|
|
117
|
-
resume(): Promise<void>;
|
|
118
|
-
remove(): Promise<void>;
|
|
119
|
-
refresh(): Promise<ScheduleSnapshot>;
|
|
120
|
-
}
|
|
121
|
-
export interface ScheduleSnapshot {
|
|
122
|
-
id: string;
|
|
123
|
-
jobName: string;
|
|
124
|
-
cron: string;
|
|
125
|
-
timezone: string;
|
|
126
|
-
status: "active" | "paused";
|
|
127
|
-
nextRunAt: number;
|
|
128
|
-
input: unknown;
|
|
129
|
-
catchUp: boolean;
|
|
130
|
-
}
|
|
131
|
-
export interface JobHandle<Output = unknown, Input = unknown, Name extends string = string> {
|
|
132
|
-
readonly id: string;
|
|
133
|
-
readonly name: Name;
|
|
134
|
-
readonly input: Input;
|
|
135
|
-
readonly status: JobStatus;
|
|
136
|
-
readonly deduplicated: boolean;
|
|
137
|
-
readonly result: Promise<Output>;
|
|
138
|
-
cancel(reason?: string): Promise<boolean>;
|
|
139
|
-
refresh(): Promise<JobSnapshot<Input, Output, Name>>;
|
|
140
|
-
}
|
|
141
|
-
export interface JobCallable<Input, RunInput, Output, Name extends string, Schema extends StandardSchemaV1 | undefined = undefined> {
|
|
142
|
-
(input: Input, options?: SubmitOptions): Promise<JobHandle<Output, RunInput, Name>>;
|
|
143
|
-
bulk(inputs: readonly Input[], options?: BulkOptions): Promise<Array<JobHandle<Output, RunInput, Name>>>;
|
|
144
|
-
schedule(options: ScheduleOptions<Input>): Promise<ScheduleHandle>;
|
|
145
|
-
readonly input: Schema;
|
|
146
|
-
}
|
|
147
|
-
type DefinitionSchema<Definition> = Definition extends SchemaJobDefinition<infer Schema, unknown> ? Schema : undefined;
|
|
148
|
-
export type JobsApi<Definitions extends JobDefinitions> = {
|
|
149
|
-
readonly [Name in keyof Definitions]: JobCallable<DefinitionInput<Definitions[Name]>, DefinitionRunInput<Definitions[Name]>, DefinitionOutput<Definitions[Name]>, Extract<Name, string>, DefinitionSchema<Definitions[Name]>>;
|
|
150
|
-
} & {
|
|
151
|
-
readonly queue: QueueApi<Definitions>;
|
|
152
|
-
readonly worker: WorkerApi;
|
|
153
|
-
};
|
|
154
|
-
export type AnyJobSnapshot<Definitions extends JobDefinitions> = {
|
|
155
|
-
[Name in keyof Definitions]: JobSnapshot<DefinitionRunInput<Definitions[Name]>, DefinitionOutput<Definitions[Name]>, Extract<Name, string>>;
|
|
156
|
-
}[keyof Definitions];
|
|
157
|
-
export interface JobListQuery {
|
|
158
|
-
status?: JobStatus;
|
|
159
|
-
name?: string;
|
|
160
|
-
before?: number;
|
|
161
|
-
after?: number;
|
|
162
|
-
limit?: number;
|
|
163
|
-
cursor?: string;
|
|
164
|
-
}
|
|
165
|
-
export interface JobListPage<Job = JobSnapshot> {
|
|
166
|
-
jobs: Job[];
|
|
167
|
-
cursor?: string;
|
|
168
|
-
}
|
|
169
|
-
export interface CleanupQuery {
|
|
170
|
-
status?: JobStatus | readonly JobStatus[];
|
|
171
|
-
olderThan?: number;
|
|
172
|
-
limit?: number;
|
|
173
|
-
}
|
|
174
|
-
export interface QueueApi<Definitions extends JobDefinitions> {
|
|
175
|
-
get(id: string): Promise<AnyJobSnapshot<Definitions> | undefined>;
|
|
176
|
-
list(query?: JobListQuery): Promise<JobListPage<AnyJobSnapshot<Definitions>>>;
|
|
177
|
-
stats(): Promise<QueueStats>;
|
|
178
|
-
pause(): Promise<void>;
|
|
179
|
-
resume(): Promise<void>;
|
|
180
|
-
setConcurrency(limit: number): Promise<void>;
|
|
181
|
-
redrive(id: string): Promise<JobHandle>;
|
|
182
|
-
cleanup(query?: CleanupQuery): Promise<string[]>;
|
|
183
|
-
on<Event extends keyof QueueEventMap>(event: Event, listener: (payload: QueueEventMap[Event]) => void): () => void;
|
|
184
|
-
}
|
|
185
|
-
export interface WorkerStartOptions {
|
|
186
|
-
concurrency?: number;
|
|
187
|
-
}
|
|
188
|
-
export interface WorkerApi {
|
|
189
|
-
readonly running: boolean;
|
|
190
|
-
start(options?: WorkerStartOptions): Promise<void>;
|
|
191
|
-
pause(): Promise<void>;
|
|
192
|
-
resume(): Promise<void>;
|
|
193
|
-
onIdle(): Promise<void>;
|
|
194
|
-
close(options?: {
|
|
195
|
-
drain?: boolean;
|
|
196
|
-
}): Promise<void>;
|
|
197
|
-
}
|
|
198
|
-
export interface WorkerOptions {
|
|
199
|
-
concurrency?: number;
|
|
200
|
-
autoStart?: boolean;
|
|
201
|
-
}
|
|
202
|
-
export interface TelemetryEvent {
|
|
203
|
-
readonly type: string;
|
|
204
|
-
readonly queue: string;
|
|
205
|
-
readonly timestamp: number;
|
|
206
|
-
readonly job?: JobSnapshot;
|
|
207
|
-
readonly fields?: Readonly<Record<string, unknown>>;
|
|
208
|
-
}
|
|
209
|
-
export interface Telemetry {
|
|
210
|
-
emit(event: TelemetryEvent): void;
|
|
211
|
-
}
|
|
212
|
-
export interface SharedEnqiuOptions {
|
|
213
|
-
name?: string;
|
|
214
|
-
worker?: false | WorkerOptions;
|
|
215
|
-
retry?: number | RetryPolicy;
|
|
216
|
-
timeout?: number;
|
|
217
|
-
historyLimit?: number;
|
|
218
|
-
logLimit?: number;
|
|
219
|
-
telemetry?: Telemetry;
|
|
220
|
-
}
|
|
221
|
-
export interface MemoryEnqiuOptions extends SharedEnqiuOptions {
|
|
222
|
-
driver?: undefined;
|
|
223
|
-
}
|
|
224
|
-
export interface RedisEnqiuOptions extends SharedEnqiuOptions {
|
|
225
|
-
driver: RedisDriver;
|
|
226
|
-
/** Redis processes must explicitly choose producer-only or worker mode. */
|
|
227
|
-
worker: false | WorkerOptions;
|
|
228
|
-
}
|
|
229
|
-
export type EnqiuOptions = MemoryEnqiuOptions | RedisEnqiuOptions;
|
|
230
|
-
export declare class JobValidationError extends TypeError {
|
|
231
|
-
readonly issues: readonly StandardSchemaIssue[];
|
|
232
|
-
constructor(name: string, issues: readonly StandardSchemaIssue[]);
|
|
233
|
-
}
|
|
234
|
-
export declare function enqiu<const Definitions extends JobDefinitions>(definitions: Definitions, options?: MemoryEnqiuOptions): JobsApi<Definitions>;
|
|
235
|
-
export declare function enqiu<const Definitions extends JobDefinitions>(definitions: Definitions, options: RedisEnqiuOptions): JobsApi<Definitions>;
|
|
236
|
-
export { JobCancelledError, JobExpiredError, JobFailedError, JobSerializationError, JobTimeoutError, QueueClosedError, };
|
|
1
|
+
/**
|
|
2
|
+
* `enqiu()` — a typed layer over BullMQ.
|
|
3
|
+
*
|
|
4
|
+
* Enqiu owns the developer experience: inferred job names, schema-validated
|
|
5
|
+
* input, and one object per job that you call like a function. BullMQ owns
|
|
6
|
+
* storage, scheduling and execution. Anything BullMQ's open-source tier cannot
|
|
7
|
+
* express is absent rather than faked, with two exceptions Enqiu enforces
|
|
8
|
+
* itself around the handler because they cost nothing to add: `timeout` and
|
|
9
|
+
* `expiresIn`.
|
|
10
|
+
*
|
|
11
|
+
* This file composes; the parts it composes live next to it.
|
|
12
|
+
*/
|
|
13
|
+
import type { Enqiu, EnqiuOptions, JobDefinitions } from "./types.js";
|
|
14
|
+
export { job } from "./definition.js";
|
|
15
|
+
/** Build a typed job API backed by a BullMQ queue. */
|
|
16
|
+
export declare function enqiu<const Definitions extends JobDefinitions>(definitions: Definitions, options: EnqiuOptions): Enqiu<Definitions>;
|