@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.
- package/README.md +8 -38
- package/dist/batch-config.d.ts +64 -0
- package/dist/batch-config.js +76 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +52 -3
- package/dist/enqueue.js +35 -1
- package/dist/handler.js +14 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/payload-hasher.d.ts +50 -0
- package/dist/payload-hasher.js +348 -0
- package/dist/query.d.ts +51 -1
- package/dist/query.js +62 -0
- package/dist/resources.d.ts +23 -1
- package/dist/resources.js +16 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +30 -0
- package/dist/types.d.ts +106 -1
- package/dist/unique-key.d.ts +5 -13
- package/dist/unique-key.js +18 -75
- package/package.json +2 -2
|
@@ -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,
|
package/dist/resources.d.ts
CHANGED
|
@@ -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 {};
|