@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
package/dist/types.d.ts
CHANGED
|
@@ -13,6 +13,29 @@ export type JobStatus = "ready" | "in_flight" | "scheduled" | "completed" | "dea
|
|
|
13
13
|
export type SortDirection = "asc" | "desc";
|
|
14
14
|
/** Uniqueness scope for deduplication. */
|
|
15
15
|
export type UniqueScope = "queued" | "active" | "exists";
|
|
16
|
+
/**
|
|
17
|
+
* Batching configuration for folded jobs.
|
|
18
|
+
*
|
|
19
|
+
* When present on an enqueue, subsequent enqueues that share `key` may
|
|
20
|
+
* be folded into the same pending job's payload. `when` (jq predicate)
|
|
21
|
+
* decides whether to fold; `fold` (jq reducer) produces the merged
|
|
22
|
+
* payload. Both run with `$existing` bound to the current pending
|
|
23
|
+
* payload and `$new` bound to the incoming one.
|
|
24
|
+
*
|
|
25
|
+
* Requires a pro license on the server.
|
|
26
|
+
*/
|
|
27
|
+
export interface BatchConfig {
|
|
28
|
+
/** Identifies the batch. Only one pending batch per key at a time. */
|
|
29
|
+
key: string;
|
|
30
|
+
/**
|
|
31
|
+
* jq predicate deciding whether to fold. Truthy → merge into the
|
|
32
|
+
* existing batch. Falsy → seal the existing batch and start a fresh
|
|
33
|
+
* one from this enqueue.
|
|
34
|
+
*/
|
|
35
|
+
when: string;
|
|
36
|
+
/** jq expression producing the merged payload when `when` returns true. */
|
|
37
|
+
fold: string;
|
|
38
|
+
}
|
|
16
39
|
/** Serialisation format for client-server communication. */
|
|
17
40
|
export type Format = "json" | "msgpack";
|
|
18
41
|
/**
|
|
@@ -99,6 +122,12 @@ export interface EnqueueOptions {
|
|
|
99
122
|
* Uniqueness scope. Only valid when `uniqueKey` is set.
|
|
100
123
|
*/
|
|
101
124
|
uniqueWhile?: UniqueScope;
|
|
125
|
+
/**
|
|
126
|
+
* Batching configuration for folded jobs.
|
|
127
|
+
*
|
|
128
|
+
* Requires a pro license on the server.
|
|
129
|
+
*/
|
|
130
|
+
batch?: BatchConfig;
|
|
102
131
|
}
|
|
103
132
|
/** Options for reporting a job failure. */
|
|
104
133
|
export interface FailureOptions {
|
|
@@ -161,6 +190,32 @@ export interface TakeOptions {
|
|
|
161
190
|
/** AbortSignal to cancel the streaming connection. */
|
|
162
191
|
signal?: AbortSignal;
|
|
163
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* A range with inclusive bounds. Omit either side for an unbounded end.
|
|
195
|
+
*
|
|
196
|
+
* Used by the `priority`, `readyAt`, and `attempts` filter fields. An
|
|
197
|
+
* empty object (`{}`) is treated as a fully unbounded range.
|
|
198
|
+
*/
|
|
199
|
+
export interface RangeBounds {
|
|
200
|
+
/** Lower bound, inclusive. Omit for no lower bound. */
|
|
201
|
+
min?: number;
|
|
202
|
+
/** Upper bound, inclusive. Omit for no upper bound. */
|
|
203
|
+
max?: number;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A range filter that matches either a single value or a span of values.
|
|
207
|
+
*
|
|
208
|
+
* Pass a bare `number` for an exact match. Pass an object with `min` and/or
|
|
209
|
+
* `max` for a range — both bounds are **inclusive**. Omit either side for
|
|
210
|
+
* an unbounded end.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* priority: 50 // exactly 50
|
|
214
|
+
* priority: { min: 0, max: 100 } // 0..100 inclusive
|
|
215
|
+
* priority: { min: 50 } // 50 or higher
|
|
216
|
+
* priority: { max: 100 } // 100 or lower
|
|
217
|
+
*/
|
|
218
|
+
export type RangeFilter = number | RangeBounds;
|
|
164
219
|
/** Options for listing jobs with cursor-based pagination. */
|
|
165
220
|
export interface ListJobsOptions {
|
|
166
221
|
/** Cursor: start after this job ID (exclusive). */
|
|
@@ -177,6 +232,21 @@ export interface ListJobsOptions {
|
|
|
177
232
|
type?: string | string[];
|
|
178
233
|
/** Filter by job ID. Accepts a single value or an array. */
|
|
179
234
|
id?: string | string[];
|
|
235
|
+
/**
|
|
236
|
+
* Filter by priority. Accepts an exact value or a `{min, max}` range
|
|
237
|
+
* with inclusive bounds. Lower numbers are higher priority.
|
|
238
|
+
*/
|
|
239
|
+
priority?: RangeFilter;
|
|
240
|
+
/**
|
|
241
|
+
* Filter by `readyAt` (milliseconds since the Unix epoch). Accepts an
|
|
242
|
+
* exact value or a `{min, max}` range with inclusive bounds.
|
|
243
|
+
*/
|
|
244
|
+
readyAt?: RangeFilter;
|
|
245
|
+
/**
|
|
246
|
+
* Filter by failure count. Accepts an exact value or a `{min, max}`
|
|
247
|
+
* range with inclusive bounds. `0` selects jobs that have never failed.
|
|
248
|
+
*/
|
|
249
|
+
attempts?: RangeFilter;
|
|
180
250
|
/** jq expression to filter jobs by payload. */
|
|
181
251
|
filter?: string;
|
|
182
252
|
}
|
|
@@ -211,6 +281,21 @@ export interface JobFilter {
|
|
|
211
281
|
queue?: string | string[];
|
|
212
282
|
/** Filter by job type. Accepts a single value or an array. */
|
|
213
283
|
type?: string | string[];
|
|
284
|
+
/**
|
|
285
|
+
* Filter by priority. Accepts an exact value or a `{min, max}` range
|
|
286
|
+
* with inclusive bounds. Lower numbers are higher priority.
|
|
287
|
+
*/
|
|
288
|
+
priority?: RangeFilter;
|
|
289
|
+
/**
|
|
290
|
+
* Filter by `readyAt` (milliseconds since the Unix epoch). Accepts an
|
|
291
|
+
* exact value or a `{min, max}` range with inclusive bounds.
|
|
292
|
+
*/
|
|
293
|
+
readyAt?: RangeFilter;
|
|
294
|
+
/**
|
|
295
|
+
* Filter by failure count. Accepts an exact value or a `{min, max}`
|
|
296
|
+
* range with inclusive bounds. `0` selects jobs that have never failed.
|
|
297
|
+
*/
|
|
298
|
+
attempts?: RangeFilter;
|
|
214
299
|
/** jq expression to filter jobs by payload. */
|
|
215
300
|
filter?: string;
|
|
216
301
|
}
|
|
@@ -310,8 +395,28 @@ export interface EnqueueInput {
|
|
|
310
395
|
*
|
|
311
396
|
* The key is global across all queues and job types. Prefix with the job
|
|
312
397
|
* type to make it unique per job type.
|
|
398
|
+
*
|
|
399
|
+
* Accepts either a literal string or a function that derives the key
|
|
400
|
+
* from this enqueue input at call time. The function form is what
|
|
401
|
+
* `payloadHasher(...)` returns.
|
|
313
402
|
*/
|
|
314
|
-
uniqueKey?: string;
|
|
403
|
+
uniqueKey?: string | ((input: EnqueueInput) => string);
|
|
315
404
|
/** Uniqueness scope. Only valid when `uniqueKey` is set. */
|
|
316
405
|
uniqueWhile?: UniqueScope;
|
|
406
|
+
/**
|
|
407
|
+
* Batching configuration for folded jobs.
|
|
408
|
+
*
|
|
409
|
+
* `key` can be either a literal string or a function that derives the
|
|
410
|
+
* key from this enqueue input at call time. `batchConfig(...)`
|
|
411
|
+
* returns a value shaped like this with `key` as a function; users
|
|
412
|
+
* can also spread its result and override `key` with a custom
|
|
413
|
+
* derivation.
|
|
414
|
+
*
|
|
415
|
+
* Requires a pro license on the server.
|
|
416
|
+
*/
|
|
417
|
+
batch?: {
|
|
418
|
+
key: string | ((input: EnqueueInput) => string);
|
|
419
|
+
when: string;
|
|
420
|
+
fold: string;
|
|
421
|
+
};
|
|
317
422
|
}
|
package/dist/unique-key.d.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import { type Hash } from "node:crypto";
|
|
10
9
|
import type { JobFunction } from "./handler.ts";
|
|
11
10
|
/**
|
|
12
11
|
* Build a function that computes a unique key from a subset of the payload.
|
|
@@ -24,6 +23,11 @@ import type { JobFunction } from "./handler.ts";
|
|
|
24
23
|
* that represent the same logical event), write your own plain function
|
|
25
24
|
* with whatever key format you want; it will pass through unchanged.
|
|
26
25
|
*
|
|
26
|
+
* For the newer `(input) => string` shape used by `client.enqueue({...})`
|
|
27
|
+
* directly, use {@link payloadHasher} instead — this function is a
|
|
28
|
+
* `(fn, payload)`-signature adapter around it that fits the
|
|
29
|
+
* `zizqOptions.uniqueKey` slot on job functions.
|
|
30
|
+
*
|
|
27
31
|
* @example Unique by specific payload fields
|
|
28
32
|
* ```ts
|
|
29
33
|
* import { uniqueKey } from "@zizq-labs/zizq";
|
|
@@ -44,15 +48,3 @@ import type { JobFunction } from "./handler.ts";
|
|
|
44
48
|
* ```
|
|
45
49
|
*/
|
|
46
50
|
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;
|
package/dist/unique-key.js
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 Chris Corbyn <chris@zizq.io>
|
|
2
2
|
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
* Helpers for building unique keys from job payloads.
|
|
5
|
-
*
|
|
6
|
-
* The returned functions are suitable for assigning to `zizqOptions.uniqueKey`
|
|
7
|
-
* on a job function.
|
|
8
|
-
*
|
|
9
|
-
* @module
|
|
10
|
-
*/
|
|
11
|
-
import { createHash } from "node:crypto";
|
|
3
|
+
import { payloadHasher } from "./payload-hasher.js";
|
|
12
4
|
/**
|
|
13
5
|
* Build a function that computes a unique key from a subset of the payload.
|
|
14
6
|
*
|
|
@@ -25,6 +17,11 @@ import { createHash } from "node:crypto";
|
|
|
25
17
|
* that represent the same logical event), write your own plain function
|
|
26
18
|
* with whatever key format you want; it will pass through unchanged.
|
|
27
19
|
*
|
|
20
|
+
* For the newer `(input) => string` shape used by `client.enqueue({...})`
|
|
21
|
+
* directly, use {@link payloadHasher} instead — this function is a
|
|
22
|
+
* `(fn, payload)`-signature adapter around it that fits the
|
|
23
|
+
* `zizqOptions.uniqueKey` slot on job functions.
|
|
24
|
+
*
|
|
28
25
|
* @example Unique by specific payload fields
|
|
29
26
|
* ```ts
|
|
30
27
|
* import { uniqueKey } from "@zizq-labs/zizq";
|
|
@@ -45,78 +42,24 @@ import { createHash } from "node:crypto";
|
|
|
45
42
|
* ```
|
|
46
43
|
*/
|
|
47
44
|
export function uniqueKey(...fields) {
|
|
45
|
+
const only = fields.length === 0 ? undefined : fields.map((f) => `.${f}`);
|
|
46
|
+
const hasher = payloadHasher({ only });
|
|
48
47
|
return (fn, payload) => {
|
|
49
48
|
const jobType = fn.zizqOptions?.type ?? fn.name;
|
|
50
49
|
if (!jobType) {
|
|
51
50
|
throw new Error("uniqueKey: job function must have a name or zizqOptions.type");
|
|
52
51
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
if (fields.length > 0) {
|
|
53
|
+
if (payload === null ||
|
|
54
|
+
typeof payload !== "object" ||
|
|
55
|
+
Array.isArray(payload)) {
|
|
56
|
+
throw new Error(`uniqueKey: cannot pick fields from a non-object payload (got ${payload === null
|
|
57
|
+
? "null"
|
|
58
|
+
: Array.isArray(payload)
|
|
59
|
+
? "array"
|
|
60
|
+
: typeof payload}). Use uniqueKey() with no fields to hash the whole payload, or write a custom resolver.`);
|
|
60
61
|
}
|
|
61
|
-
input = pick(payload, fields);
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
// NaN, undefined, BigInt, functions, etc.) and detect circular
|
|
65
|
-
// references. The result is plain JSON-compatible data.
|
|
66
|
-
const normalised = JSON.parse(JSON.stringify(input));
|
|
67
|
-
const hash = createHash("sha256");
|
|
68
|
-
hashInto(hash, normalised);
|
|
69
|
-
return `${jobType}:${hash.digest("hex")}`;
|
|
63
|
+
return hasher({ type: fn, queue: "", payload });
|
|
70
64
|
};
|
|
71
65
|
}
|
|
72
|
-
// --- Internal helpers ---
|
|
73
|
-
/** Extract the given top-level fields from an object, preserving order. */
|
|
74
|
-
function pick(obj, fields) {
|
|
75
|
-
const result = {};
|
|
76
|
-
for (const field of fields) {
|
|
77
|
-
if (field in obj)
|
|
78
|
-
result[field] = obj[field];
|
|
79
|
-
}
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Stream a JSON-compatible value into a crypto hash as canonical JSON:
|
|
84
|
-
* object keys sorted, arrays in order, primitives emitted via `JSON.stringify`.
|
|
85
|
-
*
|
|
86
|
-
* The resulting byte stream is unambiguous because strings are quoted,
|
|
87
|
-
* `null`/`true`/`false` are fixed tokens, and commas separate items within
|
|
88
|
-
* containers (so `[1,2]` and `[12]` hash differently).
|
|
89
|
-
*
|
|
90
|
-
* The input must already be normalised JSON data, as from
|
|
91
|
-
* `JSON.parse(JSON.stringify(x))`.
|
|
92
|
-
*/
|
|
93
|
-
export function hashInto(hash, value) {
|
|
94
|
-
// Bare number, string, boolean or null. Use JSON repr.
|
|
95
|
-
if (value === null || typeof value !== "object") {
|
|
96
|
-
hash.update(JSON.stringify(value));
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// Arrays hash in the original order, with "[" and "]" markers.
|
|
100
|
-
// We don't worry about the trailing comma; we just need a stable digest.
|
|
101
|
-
if (Array.isArray(value)) {
|
|
102
|
-
hash.update("[");
|
|
103
|
-
for (const item of value) {
|
|
104
|
-
hashInto(hash, item);
|
|
105
|
-
hash.update(",");
|
|
106
|
-
}
|
|
107
|
-
hash.update("]");
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
// Objects hash in key-sorted order, with "{" and "}" markers.
|
|
111
|
-
// We don't worry about the trailing comma; we just need a stable digest.
|
|
112
|
-
hash.update("{");
|
|
113
|
-
const obj = value;
|
|
114
|
-
const keys = Object.keys(obj).sort();
|
|
115
|
-
for (const key of keys) {
|
|
116
|
-
hash.update(JSON.stringify(key));
|
|
117
|
-
hash.update(":");
|
|
118
|
-
hashInto(hash, obj[key]);
|
|
119
|
-
hash.update(",");
|
|
120
|
-
}
|
|
121
|
-
hash.update("}");
|
|
122
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zizq-labs/zizq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Node.js client for the Zizq job queue server",
|
|
5
5
|
"homepage": "https://zizq.io",
|
|
6
6
|
"repository": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsc",
|
|
36
36
|
"typecheck": "tsc --noEmit",
|
|
37
|
-
"test": "node --test --experimental-strip-types 'src/**/*.test.ts'"
|
|
37
|
+
"test": "node --test --experimental-strip-types --import ./src/test-setup.ts 'src/**/*.test.ts'"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=22.19"
|