@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/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
  }
@@ -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;
@@ -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
- let input;
54
- if (fields.length === 0) {
55
- input = payload;
56
- }
57
- else {
58
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
59
- throw new Error(`uniqueKey: cannot pick fields from a non-object payload (got ${payload === null ? "null" : Array.isArray(payload) ? "array" : typeof payload}). Use uniqueKey() with no fields to hash the whole payload, or write a custom resolver.`);
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
- // Round-trip through JSON to normalise any exotic values (Dates,
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.4.2",
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"