enqiu 0.1.1 → 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/README.md CHANGED
@@ -1,244 +1,194 @@
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/beta?style=flat-square&label=beta)](https://www.npmjs.com/package/enqiu)
4
+ [![status](https://img.shields.io/badge/status-beta-2563eb?style=flat-square)](#status-beta)
5
+ [![built on](https://img.shields.io/badge/built_on-BullMQ-b91c1c?style=flat-square)](https://bullmq.io)
6
+ [![license](https://img.shields.io/npm/l/enqiu?style=flat-square)](LICENSE)
7
+
8
+ A type-safe job API on top of [BullMQ](https://bullmq.io). Define each job once
9
+ with a schema, then call it like a function — the name, input and result types
10
+ are inferred, so there is no separate registry and no string-keyed dispatch.
11
+
12
+ BullMQ owns storage, scheduling and execution. Enqiu owns the developer
13
+ experience.
14
+
15
+ [npm](https://www.npmjs.com/package/enqiu) ·
16
+ [Issues](https://github.com/moji2002/enqiu/issues)
17
+
18
+ ## Status: beta
19
+
20
+ > [!NOTE]
21
+ > **Enqiu is beta software.** The shape of the API is settled and the hard
22
+ > parts — storage, retries, scheduling and crash recovery — are BullMQ's,
23
+ > which is mature and widely deployed.
24
+ >
25
+ > What is new is the layer in between. Expect edge-case bugs there, and pin an
26
+ > exact version: it is published under the `beta` dist-tag, so a plain
27
+ > `npm install enqiu` will not install it.
28
+ >
29
+ > The layer in between is new. It is covered at 99% of statements and 91% of
30
+ > branches against a real Redis, and the two parts that need no server — the
31
+ > BullMQ vocabulary mapping and the serialization check — are held to their own
32
+ > thresholds in either mode.
5
33
 
6
34
  ```bash
7
- pnpm add enqiu
35
+ npm install enqiu@beta bullmq ioredis
8
36
  ```
9
37
 
10
- Enqiu has no runtime dependencies. Redis, schema, Hono, and telemetry packages
11
- remain your choice.
38
+ `bullmq` and `ioredis` are peer dependencies — Enqiu does not pick versions or
39
+ open connections for you.
12
40
 
13
41
  ## Quick start
14
42
 
15
- Define each job once, then call it like a function. The name, input, and result
16
- types are inferred.
17
-
18
43
  ```ts
19
44
  import { enqiu, job } from "enqiu";
20
45
  import { z } from "zod";
21
46
 
22
- const jobs = enqiu({
23
- sendEmail: job({
24
- input: z.object({
25
- to: z.email(),
26
- subject: z.string(),
47
+ const { jobs, queue, worker, close } = enqiu(
48
+ {
49
+ sendEmail: job({
50
+ input: z.object({ to: z.string(), subject: z.string() }),
51
+ retry: { attempts: 3, backoff: { type: "exponential", delay: 500 } },
52
+ timeout: 30_000,
53
+ run: async (input, { log }) => {
54
+ log.info("sending", { to: input.to });
55
+ return { delivered: true, subject: input.subject };
56
+ },
27
57
  }),
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
58
  },
63
- });
64
- ```
65
-
66
- ## Redis
67
-
68
- Inject an existing client; Enqiu does not create connections or install a Redis
69
- library. It accepts Bun's `send(command, args)` client shape and node-redis'
70
- `sendCommand(args)` shape.
71
-
72
- ```ts
73
- import { createClient } from "redis";
74
- import { enqiu, redis } from "enqiu";
75
-
76
- const client = createClient({ url: process.env.REDIS_URL });
77
- await client.connect();
59
+ {
60
+ name: "notifications",
61
+ connection: { host: "localhost", port: 6379 },
62
+ worker: { concurrency: 10 },
63
+ },
64
+ );
78
65
 
79
- const jobs = enqiu(definitions, {
80
- name: "notifications",
81
- driver: redis(client),
82
- worker: { concurrency: 20 },
83
- });
66
+ const handle = await jobs.sendEmail({ to: "a@b.c", subject: "Welcome" });
67
+ const result = await handle.result; // { delivered: boolean; subject: string }
84
68
  ```
85
69
 
86
- Use the same definitions in a producer-only process:
70
+ `await jobs.sendEmail(input)` resolves once BullMQ accepts the job and returns a
71
+ handle. It does not wait for the handler. Await `handle.result` only when the
72
+ caller needs the result.
73
+
74
+ `jobs` holds your jobs and nothing else, which is why no job name is reserved —
75
+ `jobs.queue` is a job you called `queue`. The queue and worker controls sit
76
+ beside it:
87
77
 
88
78
  ```ts
89
- const jobs = enqiu(definitions, {
90
- name: "notifications",
91
- driver: redis(client),
92
- worker: false,
93
- });
79
+ await queue.stats(); // counts by status
80
+ await queue.onIdle(); // resolves when nothing is outstanding
81
+ await close(); // queue, worker and event stream
94
82
  ```
95
83
 
96
- Redis jobs use atomic Lua transitions, visibility leases, and deterministic
97
- recovery so multiple Node.js or Bun workers can safely share a queue.
84
+ Only what Enqiu types or computes is here. Pausing a queue or a worker, setting
85
+ global concurrency and anything else BullMQ already exposes is `bull.queue.*`
86
+ and `bull.worker.*` — a second name for the same call would be one more thing
87
+ to learn and nothing else.
98
88
 
99
- ## Job policies
100
-
101
- Policies live beside the handler and keep call sites clean. Durations are
102
- numbers in milliseconds, so applications may use plain numbers or a helper
103
- such as `ms("30s")` without making it an Enqiu dependency.
89
+ A plain handler works too, with input and output still inferred:
104
90
 
105
91
  ```ts
106
- const jobs = enqiu({
107
- syncAccount: job({
108
- input: z.object({
109
- tenantId: z.string(),
110
- accountId: z.string(),
111
- }),
112
- retry: {
113
- attempts: 5,
114
- backoff: { type: "exponential", delay: 250, jitter: 0.2 },
115
- },
116
- timeout: 30_000,
117
- expiresIn: 5 * 60_000,
118
- concurrency: {
119
- limit: 2,
120
- by: (input) => input.tenantId,
121
- },
122
- throttle: {
123
- limit: 100,
124
- per: 60_000,
125
- burst: 10,
126
- by: (input) => input.tenantId,
127
- },
128
- run: async (input, context) => {
129
- return syncAccount(input, { signal: context.signal });
130
- },
131
- }),
132
- });
92
+ const { jobs } = enqiu(
93
+ { resizeImage: async (input: { key: string; width: number }) => input },
94
+ { connection },
95
+ );
133
96
  ```
134
97
 
135
- - `concurrency` limits simultaneous work globally or by a key such as tenant.
136
- - `throttle` limits starts over time; `burst` allows short spikes.
137
- - `debounce: { mode: "leading" }` keeps the first call in a window.
138
- - `debounce: { mode: "trailing" }` keeps the most recent call in a window.
139
- - `expiresIn` prevents stale jobs from starting.
140
- - `idempotencyKey` makes repeated submissions return the same job.
141
-
142
- Per-call delivery options are available when needed:
98
+ ## What Enqiu adds
99
+
100
+ - **Inferred types end to end.** Job names come from the object keys; input and
101
+ result types come from the schema and handler. No generics to write.
102
+ - **Standard Schema validation at the boundary.** Zod, Valibot, ArkType or
103
+ anything else implementing the spec. Invalid input is rejected before a job
104
+ is queued.
105
+ - **Per-attempt `timeout`,** with an `AbortSignal` handed to the handler. BullMQ
106
+ has no job timeout; Enqiu enforces this itself.
107
+ - **`expiresIn`,** which fails a job that waited too long without running it.
108
+ Also enforced by Enqiu.
109
+ - **A serialization guard** that rejects functions, symbols, cycles and sparse
110
+ arrays with the exact path, instead of failing later inside the queue.
111
+ - **A `cancelled` status,** which BullMQ has no state for: cancelling a job that
112
+ has not started removes it, so Enqiu records the finished snapshot and
113
+ `refresh()` can still tell "cancelled" from "never existed".
114
+ - **Failures that survive as classes.** BullMQ hands a failure to another
115
+ process as one string; a timeout or an expiry writes its kind down, so
116
+ `handle.result` rejects with `JobTimeoutError` rather than a bare `Error`.
117
+
118
+ ## Escaping the layer
119
+
120
+ Enqiu models a deliberate subset. Everything else BullMQ can do — flows, Pro
121
+ groups, metrics, raw job options — is one property away, with no wrapper in
122
+ between and no fork required:
143
123
 
144
124
  ```ts
145
- const handle = await jobs.syncAccount(input, {
146
- idempotencyKey: `sync:${input.accountId}`,
147
- idempotencyTtl: 24 * 60 * 60_000,
148
- delay: 5_000,
149
- priority: "high",
150
- });
125
+ const { bull } = enqiu(definitions, { connection });
126
+
127
+ bull.queue // the real BullMQ Queue
128
+ bull.worker // the real BullMQ Worker, or undefined for a producer
151
129
  ```
152
130
 
153
- ## Progress and logs
131
+ Enqiu reads its own state from those objects rather than mirroring it, so
132
+ pausing `bull.worker` or closing `bull.queue` is seen on the Enqiu side too —
133
+ the two cannot drift apart.
154
134
 
155
- Progress uses real units rather than an ambiguous fraction:
135
+ Measured against raw BullMQ on the same Redis — 10,000 jobs, concurrency 32,
136
+ contestants interleaved, median of 7 — the typed path costs about 2%, and Zod
137
+ validation about 3%, varying by a point between runs. Calls through `bull` cost
138
+ nothing, because nothing is in the way.
156
139
 
157
- ```ts
158
- const jobs = enqiu({
159
- importRows: async (rows: string[], context) => {
160
- for (let index = 0; index < rows.length; index += 1) {
161
- await importRow(rows[index]);
162
- await context.reportProgress({
163
- completed: index + 1,
164
- total: rows.length,
165
- message: "Importing rows",
166
- });
167
- }
168
-
169
- context.log.info("Import complete", { rows: rows.length });
170
- },
171
- });
172
- ```
140
+ Reproduce with `pnpm tsx bench/overhead.ts`.
173
141
 
174
- Subscribe to lifecycle events with `jobs.queue.on(...)`. Memory events stay
175
- inside the process; Redis events are shared between producers and workers.
142
+ ## What BullMQ provides
176
143
 
177
- ## Cron schedules
144
+ Retries and backoff, priorities, delays, cron schedules, deduplication, bulk
145
+ submission, progress, logs, events and cleanup are BullMQ's, surfaced through
146
+ Enqiu's API.
178
147
 
179
- Schedules use standard five-field cron expressions and IANA time zones:
148
+ ## Compatibility notes
180
149
 
181
- ```ts
182
- const schedule = await jobs.sendDigest.schedule({
183
- id: "weekday-digest",
184
- cron: "0 9 * * 1-5",
185
- timezone: "Europe/Nicosia",
186
- input: { audience: "daily" },
187
- catchUp: true,
188
- });
189
-
190
- await schedule.pause();
191
- await schedule.resume();
192
- await schedule.remove();
193
- ```
150
+ Enqiu deliberately does not paper over gaps in BullMQ's open-source tier:
194
151
 
195
- Memory schedules live for the process lifetime. Redis schedules are durable
196
- and use deterministic occurrence IDs to avoid duplicate runs.
152
+ | Not available | Why |
153
+ | --- | --- |
154
+ | Per-key concurrency (`concurrency: { by }`) | BullMQ groups are a **BullMQ Pro** feature. |
155
+ | Per-key rate limiting (`throttle: { by }`) | The OSS limiter is one global `{ max, duration }` per worker. |
156
+ | Debounce | No open-source equivalent. |
157
+ | In-browser queues | BullMQ requires Redis and Node. |
197
158
 
198
- ## Hono
159
+ If you need any of those, use BullMQ Pro directly, or pin Enqiu 0.2.x, which
160
+ shipped first-party memory and Redis drivers that implemented them.
199
161
 
200
- Enqiu uses Standard Schema and exposes each job's input schema, so the same
201
- schema can validate an HTTP route without redefining a type:
162
+ ## Testing
202
163
 
203
- ```ts
204
- import { sValidator } from "@hono/standard-validator";
205
-
206
- app.post(
207
- "/emails",
208
- sValidator("json", jobs.sendEmail.input),
209
- async (c) => {
210
- const handle = await jobs.sendEmail(c.req.valid("json"));
211
- return c.json({ id: handle.id }, 202);
212
- },
213
- );
164
+ ```bash
165
+ docker run -d -p 6379:6379 redis:7-alpine
166
+ ENQIU_TEST_REDIS_URL=redis://localhost:6379 pnpm run check
167
+ ENQIU_TEST_REDIS_URL=redis://localhost:6379 pnpm run scenarios
214
168
  ```
215
169
 
216
- Hono and `@hono/standard-validator` are optional application dependencies.
170
+ Most tests need a real Redis, because most code paths go through BullMQ. The
171
+ exceptions are the vocabulary mapping and the serialization check, which are
172
+ pure and stay covered without a server — so a run without
173
+ `ENQIU_TEST_REDIS_URL` still verifies something rather than nothing.
217
174
 
218
- ## Queue and worker controls
175
+ Five runnable, self-asserting scenarios live in
176
+ [`examples/scenarios/`](examples/scenarios): webhook ingestion, notification
177
+ campaigns, report progress, a transcoding pool, and failure triage. The
178
+ reasoning behind the workload choices is in
179
+ [`docs/use-case-research.md`](docs/use-case-research.md).
219
180
 
220
- ```ts
221
- await jobs.queue.pause();
222
- await jobs.queue.resume();
223
- await jobs.queue.setConcurrency(50);
224
-
225
- const page = await jobs.queue.list({ status: "failed", limit: 100 });
226
- const snapshot = await jobs.queue.get(handle.id);
227
- await jobs.queue.redrive(handle.id);
228
- await jobs.queue.cleanup({ olderThan: Date.now() - 7 * 24 * 60 * 60_000 });
229
-
230
- await jobs.worker.pause();
231
- await jobs.worker.resume();
232
- await jobs.worker.onIdle();
233
- await jobs.worker.close();
234
- ```
181
+ ## Releasing
235
182
 
236
- ## Runtime support
183
+ ```bash
184
+ pnpm run release:beta # npm publish --tag beta
185
+ ```
237
186
 
238
- - Node.js 20 and newer
239
- - Current stable Bun
240
- - Memory and Redis drivers
241
- - ESM and TypeScript declarations
187
+ The tag is in the script rather than in `publishConfig`, because npm 11 does not
188
+ honour `publishConfig.tag` — a plain `npm publish` resolves to `latest` and would
189
+ hand a beta to every `npm install enqiu`. `prepack` runs the full check first,
190
+ and `build` cleans `dist` before compiling, since `tsc` leaves deleted modules
191
+ behind and they would otherwise ship.
242
192
 
243
193
  ## License
244
194
 
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>;