@zizq-labs/zizq 0.1.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,281 @@
1
+ /**
2
+ * Typed wrappers over API responses.
3
+ *
4
+ * These classes wrap raw API data with a Client reference, providing
5
+ * action methods and pagination helpers.
6
+ *
7
+ * @module
8
+ */
9
+ import type { Client, JobStatus, UniqueScope, BackoffConfig, RetentionConfig, FailureOptions, UpdateJobOptions } from "./client.ts";
10
+ import { ErrorQuery, type ErrorQueryOptions } from "./query.ts";
11
+ /** Raw job data shape (camelCase, as returned by the API translation layer). */
12
+ export interface JobData {
13
+ /** Unique job identifier. */
14
+ id: string;
15
+ /** Job type, e.g. "send_email". */
16
+ type: string;
17
+ /** Queue this job belongs to. */
18
+ queue: string;
19
+ /** Priority (0 - 65536, lower number = higher priority). */
20
+ priority: number;
21
+ /** Lifecycle status. */
22
+ status: JobStatus;
23
+ /**
24
+ * Arbitrary payload provided by the enqueuer.
25
+ *
26
+ * Not present on metadata-only requests.
27
+ */
28
+ payload?: unknown;
29
+ /** When the job becomes eligible to run (ms since Unix epoch). */
30
+ readyAt: number;
31
+ /** Number of times this job has been previously attempted. */
32
+ attempts: number;
33
+ /** Maximum retries before the job is killed. */
34
+ retryLimit?: number;
35
+ /** Per-job backoff configuration. */
36
+ backoff?: BackoffConfig;
37
+ /** When the job was last dequeued (ms since Unix epoch). */
38
+ dequeuedAt?: number;
39
+ /** When the job last failed (ms since Unix epoch). */
40
+ failedAt?: number;
41
+ /** When the job was completed (ms since Unix epoch). */
42
+ completedAt?: number;
43
+ /** Per-job retention configuration. */
44
+ retention?: RetentionConfig;
45
+ /** When the reaper will hard-delete this job (ms since Unix epoch). */
46
+ purgeAt?: number;
47
+ /** Unique key used for enqueue-time deduplication. */
48
+ uniqueKey?: string;
49
+ /** Uniqueness scope. */
50
+ uniqueWhile?: UniqueScope;
51
+ /** True if this job was returned as a duplicate (enqueue responses only). */
52
+ duplicate?: boolean;
53
+ }
54
+ /**
55
+ * A job returned by the Zizq server.
56
+ *
57
+ * Provides readonly access to all job fields plus action methods that
58
+ * operate on this job via the Client.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const job = await client.enqueue({ type: "send_email", queue: "emails", payload: {} });
63
+ * console.log(job.id, job.status);
64
+ *
65
+ * // Mark as complete
66
+ * await job.complete();
67
+ * ```
68
+ */
69
+ export declare class Job {
70
+ /** Unique job identifier. */
71
+ readonly id: string;
72
+ /** Job type, e.g. "send_email". */
73
+ readonly type: string;
74
+ /** Queue this job belongs to. */
75
+ readonly queue: string;
76
+ /** Priority (0 - 65536, lower number = higher priority). */
77
+ readonly priority: number;
78
+ /** Lifecycle status. */
79
+ readonly status: JobStatus;
80
+ /**
81
+ * Arbitrary payload provided by the enqueuer.
82
+ *
83
+ * Not present on metadata-only requests.
84
+ */
85
+ readonly payload?: unknown;
86
+ /** When the job becomes eligible to run (ms since Unix epoch). */
87
+ readonly readyAt: number;
88
+ /** Number of times this job has been previously attempted. */
89
+ readonly attempts: number;
90
+ /** Maximum retries before the job is killed. */
91
+ readonly retryLimit?: number;
92
+ /** Per-job backoff configuration. */
93
+ readonly backoff?: BackoffConfig;
94
+ /** When the job was last dequeued (ms since Unix epoch). */
95
+ readonly dequeuedAt?: number;
96
+ /** When the job last failed (ms since Unix epoch). */
97
+ readonly failedAt?: number;
98
+ /** When the job was completed (ms since Unix epoch). */
99
+ readonly completedAt?: number;
100
+ /** Per-job retention configuration. */
101
+ readonly retention?: RetentionConfig;
102
+ /** When the reaper will hard-delete this job (ms since Unix epoch). */
103
+ readonly purgeAt?: number;
104
+ /** Unique key used for enqueue-time deduplication. */
105
+ readonly uniqueKey?: string;
106
+ /** Uniqueness scope. */
107
+ readonly uniqueWhile?: UniqueScope;
108
+ /** True if this job was returned as a duplicate (enqueue responses only). */
109
+ readonly duplicate?: boolean;
110
+ /** @internal */
111
+ private client;
112
+ /** @internal */
113
+ constructor(client: Client, data: JobData);
114
+ /**
115
+ * Return a lazy async iterator over error records for this job.
116
+ *
117
+ * Supports chainable builder methods for configuring order, limit,
118
+ * and page size.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * for await (const error of job.errors()) {
123
+ * console.log(`Attempt ${error.attempt}: ${error.message}`);
124
+ * }
125
+ *
126
+ * // Last 5 errors
127
+ * const recent = await job.errors({ order: "desc", limit: 5 }).toArray();
128
+ * ```
129
+ */
130
+ errors(options?: ErrorQueryOptions): ErrorQuery;
131
+ /** Mark this job as successfully completed. */
132
+ complete(): Promise<void>;
133
+ /**
134
+ * Report this job as failed.
135
+ *
136
+ * @param options - Error details (message, stack trace, etc.).
137
+ * @returns The updated job with new status and attempt count.
138
+ */
139
+ fail(options: FailureOptions): Promise<Job>;
140
+ /** Delete this job. */
141
+ delete(): Promise<void>;
142
+ /**
143
+ * Update this job's mutable fields.
144
+ *
145
+ * @param options - Fields to update. See `UpdateJobOptions` for null/undefined semantics.
146
+ * @returns The updated job.
147
+ */
148
+ update(options: UpdateJobOptions): Promise<Job>;
149
+ /** Return the raw job data as a plain object. */
150
+ toJSON(): JobData;
151
+ }
152
+ /**
153
+ * A page of jobs returned by `Client.listJobs()`.
154
+ *
155
+ * Contains the jobs on this page and methods to navigate to adjacent pages.
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * let page = await client.listJobs({ queue: ["emails"], limit: 10 });
160
+ *
161
+ * while (page) {
162
+ * for (const job of page) {
163
+ * console.log(job.id, job.status);
164
+ * }
165
+ * page = await page.nextPage();
166
+ * }
167
+ * ```
168
+ */
169
+ export declare class JobPage {
170
+ /** The jobs on this page. */
171
+ readonly jobs: Job[];
172
+ /** @internal */
173
+ private client;
174
+ private nextUrl;
175
+ private prevUrl;
176
+ /** @internal */
177
+ constructor(client: Client, jobs: Job[], pages: {
178
+ next?: string | null;
179
+ prev?: string | null;
180
+ });
181
+ /** Whether there is a next page. */
182
+ get hasNext(): boolean;
183
+ /** Whether there is a previous page. */
184
+ get hasPrev(): boolean;
185
+ /** Iterate over the jobs on this page. */
186
+ [Symbol.iterator](): IterableIterator<Job>;
187
+ /**
188
+ * Delete all jobs on this page.
189
+ *
190
+ * @returns The number of deleted jobs.
191
+ */
192
+ deleteAll(): Promise<number>;
193
+ /**
194
+ * Update all jobs on this page.
195
+ *
196
+ * @param apply - Fields to update. See `UpdateJobOptions` for null/undefined semantics.
197
+ * @returns The number of updated jobs.
198
+ */
199
+ updateAll(apply: UpdateJobOptions): Promise<number>;
200
+ /**
201
+ * Fetch the next page, or `null` if this is the last page.
202
+ */
203
+ nextPage(): Promise<JobPage | null>;
204
+ /**
205
+ * Fetch the previous page, or `null` if this is the first page.
206
+ */
207
+ prevPage(): Promise<JobPage | null>;
208
+ }
209
+ /** Raw error record data (camelCase). */
210
+ export interface ErrorRecordData {
211
+ /** Which attempt this error corresponds to (1-based). */
212
+ attempt: number;
213
+ /** Error message from the worker. */
214
+ message: string;
215
+ /** Error class, e.g. "TimeoutError". */
216
+ errorType?: string;
217
+ /** Stack trace / backtrace. */
218
+ backtrace?: string;
219
+ /** When the job was dequeued for this attempt (ms since Unix epoch). */
220
+ dequeuedAt: number;
221
+ /** When the job failed (ms since Unix epoch). */
222
+ failedAt: number;
223
+ }
224
+ /**
225
+ * An error record for a failed job attempt.
226
+ */
227
+ export declare class ErrorRecord {
228
+ /** Which attempt this error corresponds to (1-based). */
229
+ readonly attempt: number;
230
+ /** Error message from the worker. */
231
+ readonly message: string;
232
+ /** Error class, e.g. "TimeoutError". */
233
+ readonly errorType?: string;
234
+ /** Stack trace / backtrace. */
235
+ readonly backtrace?: string;
236
+ /** When the job was dequeued for this attempt (ms since Unix epoch). */
237
+ readonly dequeuedAt: number;
238
+ /** When the job failed (ms since Unix epoch). */
239
+ readonly failedAt: number;
240
+ /** @internal */
241
+ constructor(data: ErrorRecordData);
242
+ }
243
+ /**
244
+ * A page of error records returned by `Client.listErrors()`.
245
+ *
246
+ * @example
247
+ * ```ts
248
+ * let page = await client.listErrors("job-id");
249
+ *
250
+ * for (const error of page) {
251
+ * console.log(`Attempt ${error.attempt}: ${error.message}`);
252
+ * }
253
+ * ```
254
+ */
255
+ export declare class ErrorPage {
256
+ /** The error records on this page. */
257
+ readonly errors: ErrorRecord[];
258
+ /** @internal */
259
+ private client;
260
+ private nextUrl;
261
+ private prevUrl;
262
+ /** @internal */
263
+ constructor(client: Client, errors: ErrorRecord[], pages: {
264
+ next?: string | null;
265
+ prev?: string | null;
266
+ });
267
+ /** Whether there is a next page. */
268
+ get hasNext(): boolean;
269
+ /** Whether there is a previous page. */
270
+ get hasPrev(): boolean;
271
+ /** Iterate over the error records on this page. */
272
+ [Symbol.iterator](): IterableIterator<ErrorRecord>;
273
+ /**
274
+ * Fetch the next page, or `null` if this is the last page.
275
+ */
276
+ nextPage(): Promise<ErrorPage | null>;
277
+ /**
278
+ * Fetch the previous page, or `null` if this is the first page.
279
+ */
280
+ prevPage(): Promise<ErrorPage | null>;
281
+ }
@@ -0,0 +1,319 @@
1
+ // Copyright (c) 2026 Chris Corbyn <chris@zizq.io>
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+ import { ErrorQuery } from "./query.js";
4
+ // --- Job ---
5
+ /**
6
+ * A job returned by the Zizq server.
7
+ *
8
+ * Provides readonly access to all job fields plus action methods that
9
+ * operate on this job via the Client.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const job = await client.enqueue({ type: "send_email", queue: "emails", payload: {} });
14
+ * console.log(job.id, job.status);
15
+ *
16
+ * // Mark as complete
17
+ * await job.complete();
18
+ * ```
19
+ */
20
+ export class Job {
21
+ /** Unique job identifier. */
22
+ id;
23
+ /** Job type, e.g. "send_email". */
24
+ type;
25
+ /** Queue this job belongs to. */
26
+ queue;
27
+ /** Priority (0 - 65536, lower number = higher priority). */
28
+ priority;
29
+ /** Lifecycle status. */
30
+ status;
31
+ /**
32
+ * Arbitrary payload provided by the enqueuer.
33
+ *
34
+ * Not present on metadata-only requests.
35
+ */
36
+ payload;
37
+ /** When the job becomes eligible to run (ms since Unix epoch). */
38
+ readyAt;
39
+ /** Number of times this job has been previously attempted. */
40
+ attempts;
41
+ /** Maximum retries before the job is killed. */
42
+ retryLimit;
43
+ /** Per-job backoff configuration. */
44
+ backoff;
45
+ /** When the job was last dequeued (ms since Unix epoch). */
46
+ dequeuedAt;
47
+ /** When the job last failed (ms since Unix epoch). */
48
+ failedAt;
49
+ /** When the job was completed (ms since Unix epoch). */
50
+ completedAt;
51
+ /** Per-job retention configuration. */
52
+ retention;
53
+ /** When the reaper will hard-delete this job (ms since Unix epoch). */
54
+ purgeAt;
55
+ /** Unique key used for enqueue-time deduplication. */
56
+ uniqueKey;
57
+ /** Uniqueness scope. */
58
+ uniqueWhile;
59
+ /** True if this job was returned as a duplicate (enqueue responses only). */
60
+ duplicate;
61
+ /** @internal */
62
+ client;
63
+ /** @internal */
64
+ constructor(client, data) {
65
+ this.client = client;
66
+ this.id = data.id;
67
+ this.type = data.type;
68
+ this.queue = data.queue;
69
+ this.priority = data.priority;
70
+ this.status = data.status;
71
+ this.payload = data.payload;
72
+ this.readyAt = data.readyAt;
73
+ this.attempts = data.attempts;
74
+ this.retryLimit = data.retryLimit;
75
+ this.backoff = data.backoff;
76
+ this.dequeuedAt = data.dequeuedAt;
77
+ this.failedAt = data.failedAt;
78
+ this.completedAt = data.completedAt;
79
+ this.retention = data.retention;
80
+ this.purgeAt = data.purgeAt;
81
+ this.uniqueKey = data.uniqueKey;
82
+ this.uniqueWhile = data.uniqueWhile;
83
+ this.duplicate = data.duplicate;
84
+ }
85
+ /**
86
+ * Return a lazy async iterator over error records for this job.
87
+ *
88
+ * Supports chainable builder methods for configuring order, limit,
89
+ * and page size.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * for await (const error of job.errors()) {
94
+ * console.log(`Attempt ${error.attempt}: ${error.message}`);
95
+ * }
96
+ *
97
+ * // Last 5 errors
98
+ * const recent = await job.errors({ order: "desc", limit: 5 }).toArray();
99
+ * ```
100
+ */
101
+ errors(options) {
102
+ return new ErrorQuery(this.client, this.id, options);
103
+ }
104
+ /** Mark this job as successfully completed. */
105
+ async complete() {
106
+ return this.client.reportSuccess(this.id);
107
+ }
108
+ /**
109
+ * Report this job as failed.
110
+ *
111
+ * @param options - Error details (message, stack trace, etc.).
112
+ * @returns The updated job with new status and attempt count.
113
+ */
114
+ async fail(options) {
115
+ return this.client.reportFailure(this.id, options);
116
+ }
117
+ /** Delete this job. */
118
+ async delete() {
119
+ return this.client.deleteJob(this.id);
120
+ }
121
+ /**
122
+ * Update this job's mutable fields.
123
+ *
124
+ * @param options - Fields to update. See `UpdateJobOptions` for null/undefined semantics.
125
+ * @returns The updated job.
126
+ */
127
+ async update(options) {
128
+ return this.client.updateJob(this.id, options);
129
+ }
130
+ /** Return the raw job data as a plain object. */
131
+ toJSON() {
132
+ return {
133
+ id: this.id,
134
+ type: this.type,
135
+ queue: this.queue,
136
+ priority: this.priority,
137
+ status: this.status,
138
+ payload: this.payload,
139
+ readyAt: this.readyAt,
140
+ attempts: this.attempts,
141
+ retryLimit: this.retryLimit,
142
+ backoff: this.backoff,
143
+ dequeuedAt: this.dequeuedAt,
144
+ failedAt: this.failedAt,
145
+ completedAt: this.completedAt,
146
+ retention: this.retention,
147
+ purgeAt: this.purgeAt,
148
+ uniqueKey: this.uniqueKey,
149
+ uniqueWhile: this.uniqueWhile,
150
+ duplicate: this.duplicate,
151
+ };
152
+ }
153
+ }
154
+ // --- JobPage ---
155
+ /**
156
+ * A page of jobs returned by `Client.listJobs()`.
157
+ *
158
+ * Contains the jobs on this page and methods to navigate to adjacent pages.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * let page = await client.listJobs({ queue: ["emails"], limit: 10 });
163
+ *
164
+ * while (page) {
165
+ * for (const job of page) {
166
+ * console.log(job.id, job.status);
167
+ * }
168
+ * page = await page.nextPage();
169
+ * }
170
+ * ```
171
+ */
172
+ export class JobPage {
173
+ /** The jobs on this page. */
174
+ jobs;
175
+ /** @internal */
176
+ client;
177
+ nextUrl;
178
+ prevUrl;
179
+ /** @internal */
180
+ constructor(client, jobs, pages) {
181
+ this.client = client;
182
+ this.jobs = jobs;
183
+ this.nextUrl = pages.next ?? null;
184
+ this.prevUrl = pages.prev ?? null;
185
+ }
186
+ /** Whether there is a next page. */
187
+ get hasNext() {
188
+ return this.nextUrl !== null;
189
+ }
190
+ /** Whether there is a previous page. */
191
+ get hasPrev() {
192
+ return this.prevUrl !== null;
193
+ }
194
+ /** Iterate over the jobs on this page. */
195
+ [Symbol.iterator]() {
196
+ return this.jobs[Symbol.iterator]();
197
+ }
198
+ /**
199
+ * Delete all jobs on this page.
200
+ *
201
+ * @returns The number of deleted jobs.
202
+ */
203
+ async deleteAll() {
204
+ const ids = this.jobs.map((j) => j.id);
205
+ if (ids.length === 0)
206
+ return 0;
207
+ return this.client.deleteAllJobs({ where: { id: ids } });
208
+ }
209
+ /**
210
+ * Update all jobs on this page.
211
+ *
212
+ * @param apply - Fields to update. See `UpdateJobOptions` for null/undefined semantics.
213
+ * @returns The number of updated jobs.
214
+ */
215
+ async updateAll(apply) {
216
+ const ids = this.jobs.map((j) => j.id);
217
+ if (ids.length === 0)
218
+ return 0;
219
+ return this.client.updateAllJobs({ where: { id: ids }, apply });
220
+ }
221
+ /**
222
+ * Fetch the next page, or `null` if this is the last page.
223
+ */
224
+ async nextPage() {
225
+ if (!this.nextUrl)
226
+ return null;
227
+ return this.client.listJobsByPath(this.nextUrl);
228
+ }
229
+ /**
230
+ * Fetch the previous page, or `null` if this is the first page.
231
+ */
232
+ async prevPage() {
233
+ if (!this.prevUrl)
234
+ return null;
235
+ return this.client.listJobsByPath(this.prevUrl);
236
+ }
237
+ }
238
+ /**
239
+ * An error record for a failed job attempt.
240
+ */
241
+ export class ErrorRecord {
242
+ /** Which attempt this error corresponds to (1-based). */
243
+ attempt;
244
+ /** Error message from the worker. */
245
+ message;
246
+ /** Error class, e.g. "TimeoutError". */
247
+ errorType;
248
+ /** Stack trace / backtrace. */
249
+ backtrace;
250
+ /** When the job was dequeued for this attempt (ms since Unix epoch). */
251
+ dequeuedAt;
252
+ /** When the job failed (ms since Unix epoch). */
253
+ failedAt;
254
+ /** @internal */
255
+ constructor(data) {
256
+ this.attempt = data.attempt;
257
+ this.message = data.message;
258
+ this.errorType = data.errorType;
259
+ this.backtrace = data.backtrace;
260
+ this.dequeuedAt = data.dequeuedAt;
261
+ this.failedAt = data.failedAt;
262
+ }
263
+ }
264
+ // --- ErrorPage ---
265
+ /**
266
+ * A page of error records returned by `Client.listErrors()`.
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * let page = await client.listErrors("job-id");
271
+ *
272
+ * for (const error of page) {
273
+ * console.log(`Attempt ${error.attempt}: ${error.message}`);
274
+ * }
275
+ * ```
276
+ */
277
+ export class ErrorPage {
278
+ /** The error records on this page. */
279
+ errors;
280
+ /** @internal */
281
+ client;
282
+ nextUrl;
283
+ prevUrl;
284
+ /** @internal */
285
+ constructor(client, errors, pages) {
286
+ this.client = client;
287
+ this.errors = errors;
288
+ this.nextUrl = pages.next ?? null;
289
+ this.prevUrl = pages.prev ?? null;
290
+ }
291
+ /** Whether there is a next page. */
292
+ get hasNext() {
293
+ return this.nextUrl !== null;
294
+ }
295
+ /** Whether there is a previous page. */
296
+ get hasPrev() {
297
+ return this.prevUrl !== null;
298
+ }
299
+ /** Iterate over the error records on this page. */
300
+ [Symbol.iterator]() {
301
+ return this.errors[Symbol.iterator]();
302
+ }
303
+ /**
304
+ * Fetch the next page, or `null` if this is the last page.
305
+ */
306
+ async nextPage() {
307
+ if (!this.nextUrl)
308
+ return null;
309
+ return this.client.listErrorsByPath(this.nextUrl);
310
+ }
311
+ /**
312
+ * Fetch the previous page, or `null` if this is the first page.
313
+ */
314
+ async prevPage() {
315
+ if (!this.prevUrl)
316
+ return null;
317
+ return this.client.listErrorsByPath(this.prevUrl);
318
+ }
319
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Helpers for building unique keys from job payloads.
3
+ *
4
+ * The returned functions are suitable for assigning to `zizqOptions.uniqueKey`
5
+ * on a job function.
6
+ *
7
+ * @module
8
+ */
9
+ import { type Hash } from "node:crypto";
10
+ import type { JobFunction } from "./handler.ts";
11
+ /**
12
+ * Build a function that computes a unique key from a subset of the payload.
13
+ *
14
+ * At enqueue time, the named fields are picked from the payload,
15
+ * round-tripped through `JSON` to normalise any exotic values, hashed
16
+ * with SHA-256, and prefixed with the job type.
17
+ *
18
+ * When no fields are passed, the entire payload is hashed.
19
+ *
20
+ * The returned function is assigned to `zizqOptions.uniqueKey` on a job
21
+ * function. The resolver is called with `(fn, payload)` at enqueue time.
22
+ *
23
+ * For cross-type deduplication (e.g. a push notification and an email
24
+ * that represent the same logical event), write your own plain function
25
+ * with whatever key format you want; it will pass through unchanged.
26
+ *
27
+ * @example Unique by specific payload fields
28
+ * ```ts
29
+ * import { uniqueKey } from "@zizq-labs/zizq";
30
+ *
31
+ * async function sendEmail(payload) { ... }
32
+ * sendEmail.zizqOptions = {
33
+ * queue: "emails",
34
+ * uniqueKey: uniqueKey("userId", "action"),
35
+ * uniqueWhile: "queued",
36
+ * };
37
+ * ```
38
+ *
39
+ * @example Unique by the entire payload
40
+ * ```ts
41
+ * sendEmail.zizqOptions = {
42
+ * uniqueKey: uniqueKey(),
43
+ * };
44
+ * ```
45
+ */
46
+ export declare function uniqueKey(...fields: string[]): (fn: JobFunction, payload: unknown) => string;
47
+ /**
48
+ * Stream a JSON-compatible value into a crypto hash as canonical JSON:
49
+ * object keys sorted, arrays in order, primitives emitted via `JSON.stringify`.
50
+ *
51
+ * The resulting byte stream is unambiguous because strings are quoted,
52
+ * `null`/`true`/`false` are fixed tokens, and commas separate items within
53
+ * containers (so `[1,2]` and `[12]` hash differently).
54
+ *
55
+ * The input must already be normalised JSON data, as from
56
+ * `JSON.parse(JSON.stringify(x))`.
57
+ */
58
+ export declare function hashInto(hash: Hash, value: unknown): void;