@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/README.md
CHANGED
|
@@ -12,7 +12,7 @@ This is the official Zizq client library for Node.js, written in TypeScript.
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
* Concurrent async-based worker
|
|
15
|
-
*
|
|
15
|
+
* Simple handler functions, or composable `Router`
|
|
16
16
|
* Enqueue and process jobs from one language to another
|
|
17
17
|
* Arbitrary named queues
|
|
18
18
|
* Granular job priorities
|
|
@@ -21,7 +21,8 @@ This is the official Zizq client library for Node.js, written in TypeScript.
|
|
|
21
21
|
* Configurable job retention policies
|
|
22
22
|
* Recurring jobs (cron)
|
|
23
23
|
* Job introspection and management APIs, with support for `jq` query filters
|
|
24
|
-
* Unique jobs
|
|
24
|
+
* Unique jobs (de-duplicated)
|
|
25
|
+
* Batched jobs (folded/merged)
|
|
25
26
|
|
|
26
27
|
## Installation
|
|
27
28
|
|
|
@@ -106,7 +107,7 @@ await client.enqueueBulk([
|
|
|
106
107
|
|
|
107
108
|
### Defining handlers
|
|
108
109
|
|
|
109
|
-
A handler is
|
|
110
|
+
A handler is any `async` function that accepts a `job` and either resolves
|
|
110
111
|
(the worker acks it as successful) or throws (the worker reports a failure
|
|
111
112
|
and the server retries per the backoff policy). The simplest version is a
|
|
112
113
|
`switch` on `job.type`:
|
|
@@ -124,38 +125,9 @@ async function handler(job) {
|
|
|
124
125
|
}
|
|
125
126
|
```
|
|
126
127
|
|
|
127
|
-
For a more composable style, the client ships `
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
(queue, priority, backoff, retention, uniqueness), so enqueuing later only
|
|
131
|
-
needs the payload:
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
import { buildHandler } from "@zizq-labs/zizq";
|
|
135
|
-
|
|
136
|
-
async function sendEmail(payload, job) {
|
|
137
|
-
// ...
|
|
138
|
-
}
|
|
139
|
-
sendEmail.zizqOptions = { queue: "emails", priority: 100 };
|
|
140
|
-
|
|
141
|
-
async function generateReport(payload) {
|
|
142
|
-
// ...
|
|
143
|
-
}
|
|
144
|
-
generateReport.zizqOptions = { queue: "reports" };
|
|
145
|
-
|
|
146
|
-
const handler = buildHandler([sendEmail, generateReport]);
|
|
147
|
-
|
|
148
|
-
// Job functions can be enqueued directly — the queue and other defaults
|
|
149
|
-
// come from the function's zizqOptions.
|
|
150
|
-
await client.enqueue({
|
|
151
|
-
type: sendEmail,
|
|
152
|
-
payload: { userId: 42, template: "welcome" },
|
|
153
|
-
});
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
For cross-language workflows or when you want explicit `type -> handler`
|
|
157
|
-
registration with optional fallback, use `Router`. Routes match by job
|
|
158
|
-
type (a string the producer agrees on with the consumer), and an
|
|
128
|
+
For a more composable style, the client ships a `Router` that implements
|
|
129
|
+
handler registration for named job types with optional fallback. Routes match
|
|
130
|
+
by job type (a string the producer agrees on with the consumer), and an
|
|
159
131
|
optional `fallback` catches anything unmatched:
|
|
160
132
|
|
|
161
133
|
```ts
|
|
@@ -220,8 +192,6 @@ replacing, and removing entries as the definition changes. Cron requires
|
|
|
220
192
|
a Pro license on the server.
|
|
221
193
|
|
|
222
194
|
```ts
|
|
223
|
-
import { sendDailyDigest } from "./handlers";
|
|
224
|
-
|
|
225
195
|
await client.cron("maintenance").register({
|
|
226
196
|
timezone: "Europe/London",
|
|
227
197
|
entries: [
|
|
@@ -235,7 +205,7 @@ await client.cron("maintenance").register({
|
|
|
235
205
|
{
|
|
236
206
|
name: "daily_digest",
|
|
237
207
|
expression: "0 9 * * *",
|
|
238
|
-
type:
|
|
208
|
+
type: "send_daily_digest",
|
|
239
209
|
payload: {},
|
|
240
210
|
},
|
|
241
211
|
],
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { EnqueueInput } from "./types.ts";
|
|
2
|
+
/** Options for the third argument of {@link batchConfig}. */
|
|
3
|
+
export interface BatchConfigOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Append `| unique` to the fold expression to deduplicate entries
|
|
6
|
+
* within the batch. `unique` in jq also sorts, so it subsumes
|
|
7
|
+
* `sorted:` when both are set.
|
|
8
|
+
*/
|
|
9
|
+
dedup?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Append `| sort` to the fold expression to sort entries within
|
|
12
|
+
* the batch. Ignored when `dedup:` is also set.
|
|
13
|
+
*/
|
|
14
|
+
sorted?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build a batched-job configuration for a specific target path.
|
|
18
|
+
*
|
|
19
|
+
* Returns the `{key, when, fold}` shape expected by
|
|
20
|
+
* `client.enqueue({batch: ...})`, with `key` as a function that
|
|
21
|
+
* derives a stable batch key from the enqueue input at call time
|
|
22
|
+
* (hashing everything *except* the batch target path).
|
|
23
|
+
*
|
|
24
|
+
* @param limit Maximum combined length of the batched value at
|
|
25
|
+
* `path` before the current batch is sealed and a new one starts.
|
|
26
|
+
* @param path jq path to the batch target within the payload.
|
|
27
|
+
* Defaults to `.` (the whole payload is the batch target, assumed
|
|
28
|
+
* to be an array).
|
|
29
|
+
* @param options `dedup: true` deduplicates via `| unique`;
|
|
30
|
+
* `sorted: true` sorts via `| sort`. `dedup:` subsumes `sorted:`.
|
|
31
|
+
*
|
|
32
|
+
* @example Whole-payload batch (payload is an array)
|
|
33
|
+
* ```ts
|
|
34
|
+
* await client.enqueue({
|
|
35
|
+
* type: "audit.events",
|
|
36
|
+
* queue: "audit",
|
|
37
|
+
* payload: [{ actor: "u1", action: "login" }],
|
|
38
|
+
* batch: batchConfig(1000),
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @example Batch a specific field
|
|
43
|
+
* ```ts
|
|
44
|
+
* await client.enqueue({
|
|
45
|
+
* type: "push",
|
|
46
|
+
* queue: "push",
|
|
47
|
+
* payload: { deviceIds: [id], platform: "apple" },
|
|
48
|
+
* batch: batchConfig(100, ".deviceIds"),
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @example Custom key (spread + override)
|
|
53
|
+
* ```ts
|
|
54
|
+
* batch: {
|
|
55
|
+
* ...batchConfig(100, ".deviceIds"),
|
|
56
|
+
* key: (input) => `push:tenant-${input.payload.tenantId}`,
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function batchConfig(limit: number, path?: string, options?: BatchConfigOptions): {
|
|
61
|
+
key: (input: EnqueueInput) => string;
|
|
62
|
+
when: string;
|
|
63
|
+
fold: string;
|
|
64
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Copyright (c) 2026 Chris Corbyn <chris@zizq.io>
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
/**
|
|
4
|
+
* Helper for building batched-job configuration objects. See
|
|
5
|
+
* {@link batchConfig}.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { payloadHasher } from "./payload-hasher.js";
|
|
10
|
+
/**
|
|
11
|
+
* Build a batched-job configuration for a specific target path.
|
|
12
|
+
*
|
|
13
|
+
* Returns the `{key, when, fold}` shape expected by
|
|
14
|
+
* `client.enqueue({batch: ...})`, with `key` as a function that
|
|
15
|
+
* derives a stable batch key from the enqueue input at call time
|
|
16
|
+
* (hashing everything *except* the batch target path).
|
|
17
|
+
*
|
|
18
|
+
* @param limit Maximum combined length of the batched value at
|
|
19
|
+
* `path` before the current batch is sealed and a new one starts.
|
|
20
|
+
* @param path jq path to the batch target within the payload.
|
|
21
|
+
* Defaults to `.` (the whole payload is the batch target, assumed
|
|
22
|
+
* to be an array).
|
|
23
|
+
* @param options `dedup: true` deduplicates via `| unique`;
|
|
24
|
+
* `sorted: true` sorts via `| sort`. `dedup:` subsumes `sorted:`.
|
|
25
|
+
*
|
|
26
|
+
* @example Whole-payload batch (payload is an array)
|
|
27
|
+
* ```ts
|
|
28
|
+
* await client.enqueue({
|
|
29
|
+
* type: "audit.events",
|
|
30
|
+
* queue: "audit",
|
|
31
|
+
* payload: [{ actor: "u1", action: "login" }],
|
|
32
|
+
* batch: batchConfig(1000),
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example Batch a specific field
|
|
37
|
+
* ```ts
|
|
38
|
+
* await client.enqueue({
|
|
39
|
+
* type: "push",
|
|
40
|
+
* queue: "push",
|
|
41
|
+
* payload: { deviceIds: [id], platform: "apple" },
|
|
42
|
+
* batch: batchConfig(100, ".deviceIds"),
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @example Custom key (spread + override)
|
|
47
|
+
* ```ts
|
|
48
|
+
* batch: {
|
|
49
|
+
* ...batchConfig(100, ".deviceIds"),
|
|
50
|
+
* key: (input) => `push:tenant-${input.payload.tenantId}`,
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function batchConfig(limit, path = ".", options = {}) {
|
|
55
|
+
// `payloadHasher` validates `path` via its jq-path parser and
|
|
56
|
+
// throws on invalid syntax at construction time — surfacing typos
|
|
57
|
+
// before any enqueue happens.
|
|
58
|
+
const key = payloadHasher({ except: [path] });
|
|
59
|
+
// Pipe-form access (`$var | <path>`) is the uniform shape that
|
|
60
|
+
// works for every path we accept, including the root `.` case
|
|
61
|
+
// where `$existing<path>` would otherwise be a syntax error.
|
|
62
|
+
const when = `(($existing | ${path}) + ($new | ${path})) | length <= ${limit}`;
|
|
63
|
+
let fold;
|
|
64
|
+
if (options.dedup) {
|
|
65
|
+
fold =
|
|
66
|
+
`$existing | ${path} = ((${path}) + ($new | ${path}) | unique)`;
|
|
67
|
+
}
|
|
68
|
+
else if (options.sorted) {
|
|
69
|
+
fold =
|
|
70
|
+
`$existing | ${path} = ((${path}) + ($new | ${path}) | sort)`;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
fold = `$existing | ${path} += ($new | ${path})`;
|
|
74
|
+
}
|
|
75
|
+
return { key, when, fold };
|
|
76
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { type Dispatcher } from "undici";
|
|
|
34
34
|
import { Job, JobPage, ErrorRecord, ErrorPage, CronGroup, CronEntry } from "./resources.ts";
|
|
35
35
|
import { JobQuery, type JobQueryOptions } from "./query.ts";
|
|
36
36
|
import { CronHandle } from "./cron.ts";
|
|
37
|
-
export type { JobStatus, SortDirection, UniqueScope, BackoffConfig, RetentionConfig, EnqueueOptions, EnqueueInput, FailureOptions, UpdateJobOptions, Format, TakeOptions, ListJobsOptions, ListErrorsOptions, UpdateAllJobsOptions, JobFilter, DeleteAllJobsOptions, CronEntryInput, ReplaceCronGroupOptions, } from "./types.ts";
|
|
37
|
+
export type { JobStatus, SortDirection, UniqueScope, BackoffConfig, RetentionConfig, EnqueueOptions, EnqueueInput, FailureOptions, UpdateJobOptions, Format, TakeOptions, ListJobsOptions, ListErrorsOptions, UpdateAllJobsOptions, JobFilter, DeleteAllJobsOptions, CronEntryInput, RangeBounds, RangeFilter, ReplaceCronGroupOptions, } from "./types.ts";
|
|
38
38
|
import type { EnqueueOptions, EnqueueInput, FailureOptions, UpdateJobOptions, Format, TakeOptions, ListJobsOptions, ListErrorsOptions, UpdateAllJobsOptions, JobFilter, DeleteAllJobsOptions, CronEntryInput, ReplaceCronGroupOptions } from "./types.ts";
|
|
39
39
|
export { Job, JobPage, ErrorRecord, ErrorPage, CronGroup, CronEntry, type JobData, type ErrorRecordData, type CronGroupData, type CronEntryData, } from "./resources.ts";
|
|
40
40
|
/** Response shape for `GET /health`. */
|
package/dist/client.js
CHANGED
|
@@ -367,7 +367,7 @@ export class Client {
|
|
|
367
367
|
async deleteAllJobs(options = {}) {
|
|
368
368
|
assertOnlyKeys("deleteAllJobs", options, ["where"]);
|
|
369
369
|
const where = options.where ?? {};
|
|
370
|
-
assertOnlyKeys("deleteAllJobs.where", where, ["id", "status", "queue", "type", "filter"]);
|
|
370
|
+
assertOnlyKeys("deleteAllJobs.where", where, ["id", "status", "queue", "type", "filter", "priority", "readyAt", "attempts"]);
|
|
371
371
|
// Build the filter params, then short-circuit if any array filter resolved
|
|
372
372
|
// to empty — an empty filter matches nothing, and we don't want to
|
|
373
373
|
// accidentally delete everything.
|
|
@@ -417,7 +417,7 @@ export class Client {
|
|
|
417
417
|
* ```
|
|
418
418
|
*/
|
|
419
419
|
async countJobs(where = {}) {
|
|
420
|
-
assertOnlyKeys("countJobs", where, ["id", "status", "queue", "type", "filter"]);
|
|
420
|
+
assertOnlyKeys("countJobs", where, ["id", "status", "queue", "type", "filter", "priority", "readyAt", "attempts"]);
|
|
421
421
|
const params = buildJobFilter(where);
|
|
422
422
|
if (isEmptyFilter(params))
|
|
423
423
|
return 0;
|
|
@@ -468,7 +468,7 @@ export class Client {
|
|
|
468
468
|
async updateAllJobs(options) {
|
|
469
469
|
assertOnlyKeys("updateAllJobs", options, ["where", "apply"]);
|
|
470
470
|
const where = options.where ?? {};
|
|
471
|
-
assertOnlyKeys("updateAllJobs.where", where, ["id", "status", "queue", "type", "filter"]);
|
|
471
|
+
assertOnlyKeys("updateAllJobs.where", where, ["id", "status", "queue", "type", "filter", "priority", "readyAt", "attempts"]);
|
|
472
472
|
// Same empty-filter short-circuit as deleteAllJobs.
|
|
473
473
|
const params = buildJobFilter(where);
|
|
474
474
|
if (isEmptyFilter(params))
|
|
@@ -1059,6 +1059,42 @@ function isEmptyFilter(params) {
|
|
|
1059
1059
|
params.get("queue") === "" ||
|
|
1060
1060
|
params.get("type") === "");
|
|
1061
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Encode a {@link RangeFilter} value into the server's query syntax.
|
|
1064
|
+
*
|
|
1065
|
+
* The four accepted shapes:
|
|
1066
|
+
*
|
|
1067
|
+
* - `number` (e.g. `50`) -> `"50"`
|
|
1068
|
+
* - `{ min, max }` -> `"min..max"`
|
|
1069
|
+
* - `{ min }` -> `"min.."`
|
|
1070
|
+
* - `{ max }` -> `"..max"`
|
|
1071
|
+
*
|
|
1072
|
+
* Returns `undefined` for `null`/`undefined` so the caller can skip
|
|
1073
|
+
* setting the param. Throws `TypeError` for any other shape (e.g. arrays,
|
|
1074
|
+
* strings, objects with unknown keys).
|
|
1075
|
+
*/
|
|
1076
|
+
function encodeRange(value) {
|
|
1077
|
+
if (value == null)
|
|
1078
|
+
return undefined;
|
|
1079
|
+
if (typeof value === "number") {
|
|
1080
|
+
if (!Number.isFinite(value)) {
|
|
1081
|
+
throw new TypeError(`range filter value must be a finite number, got ${value}`);
|
|
1082
|
+
}
|
|
1083
|
+
return String(value);
|
|
1084
|
+
}
|
|
1085
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
1086
|
+
throw new TypeError(`range filter must be a number or { min?, max? } object, got ${typeof value}`);
|
|
1087
|
+
}
|
|
1088
|
+
const { min, max } = value;
|
|
1089
|
+
for (const [key, bound] of [["min", min], ["max", max]]) {
|
|
1090
|
+
if (bound != null && (typeof bound !== "number" || !Number.isFinite(bound))) {
|
|
1091
|
+
throw new TypeError(`range filter "${key}" must be a finite number, got ${typeof bound}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const lo = min != null ? String(min) : "";
|
|
1095
|
+
const hi = max != null ? String(max) : "";
|
|
1096
|
+
return `${lo}..${hi}`;
|
|
1097
|
+
}
|
|
1062
1098
|
/**
|
|
1063
1099
|
* Populate a {@link URLSearchParams} with the standard job filter fields.
|
|
1064
1100
|
*/
|
|
@@ -1073,6 +1109,15 @@ function buildJobFilter(where, params = new URLSearchParams()) {
|
|
|
1073
1109
|
params.set("type", toCommaList(where.type));
|
|
1074
1110
|
if (where.filter != null)
|
|
1075
1111
|
params.set("filter", where.filter);
|
|
1112
|
+
const priority = encodeRange(where.priority);
|
|
1113
|
+
if (priority != null)
|
|
1114
|
+
params.set("priority", priority);
|
|
1115
|
+
const readyAt = encodeRange(where.readyAt);
|
|
1116
|
+
if (readyAt != null)
|
|
1117
|
+
params.set("ready_at", readyAt);
|
|
1118
|
+
const attempts = encodeRange(where.attempts);
|
|
1119
|
+
if (attempts != null)
|
|
1120
|
+
params.set("attempts", attempts);
|
|
1076
1121
|
return params;
|
|
1077
1122
|
}
|
|
1078
1123
|
/**
|
|
@@ -1109,6 +1154,7 @@ function enqueueToApi(opts) {
|
|
|
1109
1154
|
retention: opts.retention && retentionToApi(opts.retention),
|
|
1110
1155
|
unique_key: opts.uniqueKey,
|
|
1111
1156
|
unique_while: opts.uniqueWhile,
|
|
1157
|
+
batch: opts.batch,
|
|
1112
1158
|
});
|
|
1113
1159
|
}
|
|
1114
1160
|
/**
|
|
@@ -1171,6 +1217,8 @@ function jobFromApi(raw) {
|
|
|
1171
1217
|
uniqueKey: r.unique_key,
|
|
1172
1218
|
uniqueWhile: r.unique_while,
|
|
1173
1219
|
duplicate: r.duplicate,
|
|
1220
|
+
folded: r.folded,
|
|
1221
|
+
batch: r.batch,
|
|
1174
1222
|
});
|
|
1175
1223
|
}
|
|
1176
1224
|
/** Convert an API-format backoff to camelCase. */
|
|
@@ -1263,6 +1311,7 @@ function cronJobToApi(job) {
|
|
|
1263
1311
|
retention: job.retention && retentionToApi(job.retention),
|
|
1264
1312
|
unique_key: job.uniqueKey,
|
|
1265
1313
|
unique_while: job.uniqueWhile,
|
|
1314
|
+
batch: job.batch,
|
|
1266
1315
|
});
|
|
1267
1316
|
}
|
|
1268
1317
|
/** Convert a CronEntryInput to the server's API format. */
|
package/dist/enqueue.js
CHANGED
|
@@ -37,6 +37,16 @@ function computeUniqueKey(jobFn, payload) {
|
|
|
37
37
|
return uniqueKey(jobFn, payload);
|
|
38
38
|
return uniqueKey;
|
|
39
39
|
}
|
|
40
|
+
// Resolve a value that may be either a literal or a function derived
|
|
41
|
+
// from the enqueue input. Used for both `uniqueKey` and `batch.key`
|
|
42
|
+
// on `EnqueueInput` so `payloadHasher(...)` and other userland
|
|
43
|
+
// factories can produce a function that receives the input at call
|
|
44
|
+
// time.
|
|
45
|
+
function resolveKey(value, input) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
return typeof value === "function" ? value(input) : value;
|
|
49
|
+
}
|
|
40
50
|
/**
|
|
41
51
|
* Resolve an EnqueueInput into a low-level EnqueueOptions.
|
|
42
52
|
*
|
|
@@ -54,6 +64,7 @@ export function resolveInput(input) {
|
|
|
54
64
|
if (!jobType) {
|
|
55
65
|
throw new Error("Job function must have a name or zizqOptions.type");
|
|
56
66
|
}
|
|
67
|
+
warnJobFunctionDeprecated(input.type, jobType);
|
|
57
68
|
}
|
|
58
69
|
else {
|
|
59
70
|
jobType = input.type;
|
|
@@ -62,11 +73,18 @@ export function resolveInput(input) {
|
|
|
62
73
|
if (!queue) {
|
|
63
74
|
throw new Error(`No queue specified for job type "${jobType}"`);
|
|
64
75
|
}
|
|
65
|
-
const uniqueKey = input.uniqueKey ??
|
|
76
|
+
const uniqueKey = resolveKey(input.uniqueKey, input) ??
|
|
66
77
|
(typeof input.type === "function"
|
|
67
78
|
? computeUniqueKey(input.type, input.payload)
|
|
68
79
|
: undefined);
|
|
69
80
|
const uniqueWhile = input.uniqueWhile ?? defaults?.uniqueWhile;
|
|
81
|
+
const batch = input.batch
|
|
82
|
+
? {
|
|
83
|
+
key: resolveKey(input.batch.key, input),
|
|
84
|
+
when: input.batch.when,
|
|
85
|
+
fold: input.batch.fold,
|
|
86
|
+
}
|
|
87
|
+
: undefined;
|
|
70
88
|
const opts = {
|
|
71
89
|
type: jobType,
|
|
72
90
|
queue,
|
|
@@ -78,6 +96,7 @@ export function resolveInput(input) {
|
|
|
78
96
|
retention: input.retention,
|
|
79
97
|
uniqueKey,
|
|
80
98
|
uniqueWhile: uniqueKey ? uniqueWhile : undefined,
|
|
99
|
+
batch,
|
|
81
100
|
};
|
|
82
101
|
// Apply the transform hook (if any). Transforms can change the options based
|
|
83
102
|
// on the payload or other factors, and can mutate `opts` in place, or return
|
|
@@ -88,3 +107,18 @@ export function resolveInput(input) {
|
|
|
88
107
|
}
|
|
89
108
|
return opts;
|
|
90
109
|
}
|
|
110
|
+
// --- Deprecation warnings ---
|
|
111
|
+
// One warning per unique job function, ever. WeakSet so job functions
|
|
112
|
+
// can still be GC'd even after we've warned about them.
|
|
113
|
+
const warnedFunctions = new WeakSet();
|
|
114
|
+
function warnJobFunctionDeprecated(fn, jobType) {
|
|
115
|
+
if (warnedFunctions.has(fn))
|
|
116
|
+
return;
|
|
117
|
+
warnedFunctions.add(fn);
|
|
118
|
+
process.emitWarning("Job functions are deprecated and will removed in v1.0. " +
|
|
119
|
+
`Migrate ${jobType} to ` +
|
|
120
|
+
`client.enqueue({type: '${jobType}', queue: '...', payload: ...}).`, {
|
|
121
|
+
type: "DeprecationWarning",
|
|
122
|
+
code: "ZIZQ_JOB_FUNCTIONS_DEPRECATED",
|
|
123
|
+
});
|
|
124
|
+
}
|
package/dist/handler.js
CHANGED
|
@@ -25,6 +25,7 @@ import { Router } from "./router.js";
|
|
|
25
25
|
* functions resolve to the same type.
|
|
26
26
|
*/
|
|
27
27
|
export function buildHandler(jobs) {
|
|
28
|
+
warnBuildHandlerDeprecated();
|
|
28
29
|
const seen = new Set();
|
|
29
30
|
const router = new Router();
|
|
30
31
|
for (const fn of jobs) {
|
|
@@ -40,3 +41,16 @@ export function buildHandler(jobs) {
|
|
|
40
41
|
}
|
|
41
42
|
return router.build();
|
|
42
43
|
}
|
|
44
|
+
// --- Deprecation warnings ---
|
|
45
|
+
let warnedBuildHandler = false;
|
|
46
|
+
function warnBuildHandlerDeprecated() {
|
|
47
|
+
if (warnedBuildHandler)
|
|
48
|
+
return;
|
|
49
|
+
warnedBuildHandler = true;
|
|
50
|
+
process.emitWarning("buildHandler and job functions are deprecated and will be removed in " +
|
|
51
|
+
"v1.0. Use Router directly (client.enqueue({type: '...', ...}) + a " +
|
|
52
|
+
"Router-based worker) instead.", {
|
|
53
|
+
type: "DeprecationWarning",
|
|
54
|
+
code: "ZIZQ_JOB_FUNCTIONS_DEPRECATED",
|
|
55
|
+
});
|
|
56
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,3 +14,7 @@ export { enqueue, enqueueBulk } from "./enqueue.ts";
|
|
|
14
14
|
export { CronHandle, CronEntryHandle } from "./cron.ts";
|
|
15
15
|
export type { CronEntryDefinition, RegisterCronOptions } from "./cron.ts";
|
|
16
16
|
export { uniqueKey } from "./unique-key.ts";
|
|
17
|
+
export { payloadHasher } from "./payload-hasher.ts";
|
|
18
|
+
export type { PayloadHasherOptions } from "./payload-hasher.ts";
|
|
19
|
+
export { batchConfig } from "./batch-config.ts";
|
|
20
|
+
export type { BatchConfigOptions } from "./batch-config.ts";
|
package/dist/index.js
CHANGED
|
@@ -8,3 +8,5 @@ export { Lazy, ErrorQuery, JobQuery } from "./query.js";
|
|
|
8
8
|
export { enqueue, enqueueBulk } from "./enqueue.js";
|
|
9
9
|
export { CronHandle, CronEntryHandle } from "./cron.js";
|
|
10
10
|
export { uniqueKey } from "./unique-key.js";
|
|
11
|
+
export { payloadHasher } from "./payload-hasher.js";
|
|
12
|
+
export { batchConfig } from "./batch-config.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { EnqueueInput } from "./types.ts";
|
|
2
|
+
/** Options for {@link payloadHasher}. */
|
|
3
|
+
export interface PayloadHasherOptions {
|
|
4
|
+
/**
|
|
5
|
+
* jq paths whose values participate in the hash. When multiple paths
|
|
6
|
+
* are supplied their sub-values are hashed together, keyed by path
|
|
7
|
+
* so ordering doesn't matter. Defaults to `['.']` (whole payload).
|
|
8
|
+
*
|
|
9
|
+
* Accepts a single path or an array. Cannot be combined with `except`.
|
|
10
|
+
*/
|
|
11
|
+
only?: string | string[];
|
|
12
|
+
/**
|
|
13
|
+
* jq paths whose values are excluded from the hash. The payload is
|
|
14
|
+
* deep-cloned and each path is deleted before hashing. Missing paths
|
|
15
|
+
* are silently ignored.
|
|
16
|
+
*
|
|
17
|
+
* Accepts a single path or an array. Cannot be combined with `only`.
|
|
18
|
+
*/
|
|
19
|
+
except?: string | string[];
|
|
20
|
+
/**
|
|
21
|
+
* When `true` (default), the returned key is prefixed with the job
|
|
22
|
+
* type and a `:`. When `false`, only the raw hex digest is returned.
|
|
23
|
+
*/
|
|
24
|
+
prefix?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a function that hashes some or all of an enqueue's payload
|
|
28
|
+
* into a stable key string.
|
|
29
|
+
*
|
|
30
|
+
* The returned function takes the whole `EnqueueInput` so it can read
|
|
31
|
+
* both the payload (for hashing) and the type (for the optional
|
|
32
|
+
* prefix). Suitable for assignment to `uniqueKey` or `batch.key` on
|
|
33
|
+
* `client.enqueue({...})` inputs.
|
|
34
|
+
*
|
|
35
|
+
* @example Full-payload hash with type prefix
|
|
36
|
+
* ```ts
|
|
37
|
+
* uniqueKey: payloadHasher()
|
|
38
|
+
* // => "sendEmail:<sha256>"
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example Batch by tenant only
|
|
42
|
+
* ```ts
|
|
43
|
+
* batch: {
|
|
44
|
+
* key: payloadHasher({ except: '.notifications' }),
|
|
45
|
+
* when: "...",
|
|
46
|
+
* fold: "...",
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function payloadHasher(options?: PayloadHasherOptions): (input: EnqueueInput) => string;
|