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/README.md CHANGED
@@ -1,286 +1,183 @@
1
1
  # Enqiu
2
2
 
3
- A small, type-safe job queue for Node.js and Bun. Start in memory, move to
4
- Redis without changing your job API.
3
+ [![npm](https://img.shields.io/npm/v/enqiu?style=flat-square)](https://www.npmjs.com/package/enqiu)
4
+ [![built on](https://img.shields.io/badge/built_on-BullMQ-b91c1c?style=flat-square)](https://bullmq.io)
5
+ [![license](https://img.shields.io/npm/l/enqiu?style=flat-square)](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
- pnpm add enqiu
27
+ npm install enqiu bullmq ioredis
8
28
  ```
9
29
 
10
- Enqiu has no runtime dependencies. Redis, schema, Hono, and telemetry packages
11
- remain your choice.
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
- sendEmail: job({
24
- input: z.object({
25
- to: z.email(),
26
- subject: z.string(),
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
- ## Testing in your project
67
-
68
- Create a fresh queue for each test so workers and queued state never leak
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
- ```bash
97
- pnpm add enqiu
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
- The repository keeps the complete, runnable
103
- [memory and opt-in Redis examples](https://github.com/moji2002/enqiu/tree/main/examples/testing).
104
- The Redis test runs only when `ENQIU_TEST_REDIS_URL` is set, uses a unique
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
- ## Redis
109
-
110
- Inject an existing client; Enqiu does not create connections or install a Redis
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
- const jobs = enqiu(definitions, {
132
- name: "notifications",
133
- driver: redis(client),
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
- Redis jobs use atomic Lua transitions, visibility leases, and deterministic
139
- recovery so multiple Node.js or Bun workers can safely share a queue.
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
- ## Job policies
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
- syncAccount: job({
150
- input: z.object({
151
- tenantId: z.string(),
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
- - `concurrency` limits simultaneous work globally or by a key such as tenant.
178
- - `throttle` limits starts over time; `burst` allows short spikes.
179
- - `debounce: { mode: "leading" }` keeps the first call in a window.
180
- - `debounce: { mode: "trailing" }` keeps the most recent call in a window.
181
- - `expiresIn` prevents stale jobs from starting.
182
- - `idempotencyKey` makes repeated submissions return the same job.
183
-
184
- Per-call delivery options are available when needed:
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 handle = await jobs.syncAccount(input, {
188
- idempotencyKey: `sync:${input.accountId}`,
189
- idempotencyTtl: 24 * 60 * 60_000,
190
- delay: 5_000,
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
- ## Progress and logs
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
- Progress uses real units rather than an ambiguous fraction:
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
- ```ts
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
- Subscribe to lifecycle events with `jobs.queue.on(...)`. Memory events stay
217
- inside the process; Redis events are shared between producers and workers.
134
+ ## What BullMQ provides
218
135
 
219
- ## Cron schedules
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
- Schedules use standard five-field cron expressions and IANA time zones:
140
+ ## Compatibility notes
222
141
 
223
- ```ts
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
- Memory schedules live for the process lifetime. Redis schedules are durable
238
- and use deterministic occurrence IDs to avoid duplicate runs.
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
- ## Hono
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
- Enqiu uses Standard Schema and exposes each job's input schema, so the same
243
- schema can validate an HTTP route without redefining a type:
154
+ ## Testing
244
155
 
245
- ```ts
246
- import { sValidator } from "@hono/standard-validator";
247
-
248
- app.post(
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
- Hono and `@hono/standard-validator` are optional application dependencies.
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
- ## Queue and worker controls
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
- ```ts
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
- ## Runtime support
175
+ ```bash
176
+ pnpm run release # npm publish
177
+ ```
279
178
 
280
- - Node.js 20 and newer
281
- - Current stable Bun
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
- import { JobCancelledError, JobExpiredError, JobFailedError, JobTimeoutError, QueueClosedError } from "./memory.js";
2
- import type { JobSnapshot, JobStatus, MaybePromise, QueueEventMap, QueueStats, RetryOptions } from "./memory.js";
3
- import { type RedisDriver } from "./redis.js";
4
- import { JobSerializationError } from "./codec.js";
5
- declare const definitionMarker: unique symbol;
6
- export interface StandardSchemaV1<Input = unknown, Output = Input> {
7
- readonly "~standard": {
8
- readonly version: 1;
9
- readonly vendor: string;
10
- readonly validate: (value: unknown) => MaybePromise<{
11
- readonly value: Output;
12
- readonly issues?: undefined;
13
- } | {
14
- readonly value?: undefined;
15
- readonly issues: readonly StandardSchemaIssue[];
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>;