@zizq-labs/zizq 0.4.2 → 0.6.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,348 @@
1
+ // Copyright (c) 2026 Chris Corbyn <chris@zizq.io>
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+ /**
4
+ * Builds a function that derives a stable key from an enqueue input's
5
+ * payload. Used for both batched-job keys (via `batchConfig`) and as
6
+ * a first-class option for `uniqueKey`.
7
+ *
8
+ * @module
9
+ */
10
+ import { createHash } from "node:crypto";
11
+ /**
12
+ * Build a function that hashes some or all of an enqueue's payload
13
+ * into a stable key string.
14
+ *
15
+ * The returned function takes the whole `EnqueueInput` so it can read
16
+ * both the payload (for hashing) and the type (for the optional
17
+ * prefix). Suitable for assignment to `uniqueKey` or `batch.key` on
18
+ * `client.enqueue({...})` inputs.
19
+ *
20
+ * @example Full-payload hash with type prefix
21
+ * ```ts
22
+ * uniqueKey: payloadHasher()
23
+ * // => "sendEmail:<sha256>"
24
+ * ```
25
+ *
26
+ * @example Batch by tenant only
27
+ * ```ts
28
+ * batch: {
29
+ * key: payloadHasher({ except: '.notifications' }),
30
+ * when: "...",
31
+ * fold: "...",
32
+ * }
33
+ * ```
34
+ */
35
+ export function payloadHasher(options = {}) {
36
+ const prefix = options.prefix ?? true;
37
+ const only = options.only !== undefined ? asArray(options.only) : undefined;
38
+ const except = options.except !== undefined ? asArray(options.except) : undefined;
39
+ if (only && except) {
40
+ throw new Error("payloadHasher: `only` and `except` cannot be combined");
41
+ }
42
+ // Parse paths once at construction so the returned function has no
43
+ // parse overhead per call.
44
+ const onlyPaths = only?.map(parsePath);
45
+ const exceptPaths = except?.map(parsePath);
46
+ return (input) => {
47
+ const value = deriveHashable(input.payload, onlyPaths, exceptPaths);
48
+ const hash = createHash("sha256");
49
+ hashInto(hash, value);
50
+ const digest = hash.digest("hex");
51
+ if (!prefix)
52
+ return digest;
53
+ return `${typeName(input)}:${digest}`;
54
+ };
55
+ }
56
+ // --- Internal helpers ---
57
+ function asArray(v) {
58
+ return Array.isArray(v) ? v : [v];
59
+ }
60
+ /**
61
+ * Return the value that should participate in the hash, given the
62
+ * caller's `only`/`except` config.
63
+ */
64
+ function deriveHashable(payload, onlyPaths, exceptPaths) {
65
+ // Normalise via JSON round-trip so exotic values (Dates, undefined,
66
+ // functions, etc.) become the plain JSON representation the server
67
+ // would see. Also detects circular references early.
68
+ const normalised = JSON.parse(JSON.stringify(payload));
69
+ if (onlyPaths) {
70
+ return pickPaths(normalised, onlyPaths);
71
+ }
72
+ if (exceptPaths) {
73
+ let out = normalised;
74
+ for (const steps of exceptPaths) {
75
+ out = deletePath(out, steps);
76
+ }
77
+ return out;
78
+ }
79
+ return normalised;
80
+ }
81
+ /** Sentinel returned by `walk` to distinguish "missing" from "value is null". */
82
+ const MISSING = Symbol("missing");
83
+ /**
84
+ * Reconstruct a subset of the payload containing only the requested
85
+ * paths, preserving the original nesting structure. Missing paths are
86
+ * silently skipped so hashes remain stable across payloads that omit
87
+ * optional fields.
88
+ *
89
+ * `only: ['.a', '.b']` on `{a: 1, b: 2, c: 3}` returns `{a: 1, b: 2}`.
90
+ * `only: ['.user.id']` on `{user: {id: 42, name: 'x'}}` returns
91
+ * `{user: {id: 42}}`.
92
+ */
93
+ function pickPaths(source, paths) {
94
+ let target = undefined;
95
+ for (const steps of paths) {
96
+ if (steps.length === 0)
97
+ return source; // '.' short-circuits to full payload.
98
+ const value = walk(source, steps);
99
+ if (value === MISSING)
100
+ continue;
101
+ target = setPath(target, steps, value);
102
+ }
103
+ // Never-matched: fall back to an empty object so the hash is total
104
+ // and matches what an empty pick would produce.
105
+ return target ?? {};
106
+ }
107
+ /**
108
+ * Write `value` into `target` at `steps`, creating intermediate
109
+ * objects/arrays as needed. Returns the (possibly newly-created)
110
+ * root. The choice between object/array at each level is driven by
111
+ * whether the *next* step is a key or an index.
112
+ */
113
+ function setPath(target, steps, value) {
114
+ const first = steps[0];
115
+ if (target === undefined) {
116
+ target = first.kind === "key" ? {} : [];
117
+ }
118
+ let cur = target;
119
+ for (let i = 0; i < steps.length - 1; i++) {
120
+ const step = steps[i];
121
+ const next = steps[i + 1];
122
+ const childInit = next.kind === "key" ? {} : [];
123
+ if (step.kind === "key") {
124
+ if (cur[step.name] === undefined)
125
+ cur[step.name] = childInit;
126
+ cur = cur[step.name];
127
+ }
128
+ else {
129
+ if (cur[step.index] === undefined)
130
+ cur[step.index] = childInit;
131
+ cur = cur[step.index];
132
+ }
133
+ }
134
+ const last = steps[steps.length - 1];
135
+ if (last.kind === "key")
136
+ cur[last.name] = value;
137
+ else
138
+ cur[last.index] = value;
139
+ return target;
140
+ }
141
+ /**
142
+ * Stream a JSON-compatible value into a crypto hash as canonical JSON:
143
+ * object keys sorted, arrays in order, primitives emitted via `JSON.stringify`.
144
+ *
145
+ * The resulting byte stream is unambiguous because strings are quoted,
146
+ * `null`/`true`/`false` are fixed tokens, and commas separate items within
147
+ * containers (so `[1,2]` and `[12]` hash differently).
148
+ *
149
+ * The input must already be normalised JSON data, as from
150
+ * `JSON.parse(JSON.stringify(x))`.
151
+ */
152
+ function hashInto(hash, value) {
153
+ // Bare number, string, boolean or null. Use JSON repr.
154
+ if (value === null || typeof value !== "object") {
155
+ hash.update(JSON.stringify(value));
156
+ return;
157
+ }
158
+ // Arrays hash in the original order, with "[" and "]" markers.
159
+ // We don't worry about the trailing comma; we just need a stable digest.
160
+ if (Array.isArray(value)) {
161
+ hash.update("[");
162
+ for (const item of value) {
163
+ hashInto(hash, item);
164
+ hash.update(",");
165
+ }
166
+ hash.update("]");
167
+ return;
168
+ }
169
+ // Objects hash in key-sorted order, with "{" and "}" markers.
170
+ // We don't worry about the trailing comma; we just need a stable digest.
171
+ hash.update("{");
172
+ const obj = value;
173
+ const keys = Object.keys(obj).sort();
174
+ for (const key of keys) {
175
+ hash.update(JSON.stringify(key));
176
+ hash.update(":");
177
+ hashInto(hash, obj[key]);
178
+ hash.update(",");
179
+ }
180
+ hash.update("}");
181
+ }
182
+ function typeName(input) {
183
+ if (typeof input.type === "function") {
184
+ const fn = input.type;
185
+ const name = fn.zizqOptions?.type ?? fn.name;
186
+ if (!name) {
187
+ throw new Error("payloadHasher: job function must have a name or zizqOptions.type");
188
+ }
189
+ return name;
190
+ }
191
+ return input.type;
192
+ }
193
+ /**
194
+ * Parse a jq-compatible dotted path into an ordered list of steps.
195
+ *
196
+ * Accepted forms:
197
+ * - `.` — root (no steps)
198
+ * - `.foo` — object key
199
+ * - `.foo.bar` — nested keys
200
+ * - `.foo[0]` — nth array element
201
+ * - `.[0]` — root array index
202
+ * - `.["dotted.key"]` — quoted key (escape hatch for keys with dots)
203
+ */
204
+ function parsePath(path) {
205
+ if (path === ".")
206
+ return [];
207
+ if (path === "" || path[0] !== ".") {
208
+ throw new Error(`payloadHasher: path must start with '.', got "${path}"`);
209
+ }
210
+ const steps = [];
211
+ let i = 1;
212
+ const n = path.length;
213
+ while (i < n) {
214
+ if (path[i] === "[") {
215
+ // `[N]` or `["quoted key"]`
216
+ i += 1;
217
+ if (path[i] === '"') {
218
+ // Quoted key. Consume until the matching unescaped ".
219
+ i += 1;
220
+ let name = "";
221
+ while (i < n && path[i] !== '"') {
222
+ if (path[i] === "\\" && i + 1 < n) {
223
+ name += path[i + 1];
224
+ i += 2;
225
+ }
226
+ else {
227
+ name += path[i];
228
+ i += 1;
229
+ }
230
+ }
231
+ if (path[i] !== '"') {
232
+ throw new Error(`payloadHasher: unterminated quoted key in "${path}"`);
233
+ }
234
+ i += 1; // consume "
235
+ if (path[i] !== "]") {
236
+ throw new Error(`payloadHasher: expected ']' in "${path}"`);
237
+ }
238
+ i += 1;
239
+ steps.push({ kind: "key", name });
240
+ }
241
+ else {
242
+ // Numeric index.
243
+ let digits = "";
244
+ while (i < n && path[i] >= "0" && path[i] <= "9") {
245
+ digits += path[i];
246
+ i += 1;
247
+ }
248
+ if (digits.length === 0 || path[i] !== "]") {
249
+ throw new Error(`payloadHasher: invalid array index in "${path}"`);
250
+ }
251
+ i += 1;
252
+ steps.push({ kind: "index", index: parseInt(digits, 10) });
253
+ }
254
+ }
255
+ else if (path[i] === ".") {
256
+ i += 1;
257
+ // Follow-on dot before a name or bracket.
258
+ continue;
259
+ }
260
+ else if (isNameChar(path[i], true)) {
261
+ let name = "";
262
+ while (i < n && isNameChar(path[i], false)) {
263
+ name += path[i];
264
+ i += 1;
265
+ }
266
+ steps.push({ kind: "key", name });
267
+ }
268
+ else {
269
+ throw new Error(`payloadHasher: unexpected character '${path[i]}' in "${path}"`);
270
+ }
271
+ }
272
+ return steps;
273
+ }
274
+ function isNameChar(c, first) {
275
+ if ((c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c === "_")
276
+ return true;
277
+ if (!first && c >= "0" && c <= "9")
278
+ return true;
279
+ return false;
280
+ }
281
+ /**
282
+ * Follow `steps` and return the sub-value. Returns the `MISSING`
283
+ * sentinel when any hop is absent (as opposed to a legitimate `null`
284
+ * at the terminal step). Callers use the sentinel to distinguish
285
+ * "silently skip this path" from "hash a null here".
286
+ */
287
+ function walk(value, steps) {
288
+ let cur = value;
289
+ for (const step of steps) {
290
+ if (cur == null)
291
+ return MISSING;
292
+ if (step.kind === "key") {
293
+ if (typeof cur !== "object" || Array.isArray(cur))
294
+ return MISSING;
295
+ const obj = cur;
296
+ if (!(step.name in obj))
297
+ return MISSING;
298
+ cur = obj[step.name];
299
+ }
300
+ else {
301
+ if (!Array.isArray(cur) || step.index >= cur.length)
302
+ return MISSING;
303
+ cur = cur[step.index];
304
+ }
305
+ }
306
+ return cur;
307
+ }
308
+ /** Return a copy of `value` with the value at `steps` removed. */
309
+ function deletePath(value, steps) {
310
+ if (steps.length === 0) {
311
+ // `except: ['.']` — the entire payload is excluded. Nothing left
312
+ // to hash; return null so we still produce a stable digest
313
+ // (every call collapses to the same value).
314
+ return null;
315
+ }
316
+ // Walk to the parent of the final step, cloning as we go.
317
+ const cloned = structuredClone(value);
318
+ let cur = cloned;
319
+ for (let i = 0; i < steps.length - 1; i++) {
320
+ const step = steps[i];
321
+ if (cur == null)
322
+ return cloned; // silent no-op
323
+ if (step.kind === "key") {
324
+ if (typeof cur !== "object" || Array.isArray(cur))
325
+ return cloned;
326
+ cur = cur[step.name];
327
+ }
328
+ else {
329
+ if (!Array.isArray(cur))
330
+ return cloned;
331
+ cur = cur[step.index];
332
+ }
333
+ }
334
+ if (cur == null || typeof cur !== "object")
335
+ return cloned;
336
+ const last = steps[steps.length - 1];
337
+ if (last.kind === "key") {
338
+ if (Array.isArray(cur))
339
+ return cloned;
340
+ delete cur[last.name];
341
+ }
342
+ else {
343
+ if (Array.isArray(cur)) {
344
+ cur.splice(last.index, 1);
345
+ }
346
+ }
347
+ return cloned;
348
+ }
package/dist/query.d.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * @module
21
21
  */
22
- import type { Client, JobStatus, SortDirection, UpdateJobOptions } from "./client.ts";
22
+ import type { Client, JobStatus, RangeFilter, SortDirection, UpdateJobOptions } from "./client.ts";
23
23
  import type { Job, JobPage, ErrorRecord, ErrorPage } from "./resources.ts";
24
24
  /**
25
25
  * Base class for lazy, async-iterable collections.
@@ -73,6 +73,12 @@ export interface JobQueryOptions {
73
73
  status?: JobStatus | JobStatus[];
74
74
  /** jq expression applied to the payload. */
75
75
  jqFilter?: string;
76
+ /** Priority filter — exact value or `{min, max}` inclusive range. */
77
+ priority?: RangeFilter;
78
+ /** `readyAt` filter (ms since epoch) — exact value or `{min, max}` range. */
79
+ readyAt?: RangeFilter;
80
+ /** Failure count filter — exact value or `{min, max}` inclusive range. */
81
+ attempts?: RangeFilter;
76
82
  /** Sort order. Default: "asc" (oldest first). */
77
83
  order?: SortDirection;
78
84
  /** Maximum total number of jobs to return across all pages. */
@@ -145,6 +151,9 @@ export declare class JobQuery extends Lazy<Job> {
145
151
  private _type?;
146
152
  private _status?;
147
153
  private _jqFilter?;
154
+ private _priority?;
155
+ private _readyAt?;
156
+ private _attempts?;
148
157
  private _order?;
149
158
  private _limit?;
150
159
  private _pageSize?;
@@ -172,6 +181,47 @@ export declare class JobQuery extends Lazy<Job> {
172
181
  * Add a jq payload filter. Combines with any existing filter via `and`.
173
182
  */
174
183
  addJqFilter(jqFilter: string): JobQuery;
184
+ /**
185
+ * Filter by priority. Replaces any existing priority filter.
186
+ *
187
+ * Accepts an exact value (`50`) or a `{min, max}` range with inclusive
188
+ * bounds. Omit either side for an unbounded end. Lower numbers are
189
+ * higher priority.
190
+ *
191
+ * @example
192
+ * client.jobs().byPriority(0); // exact match
193
+ * client.jobs().byPriority({ min: 0, max: 100 }); // bounded range
194
+ * client.jobs().byPriority({ min: 100 }); // 100 or higher
195
+ * client.jobs().byPriority({ max: 100 }); // 100 or lower
196
+ */
197
+ byPriority(priority: RangeFilter | undefined): JobQuery;
198
+ /**
199
+ * Filter by `readyAt` (ms since epoch). Replaces any existing filter.
200
+ *
201
+ * Accepts an exact value or a `{min, max}` range with inclusive bounds.
202
+ * Use `Date.prototype.getTime()` to derive ms from a `Date` instance.
203
+ *
204
+ * @example
205
+ * // Eligible to run by now.
206
+ * client.jobs().byReadyAt({ max: Date.now() });
207
+ *
208
+ * // Within the next hour.
209
+ * client.jobs().byReadyAt({ min: Date.now(), max: Date.now() + 3_600_000 });
210
+ */
211
+ byReadyAt(readyAt: RangeFilter | undefined): JobQuery;
212
+ /**
213
+ * Filter by failure count. Replaces any existing attempts filter.
214
+ *
215
+ * Accepts an exact value or a `{min, max}` range with inclusive bounds.
216
+ * `0` selects jobs that have never failed; `{ min: 1 }` selects anything
217
+ * that has failed at least once.
218
+ *
219
+ * @example
220
+ * client.jobs().byAttempts(0); // never failed
221
+ * client.jobs().byAttempts({ min: 1 }); // has failed
222
+ * client.jobs().byAttempts({ min: 1, max: 3 }); // 1, 2, or 3 failures
223
+ */
224
+ byAttempts(attempts: RangeFilter | undefined): JobQuery;
175
225
  /**
176
226
  * Constrain results by an exact payload match.
177
227
  *
package/dist/query.js CHANGED
@@ -161,6 +161,9 @@ export class JobQuery extends Lazy {
161
161
  _type;
162
162
  _status;
163
163
  _jqFilter;
164
+ _priority;
165
+ _readyAt;
166
+ _attempts;
164
167
  _order;
165
168
  _limit;
166
169
  _pageSize;
@@ -173,6 +176,9 @@ export class JobQuery extends Lazy {
173
176
  this._type = options.type;
174
177
  this._status = options.status;
175
178
  this._jqFilter = options.jqFilter;
179
+ this._priority = options.priority;
180
+ this._readyAt = options.readyAt;
181
+ this._attempts = options.attempts;
176
182
  this._order = options.order;
177
183
  this._limit = options.limit;
178
184
  this._pageSize = options.pageSize;
@@ -222,6 +228,53 @@ export class JobQuery extends Lazy {
222
228
  jqFilter: concat(this._jqFilter, `(${jqFilter})`).join(" and "),
223
229
  });
224
230
  }
231
+ /**
232
+ * Filter by priority. Replaces any existing priority filter.
233
+ *
234
+ * Accepts an exact value (`50`) or a `{min, max}` range with inclusive
235
+ * bounds. Omit either side for an unbounded end. Lower numbers are
236
+ * higher priority.
237
+ *
238
+ * @example
239
+ * client.jobs().byPriority(0); // exact match
240
+ * client.jobs().byPriority({ min: 0, max: 100 }); // bounded range
241
+ * client.jobs().byPriority({ min: 100 }); // 100 or higher
242
+ * client.jobs().byPriority({ max: 100 }); // 100 or lower
243
+ */
244
+ byPriority(priority) {
245
+ return this.rebuild({ priority });
246
+ }
247
+ /**
248
+ * Filter by `readyAt` (ms since epoch). Replaces any existing filter.
249
+ *
250
+ * Accepts an exact value or a `{min, max}` range with inclusive bounds.
251
+ * Use `Date.prototype.getTime()` to derive ms from a `Date` instance.
252
+ *
253
+ * @example
254
+ * // Eligible to run by now.
255
+ * client.jobs().byReadyAt({ max: Date.now() });
256
+ *
257
+ * // Within the next hour.
258
+ * client.jobs().byReadyAt({ min: Date.now(), max: Date.now() + 3_600_000 });
259
+ */
260
+ byReadyAt(readyAt) {
261
+ return this.rebuild({ readyAt });
262
+ }
263
+ /**
264
+ * Filter by failure count. Replaces any existing attempts filter.
265
+ *
266
+ * Accepts an exact value or a `{min, max}` range with inclusive bounds.
267
+ * `0` selects jobs that have never failed; `{ min: 1 }` selects anything
268
+ * that has failed at least once.
269
+ *
270
+ * @example
271
+ * client.jobs().byAttempts(0); // never failed
272
+ * client.jobs().byAttempts({ min: 1 }); // has failed
273
+ * client.jobs().byAttempts({ min: 1, max: 3 }); // 1, 2, or 3 failures
274
+ */
275
+ byAttempts(attempts) {
276
+ return this.rebuild({ attempts });
277
+ }
225
278
  /**
226
279
  * Constrain results by an exact payload match.
227
280
  *
@@ -453,6 +506,9 @@ export class JobQuery extends Lazy {
453
506
  type: this._type,
454
507
  status: this._status,
455
508
  filter: this._jqFilter,
509
+ priority: this._priority,
510
+ readyAt: this._readyAt,
511
+ attempts: this._attempts,
456
512
  order: this._order,
457
513
  limit: effectivePageSize,
458
514
  });
@@ -474,6 +530,9 @@ export class JobQuery extends Lazy {
474
530
  type: this._type,
475
531
  status: this._status,
476
532
  filter: this._jqFilter,
533
+ priority: this._priority,
534
+ readyAt: this._readyAt,
535
+ attempts: this._attempts,
477
536
  };
478
537
  }
479
538
  rebuild(overrides) {
@@ -483,6 +542,9 @@ export class JobQuery extends Lazy {
483
542
  type: this._type,
484
543
  status: this._status,
485
544
  jqFilter: this._jqFilter,
545
+ priority: this._priority,
546
+ readyAt: this._readyAt,
547
+ attempts: this._attempts,
486
548
  order: this._order,
487
549
  limit: this._limit,
488
550
  pageSize: this._pageSize,
@@ -7,7 +7,7 @@
7
7
  * @module
8
8
  */
9
9
  import type { Client } from "./client.ts";
10
- import type { JobStatus, UniqueScope, BackoffConfig, RetentionConfig, EnqueueOptions, FailureOptions, UpdateJobOptions } from "./types.ts";
10
+ import type { JobStatus, UniqueScope, BackoffConfig, BatchConfig, RetentionConfig, EnqueueOptions, FailureOptions, UpdateJobOptions } from "./types.ts";
11
11
  import { ErrorQuery, type ErrorQueryOptions } from "./query.ts";
12
12
  /** Raw job data shape (camelCase, as returned by the API translation layer). */
13
13
  export interface JobData {
@@ -51,6 +51,16 @@ export interface JobData {
51
51
  uniqueWhile?: UniqueScope;
52
52
  /** True if this job was returned as a duplicate (enqueue responses only). */
53
53
  duplicate?: boolean;
54
+ /**
55
+ * True if this enqueue was folded into an existing pending batched
56
+ * job (enqueue responses only).
57
+ */
58
+ folded?: boolean;
59
+ /**
60
+ * Batching configuration stored on this job. Present only for
61
+ * batched jobs.
62
+ */
63
+ batch?: BatchConfig;
54
64
  }
55
65
  /**
56
66
  * A job returned by the Zizq server.
@@ -108,6 +118,18 @@ export declare class Job {
108
118
  readonly uniqueWhile?: UniqueScope;
109
119
  /** True if this job was returned as a duplicate (enqueue responses only). */
110
120
  readonly duplicate?: boolean;
121
+ /**
122
+ * True if this enqueue was folded into an existing pending batched
123
+ * job (enqueue responses only).
124
+ */
125
+ readonly folded?: boolean;
126
+ /**
127
+ * Batching configuration stored on this job. Present only for
128
+ * batched jobs. The first enqueue's `when`/`fold` are the ones
129
+ * that apply for the life of the batch, so this is the source of
130
+ * truth for what's being evaluated on subsequent folds.
131
+ */
132
+ readonly batch?: BatchConfig;
111
133
  /** @internal */
112
134
  private client;
113
135
  /** @internal */
package/dist/resources.js CHANGED
@@ -57,6 +57,18 @@ export class Job {
57
57
  uniqueWhile;
58
58
  /** True if this job was returned as a duplicate (enqueue responses only). */
59
59
  duplicate;
60
+ /**
61
+ * True if this enqueue was folded into an existing pending batched
62
+ * job (enqueue responses only).
63
+ */
64
+ folded;
65
+ /**
66
+ * Batching configuration stored on this job. Present only for
67
+ * batched jobs. The first enqueue's `when`/`fold` are the ones
68
+ * that apply for the life of the batch, so this is the source of
69
+ * truth for what's being evaluated on subsequent folds.
70
+ */
71
+ batch;
60
72
  /** @internal */
61
73
  client;
62
74
  /** @internal */
@@ -80,6 +92,8 @@ export class Job {
80
92
  this.uniqueKey = data.uniqueKey;
81
93
  this.uniqueWhile = data.uniqueWhile;
82
94
  this.duplicate = data.duplicate;
95
+ this.folded = data.folded;
96
+ this.batch = data.batch;
83
97
  }
84
98
  /**
85
99
  * Return a lazy async iterator over error records for this job.
@@ -147,6 +161,8 @@ export class Job {
147
161
  uniqueKey: this.uniqueKey,
148
162
  uniqueWhile: this.uniqueWhile,
149
163
  duplicate: this.duplicate,
164
+ folded: this.folded,
165
+ batch: this.batch,
150
166
  };
151
167
  }
152
168
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ // Copyright (c) 2026 Chris Corbyn <chris@zizq.io>
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+ /**
4
+ * Loaded before every test via `node --import`.
5
+ *
6
+ * Suppresses the `ZIZQ_JOB_FUNCTIONS_DEPRECATED` warning during test
7
+ * runs — the tests that exercise the deprecated code paths still need
8
+ * to run, and we don't want their output cluttered with the same
9
+ * migration hint they're validating. Any other warning (Node's own
10
+ * experimental notices, real deprecations from dependencies) flows
11
+ * through unchanged.
12
+ *
13
+ * @module
14
+ */
15
+ const SUPPRESSED_CODES = new Set(["ZIZQ_JOB_FUNCTIONS_DEPRECATED"]);
16
+ const originalListeners = process.listeners("warning");
17
+ process.removeAllListeners("warning");
18
+ process.on("warning", (warning) => {
19
+ if (warning instanceof Error &&
20
+ "code" in warning &&
21
+ typeof warning.code === "string" &&
22
+ SUPPRESSED_CODES.has(warning.code)) {
23
+ return;
24
+ }
25
+ // Re-dispatch to whatever the default handler would have done.
26
+ for (const listener of originalListeners) {
27
+ listener(warning);
28
+ }
29
+ });
30
+ export {};