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.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Where cancellations are recorded.
3
+ *
4
+ * BullMQ has no cancelled state, and cancelling a job that has not started
5
+ * removes it — so without a marker the only evidence is gone and `refresh()`
6
+ * cannot tell "cancelled" from "never existed". A process-local Set was the
7
+ * obvious first answer and the wrong one: it made a job's fate depend on which
8
+ * process asked, and lost it entirely on restart.
9
+ *
10
+ * What is stored is the finished snapshot itself, not a reason beside a copy of
11
+ * one: it is the only record left once the job is gone, and everything a reader
12
+ * wants is already a field of `JobSnapshot`. Wrapping it meant three records of
13
+ * one event that had to agree, reassembled differently by each reader.
14
+ */
15
+ import { backendClient } from "./backend.js";
16
+ /** Deleted in batches: one HDEL naming every field can exceed Redis' limit. */
17
+ const DELETE_BATCH = 500;
18
+ export class CancellationMarkers {
19
+ queue;
20
+ key;
21
+ constructor(queue) {
22
+ this.queue = queue;
23
+ this.key = queue.toKey("enqiu:cancelled");
24
+ }
25
+ async write(id, snapshot) {
26
+ const client = await backendClient(this.queue);
27
+ await client.hset(this.key, { [id]: JSON.stringify(snapshot) });
28
+ }
29
+ async read(id) {
30
+ const client = await backendClient(this.queue);
31
+ const raw = await client.hget(this.key, id);
32
+ return raw ? JSON.parse(raw) : undefined;
33
+ }
34
+ /** Markers outlive their jobs otherwise, so the hash grows forever. */
35
+ async prune(threshold) {
36
+ const client = await backendClient(this.queue);
37
+ const stale = Object.entries(await client.hgetall(this.key))
38
+ .filter(([, raw]) => {
39
+ try {
40
+ return (JSON.parse(raw).finishedAt ?? 0) <= threshold;
41
+ }
42
+ catch {
43
+ return true;
44
+ }
45
+ })
46
+ .map(([id]) => id);
47
+ for (let from = 0; from < stale.length; from += DELETE_BATCH) {
48
+ await client.hdel(this.key, ...stale.slice(from, from + DELETE_BATCH));
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Running one job: expiry, the per-attempt deadline, and the handler's context.
3
+ *
4
+ * These are the two policies Enqiu enforces itself, because BullMQ has neither
5
+ * and both cost nothing to add around the handler. Everything else about
6
+ * execution — claiming, retrying, stalling, locking — is BullMQ's.
7
+ */
8
+ import { type Job as BullJob } from "bullmq";
9
+ import type { RuntimeDefinition } from "./definition.js";
10
+ export interface RunnerOptions {
11
+ /** Queue-wide deadline, overridden per job. */
12
+ readonly timeout: number | undefined;
13
+ }
14
+ export declare class JobRunner {
15
+ private readonly definitions;
16
+ private readonly options;
17
+ constructor(definitions: ReadonlyMap<string, RuntimeDefinition>, options: RunnerOptions);
18
+ run(bull: BullJob, external?: AbortSignal): Promise<unknown>;
19
+ private createContext;
20
+ }
package/dist/runner.js ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Running one job: expiry, the per-attempt deadline, and the handler's context.
3
+ *
4
+ * These are the two policies Enqiu enforces itself, because BullMQ has neither
5
+ * and both cost nothing to add around the handler. Everything else about
6
+ * execution — claiming, retrying, stalling, locking — is BullMQ's.
7
+ */
8
+ import { UnrecoverableError } from "bullmq";
9
+ import { JobTimeoutError, encodeFailure } from "./errors.js";
10
+ import { assertJobValue } from "./serialize.js";
11
+ /** For handlers with no deadline, whose only abort source is BullMQ's own. */
12
+ const neverAborts = new AbortController().signal;
13
+ function validateProgress(progress) {
14
+ if (!Number.isFinite(progress.completed) ||
15
+ !Number.isFinite(progress.total) ||
16
+ progress.completed < 0 ||
17
+ progress.total <= 0 ||
18
+ progress.completed > progress.total) {
19
+ throw new RangeError("Progress requires 0 <= completed <= total and total > 0");
20
+ }
21
+ }
22
+ export class JobRunner {
23
+ definitions;
24
+ options;
25
+ constructor(definitions, options) {
26
+ this.definitions = definitions;
27
+ this.options = options;
28
+ }
29
+ async run(bull, external) {
30
+ const id = String(bull.id);
31
+ const definition = this.definitions.get(bull.name);
32
+ if (!definition) {
33
+ throw new UnrecoverableError(`No handler registered for job "${bull.name}"`);
34
+ }
35
+ const { expiresIn } = definition.policy;
36
+ if (expiresIn !== undefined && Date.now() - bull.timestamp > expiresIn) {
37
+ // Expiry is a property of the job, not of this attempt, so retrying
38
+ // cannot help — UnrecoverableError stops BullMQ retrying it.
39
+ throw new UnrecoverableError(encodeFailure({ kind: "expired", jobId: id }));
40
+ }
41
+ const timeout = definition.policy.timeout ?? this.options.timeout;
42
+ if (timeout === undefined) {
43
+ // BullMQ's signal already aborts on cancellation, and with no deadline to
44
+ // merge in there is nothing left for a second controller to do.
45
+ const context = this.createContext(bull, external ?? neverAborts);
46
+ return assertJobValue(await definition.run(bull.data, context));
47
+ }
48
+ // One signal for the handler, whichever reason fires first: this queue's
49
+ // own deadline, or a cancellation delivered through BullMQ.
50
+ const controller = new AbortController();
51
+ const signal = external
52
+ ? AbortSignal.any([external, controller.signal])
53
+ : controller.signal;
54
+ const execution = Promise.resolve(definition.run(bull.data, this.createContext(bull, signal)));
55
+ let timer;
56
+ try {
57
+ return assertJobValue(await Promise.race([
58
+ execution,
59
+ new Promise((_, reject) => {
60
+ timer = setTimeout(() => {
61
+ // The handler is told what happened; BullMQ is told in a form
62
+ // that survives being stored as a single string.
63
+ controller.abort(new JobTimeoutError(id, timeout));
64
+ reject(new Error(encodeFailure({ kind: "timeout", jobId: id, timeout })));
65
+ }, timeout);
66
+ }),
67
+ ]));
68
+ }
69
+ finally {
70
+ if (timer !== undefined)
71
+ clearTimeout(timer);
72
+ }
73
+ }
74
+ createContext(bull, signal) {
75
+ const id = String(bull.id);
76
+ const write = (level, message, fields) => {
77
+ if (!message)
78
+ throw new TypeError("Job log messages must not be empty");
79
+ void bull
80
+ .log(JSON.stringify({ level, message, fields, at: Date.now() }))
81
+ .catch(() => undefined);
82
+ };
83
+ const log = {
84
+ debug: (m, f) => write("debug", m, f),
85
+ info: (m, f) => write("info", m, f),
86
+ warn: (m, f) => write("warn", m, f),
87
+ error: (m, f) => write("error", m, f),
88
+ };
89
+ return {
90
+ id,
91
+ name: bull.name,
92
+ attempt: bull.attemptsMade + 1,
93
+ signal,
94
+ log,
95
+ reportProgress: async (progress) => {
96
+ validateProgress(progress);
97
+ await bull.updateProgress(assertJobValue(progress));
98
+ },
99
+ };
100
+ }
101
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The check that a job's value can survive the trip through the queue.
3
+ *
4
+ * BullMQ serialises on submit, so copying the value first would be redundant —
5
+ * nothing the caller does afterwards can reach the stored job. What is worth
6
+ * paying for is the error: `JSON.stringify` says "Converting circular structure
7
+ * to JSON" once the value is already on its way to Redis, while this names the
8
+ * exact path that cannot be stored.
9
+ */
10
+ export declare class JobSerializationError extends TypeError {
11
+ readonly path: string;
12
+ constructor(path: string, reason: string);
13
+ }
14
+ /** Checks a value is storable and returns it unchanged. */
15
+ export declare function assertJobValue<T>(value: T): T;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The check that a job's value can survive the trip through the queue.
3
+ *
4
+ * BullMQ serialises on submit, so copying the value first would be redundant —
5
+ * nothing the caller does afterwards can reach the stored job. What is worth
6
+ * paying for is the error: `JSON.stringify` says "Converting circular structure
7
+ * to JSON" once the value is already on its way to Redis, while this names the
8
+ * exact path that cannot be stored.
9
+ */
10
+ export class JobSerializationError extends TypeError {
11
+ path;
12
+ constructor(path, reason) {
13
+ super(`Job data at ${path} is not JSON-safe: ${reason}`);
14
+ this.name = "JobSerializationError";
15
+ this.path = path;
16
+ }
17
+ }
18
+ /** Checks a value is storable and returns it unchanged. */
19
+ export function assertJobValue(value) {
20
+ assertJsonSafe(value, [], new Set());
21
+ return value;
22
+ }
23
+ /**
24
+ * `["a", 0, "b"]` reads back as `$.a[0].b`.
25
+ *
26
+ * The path is carried as one array pushed and popped down the walk, and only
27
+ * rendered when something is about to throw. Building the string at every node
28
+ * cost more than the rest of the check put together.
29
+ */
30
+ function fail(path, reason) {
31
+ let rendered = "$";
32
+ for (const segment of path) {
33
+ rendered += typeof segment === "number" ? `[${segment}]` : `.${segment}`;
34
+ }
35
+ throw new JobSerializationError(rendered, reason);
36
+ }
37
+ function assertJsonSafe(value, path, ancestors) {
38
+ if (value === null ||
39
+ typeof value === "string" ||
40
+ typeof value === "boolean") {
41
+ return;
42
+ }
43
+ if (value === undefined) {
44
+ if (path.length > 0) {
45
+ fail(path, "undefined is only valid as a root value");
46
+ }
47
+ return;
48
+ }
49
+ if (typeof value === "number") {
50
+ if (!Number.isFinite(value))
51
+ fail(path, "numbers must be finite");
52
+ return;
53
+ }
54
+ if (typeof value === "bigint" ||
55
+ typeof value === "function" ||
56
+ typeof value === "symbol") {
57
+ fail(path, `${typeof value} is unsupported`);
58
+ }
59
+ if (typeof value !== "object")
60
+ fail(path, "unsupported value");
61
+ if (ancestors.has(value))
62
+ fail(path, "circular reference");
63
+ ancestors.add(value);
64
+ if (Array.isArray(value)) {
65
+ for (let index = 0; index < value.length; index += 1) {
66
+ path.push(index);
67
+ if (!(index in value)) {
68
+ fail(path, "sparse array entries are unsupported");
69
+ }
70
+ assertJsonSafe(value[index], path, ancestors);
71
+ path.pop();
72
+ }
73
+ ancestors.delete(value);
74
+ return;
75
+ }
76
+ const prototype = Object.getPrototypeOf(value);
77
+ if (prototype !== Object.prototype && prototype !== null) {
78
+ fail(path, "only plain objects and arrays are supported");
79
+ }
80
+ const entries = value;
81
+ for (const key of Object.keys(entries)) {
82
+ path.push(key);
83
+ if (entries[key] === undefined) {
84
+ fail(path, "undefined object fields are unsupported");
85
+ }
86
+ assertJsonSafe(entries[key], path, ancestors);
87
+ path.pop();
88
+ }
89
+ ancestors.delete(value);
90
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * The public vocabulary of `enqiu()`.
3
+ *
4
+ * Enqiu is a typed layer over BullMQ: it owns inference and validation, BullMQ
5
+ * owns storage and execution. Anything BullMQ's open-source tier cannot
6
+ * express is absent here rather than faked — see the compatibility notes in
7
+ * the README.
8
+ */
9
+ import type { ConnectionOptions, Queue, Worker } from "bullmq";
10
+ import type { SerializedError, StandardSchemaIssue } from "./errors.js";
11
+ export type { SerializedError, StandardSchemaIssue } from "./errors.js";
12
+ export type MaybePromise<T> = T | PromiseLike<T>;
13
+ export declare const definitionMarker: unique symbol;
14
+ export type JobStatus = "queued" | "scheduled" | "running" | "succeeded" | "failed" | "cancelled";
15
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
16
+ readonly "~standard": {
17
+ readonly version: 1;
18
+ readonly vendor: string;
19
+ readonly validate: (value: unknown) => MaybePromise<{
20
+ readonly value: Output;
21
+ readonly issues?: undefined;
22
+ } | {
23
+ readonly value?: undefined;
24
+ readonly issues: readonly StandardSchemaIssue[];
25
+ }>;
26
+ readonly types?: {
27
+ readonly input: Input;
28
+ readonly output: Output;
29
+ } | undefined;
30
+ };
31
+ }
32
+ export type InferSchemaInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];
33
+ export type InferSchemaOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
34
+ export interface Progress {
35
+ readonly completed: number;
36
+ readonly total: number;
37
+ readonly message?: string;
38
+ readonly details?: Readonly<Record<string, unknown>>;
39
+ }
40
+ export interface JobLogger {
41
+ debug(message: string, fields?: Readonly<Record<string, unknown>>): void;
42
+ info(message: string, fields?: Readonly<Record<string, unknown>>): void;
43
+ warn(message: string, fields?: Readonly<Record<string, unknown>>): void;
44
+ error(message: string, fields?: Readonly<Record<string, unknown>>): void;
45
+ }
46
+ export interface JobContext<Name extends string = string> {
47
+ readonly id: string;
48
+ readonly name: Name;
49
+ readonly attempt: number;
50
+ readonly signal: AbortSignal;
51
+ reportProgress(progress: Progress): Promise<void>;
52
+ readonly log: JobLogger;
53
+ }
54
+ export type JobHandler<Input = unknown, Output = unknown, Name extends string = string> = (input: Input, context: JobContext<Name>) => MaybePromise<Output>;
55
+ export interface BackoffOptions {
56
+ type?: "fixed" | "exponential";
57
+ delay: number;
58
+ }
59
+ export interface RetryPolicy {
60
+ /** Total number of attempts, including the first. */
61
+ attempts: number;
62
+ backoff?: number | BackoffOptions;
63
+ }
64
+ /**
65
+ * Per-job policies.
66
+ *
67
+ * `timeout` and `expiresIn` live here rather than on a submission because
68
+ * Enqiu enforces them worker-side, and BullMQ's job options have no field to
69
+ * carry a per-submission value across the queue. Keyed concurrency, keyed
70
+ * throttling and debounce are absent: BullMQ's open-source tier offers only a
71
+ * global per-worker rate limit, and per-key grouping is a Pro feature.
72
+ */
73
+ export interface JobPolicyOptions {
74
+ retry?: number | RetryPolicy;
75
+ /** Per-attempt deadline. Aborts the handler's signal and fails the attempt. */
76
+ timeout?: number;
77
+ /** Fail without running if the job has waited longer than this. */
78
+ expiresIn?: number;
79
+ }
80
+ export interface SchemaJobDefinition<Schema extends StandardSchemaV1 = StandardSchemaV1, Output = unknown> extends JobPolicyOptions {
81
+ readonly [definitionMarker]: true;
82
+ readonly input: Schema;
83
+ readonly run: JobHandler<InferSchemaOutput<Schema>, Output>;
84
+ }
85
+ export type HandlerJobDefinition<Input = unknown, Output = unknown> = JobHandler<Input, Output>;
86
+ /**
87
+ * `any` is required in the schema position. JobHandler is contravariant in its
88
+ * input and the generic is invariant in Schema, so pinning it rejects every
89
+ * real schema and collapses the inferred API to `unknown`.
90
+ */
91
+ export type JobDefinition = SchemaJobDefinition<any, any> | HandlerJobDefinition<never, unknown>;
92
+ export type JobDefinitions = Record<string, JobDefinition>;
93
+ /**
94
+ * Everything inferred from one definition, worked out once.
95
+ *
96
+ * A schema job and a bare handler differ in four derived facts, and writing
97
+ * each as its own conditional meant restating "which of the two is this" four
98
+ * times. They are not independent: a third form of definition would have to be
99
+ * added to all of them or to none.
100
+ *
101
+ * `input` is what a caller passes and `runInput` is what the handler receives.
102
+ * They differ only for a schema that transforms — `z.string().transform(Number)`
103
+ * takes a string and hands the handler a number.
104
+ */
105
+ type DefinitionShape<Definition> = Definition extends SchemaJobDefinition<infer Schema, infer Output> ? {
106
+ input: InferSchemaInput<Schema>;
107
+ runInput: InferSchemaOutput<Schema>;
108
+ output: Awaited<Output>;
109
+ schema: Schema;
110
+ } : Definition extends JobHandler<infer Input, infer Output, string> ? {
111
+ input: Input;
112
+ runInput: Input;
113
+ output: Awaited<Output>;
114
+ schema: undefined;
115
+ } : {
116
+ input: never;
117
+ runInput: never;
118
+ output: never;
119
+ schema: undefined;
120
+ };
121
+ type DefinitionInput<Definition> = DefinitionShape<Definition>["input"];
122
+ type DefinitionRunInput<Definition> = DefinitionShape<Definition>["runInput"];
123
+ type DefinitionOutput<Definition> = DefinitionShape<Definition>["output"];
124
+ type DefinitionSchema<Definition> = DefinitionShape<Definition>["schema"];
125
+ export interface SubmitOptions {
126
+ id?: string;
127
+ /** Reuses an existing job with the same key instead of creating a new one. */
128
+ idempotencyKey?: string;
129
+ idempotencyTtl?: number;
130
+ delay?: number | Date;
131
+ priority?: number | "low" | "normal" | "high";
132
+ retry?: number | RetryPolicy;
133
+ }
134
+ /** One key across a batch would collapse it into a single job, so it is out. */
135
+ export interface BulkOptions extends Omit<SubmitOptions, "id" | "idempotencyKey" | "idempotencyTtl"> {
136
+ ids?: readonly string[];
137
+ }
138
+ export interface ScheduleOptions<Input> {
139
+ id?: string;
140
+ cron: string;
141
+ timezone?: string;
142
+ input: Input;
143
+ }
144
+ export interface ScheduleSnapshot {
145
+ id: string;
146
+ jobName: string;
147
+ cron: string;
148
+ timezone: string;
149
+ nextRunAt: number;
150
+ input: unknown;
151
+ }
152
+ export interface ScheduleHandle {
153
+ readonly id: string;
154
+ remove(): Promise<void>;
155
+ refresh(): Promise<ScheduleSnapshot>;
156
+ }
157
+ export interface JobSnapshot<Input = unknown, Output = unknown, Name extends string = string> {
158
+ id: string;
159
+ name: Name;
160
+ input: Input;
161
+ status: JobStatus;
162
+ attempt: number;
163
+ createdAt: number;
164
+ startedAt?: number | undefined;
165
+ finishedAt?: number | undefined;
166
+ progress?: unknown;
167
+ output?: Output | undefined;
168
+ error?: SerializedError | undefined;
169
+ }
170
+ /**
171
+ * What a submission gives you back.
172
+ *
173
+ * There is no `status` field. It could only ever report what was true when the
174
+ * handle was made, which is a value that goes quietly wrong while you hold it.
175
+ * `refresh()` answers the question at the moment you ask.
176
+ */
177
+ export interface JobHandle<Output = unknown, Input = unknown, Name extends string = string> {
178
+ readonly id: string;
179
+ readonly name: Name;
180
+ readonly input: Input;
181
+ readonly deduplicated: boolean;
182
+ readonly result: Promise<Output>;
183
+ cancel(reason?: string): Promise<boolean>;
184
+ refresh(): Promise<JobSnapshot<Input, Output, Name>>;
185
+ }
186
+ export interface JobCallable<Input, RunInput, Output, Name extends string, Schema extends StandardSchemaV1 | undefined = undefined> {
187
+ (input: Input, options?: SubmitOptions): Promise<JobHandle<Output, RunInput, Name>>;
188
+ bulk(inputs: readonly Input[], options?: BulkOptions): Promise<Array<JobHandle<Output, RunInput, Name>>>;
189
+ schedule(options: ScheduleOptions<Input>): Promise<ScheduleHandle>;
190
+ readonly input: Schema;
191
+ }
192
+ export interface QueueStats {
193
+ queued: number;
194
+ scheduled: number;
195
+ running: number;
196
+ succeeded: number;
197
+ failed: number;
198
+ total: number;
199
+ }
200
+ /**
201
+ * `status` is required.
202
+ *
203
+ * BullMQ ranges over each underlying state separately, so an offset across a
204
+ * merged set of states cannot mean "position in the result" — it would skip
205
+ * and repeat jobs. Naming the status keeps the cursor honest.
206
+ */
207
+ export interface JobListQuery {
208
+ status: JobStatus;
209
+ limit?: number;
210
+ cursor?: string;
211
+ }
212
+ export interface JobListPage<Job = JobSnapshot> {
213
+ jobs: Job[];
214
+ cursor?: string;
215
+ }
216
+ export interface CleanupQuery {
217
+ status?: JobStatus;
218
+ olderThan?: number;
219
+ limit?: number;
220
+ }
221
+ export type AnyJobSnapshot<Definitions extends JobDefinitions> = {
222
+ [Name in keyof Definitions]: JobSnapshot<DefinitionRunInput<Definitions[Name]>, DefinitionOutput<Definitions[Name]>, Extract<Name, string>>;
223
+ }[keyof Definitions];
224
+ export interface QueueEventMap {
225
+ added: JobSnapshot;
226
+ started: JobSnapshot;
227
+ progress: JobSnapshot;
228
+ succeeded: JobSnapshot;
229
+ failed: JobSnapshot;
230
+ error: Error;
231
+ }
232
+ /**
233
+ * Only what Enqiu types or computes. Pausing, resuming and setting global
234
+ * concurrency are `bull.queue.pause()`, `.resume()` and
235
+ * `.setGlobalConcurrency()`; re-exporting them under new names would be a
236
+ * second vocabulary for the same call.
237
+ */
238
+ export interface QueueApi<Definitions extends JobDefinitions> {
239
+ get(id: string): Promise<AnyJobSnapshot<Definitions> | undefined>;
240
+ list(query: JobListQuery): Promise<JobListPage<AnyJobSnapshot<Definitions>>>;
241
+ stats(): Promise<QueueStats>;
242
+ redrive(id: string): Promise<JobHandle>;
243
+ cleanup(query?: CleanupQuery): Promise<string[]>;
244
+ /**
245
+ * Resolves once nothing is waiting, running, delayed or prioritized.
246
+ *
247
+ * A property of the queue, not of any one worker: every process sharing the
248
+ * queue sees the same answer, and a producer that keeps submitting will keep
249
+ * this pending.
250
+ */
251
+ onIdle(): Promise<void>;
252
+ on<Event extends keyof QueueEventMap>(event: Event, listener: (payload: QueueEventMap[Event]) => void): () => void;
253
+ }
254
+ export interface WorkerStartOptions {
255
+ concurrency?: number;
256
+ }
257
+ /**
258
+ * Only what needs logic. Pausing and resuming a worker is `bull.worker.pause()`
259
+ * and `bull.worker.resume()` — forwarding those through a second name adds a
260
+ * thing to learn and nothing else.
261
+ */
262
+ export interface WorkerApi {
263
+ readonly running: boolean;
264
+ start(options?: WorkerStartOptions): Promise<void>;
265
+ }
266
+ export interface WorkerOptions {
267
+ concurrency?: number;
268
+ autoStart?: boolean;
269
+ }
270
+ export interface EnqiuOptions {
271
+ /** Queue name. @default "default" */
272
+ name?: string;
273
+ /** Redis connection, passed straight to BullMQ. */
274
+ connection: ConnectionOptions;
275
+ /** Redis key namespace. */
276
+ prefix?: string;
277
+ /** Run handlers in this process. `false` makes it producer-only. */
278
+ worker?: false | WorkerOptions;
279
+ /** Queue-wide defaults, overridden per job and (for `retry`) per submission. */
280
+ retry?: number | RetryPolicy;
281
+ timeout?: number;
282
+ /**
283
+ * Structured log lines retained per job. Read them back with
284
+ * `bull.queue.getJobLogs(id)` — an extra round trip Enqiu does not make on
285
+ * your behalf. @default 100
286
+ */
287
+ logLimit?: number;
288
+ }
289
+ /** Your jobs, and nothing else. */
290
+ export type JobsApi<Definitions extends JobDefinitions> = {
291
+ readonly [Name in keyof Definitions]: JobCallable<DefinitionInput<Definitions[Name]>, DefinitionRunInput<Definitions[Name]>, DefinitionOutput<Definitions[Name]>, Extract<Name, string>, DefinitionSchema<Definitions[Name]>>;
292
+ };
293
+ /**
294
+ * What `enqiu()` returns, meant to be destructured.
295
+ *
296
+ * Keeping the queue and worker controls beside your jobs rather than among
297
+ * them is why no job name is reserved: `jobs.queue` is a job called `queue`,
298
+ * and nothing here has to police the difference.
299
+ */
300
+ export interface Enqiu<Definitions extends JobDefinitions> {
301
+ readonly jobs: JobsApi<Definitions>;
302
+ readonly queue: QueueApi<Definitions>;
303
+ readonly worker: WorkerApi;
304
+ /**
305
+ * The BullMQ objects underneath, unwrapped.
306
+ *
307
+ * Enqiu's own surface is the typed, validated path. Everything BullMQ can do
308
+ * that Enqiu does not model — flows, Pro groups, metrics, raw options — is
309
+ * reachable here without a fork, and calls through it cost nothing extra
310
+ * because there is no layer in the way.
311
+ */
312
+ readonly bull: {
313
+ readonly queue: Queue;
314
+ readonly worker: Worker | undefined;
315
+ };
316
+ /**
317
+ * Shuts down the queue, the worker and any open event stream.
318
+ *
319
+ * `drain` waits for outstanding work first. It lives here rather than on
320
+ * `worker` because it ends all three, including on a producer-only queue
321
+ * that has no worker at all.
322
+ */
323
+ close(options?: {
324
+ drain?: boolean;
325
+ }): Promise<void>;
326
+ }
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The public vocabulary of `enqiu()`.
3
+ *
4
+ * Enqiu is a typed layer over BullMQ: it owns inference and validation, BullMQ
5
+ * owns storage and execution. Anything BullMQ's open-source tier cannot
6
+ * express is absent here rather than faked — see the compatibility notes in
7
+ * the README.
8
+ */
9
+ export const definitionMarker = Symbol("enqiu.job");