@zizq-labs/zizq 0.3.1 → 0.4.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 +156 -22
- package/dist/client.d.ts +61 -0
- package/dist/client.js +72 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,10 +7,12 @@ API.
|
|
|
7
7
|
This is the official Zizq client library for Node.js, written in TypeScript.
|
|
8
8
|
|
|
9
9
|
[](https://github.com/zizq-labs/zizq-node/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.npmjs.com/package/@zizq-labs/zizq)
|
|
10
11
|
|
|
11
12
|
## Features
|
|
12
13
|
|
|
13
14
|
* Concurrent async-based worker
|
|
15
|
+
* Plain handler functions, or composable Job Functions with attached defaults
|
|
14
16
|
* Enqueue and process jobs from one language to another
|
|
15
17
|
* Arbitrary named queues
|
|
16
18
|
* Granular job priorities
|
|
@@ -29,7 +31,7 @@ This is the official Zizq client library for Node.js, written in TypeScript.
|
|
|
29
31
|
|
|
30
32
|
Install it with your package manager of choice:
|
|
31
33
|
|
|
32
|
-
```
|
|
34
|
+
```shell
|
|
33
35
|
npm install @zizq-labs/zizq
|
|
34
36
|
```
|
|
35
37
|
|
|
@@ -39,29 +41,123 @@ Or:
|
|
|
39
41
|
yarn add @zizq-labs/zizq
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
Node **22.19 or newer** is required. Client and server share version
|
|
45
|
+
numbers — keep the client's major/minor at or below the server's.
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
> There is also a composable function-based convenience wrapper available. Read
|
|
46
|
-
> the documentation on
|
|
47
|
-
> [Handler Functions](https://zizq.io/docs/clients/node/handlers.html) for more
|
|
48
|
-
> info
|
|
47
|
+
## Configuration
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
A `Client` instance is the configuration — there is no global object. The
|
|
50
|
+
defaults talk to a server at `http://localhost:7890`, which is fine for local
|
|
51
|
+
development. For anything else, pass the URL (and TLS, if needed) when
|
|
52
|
+
constructing the client:
|
|
51
53
|
|
|
52
54
|
```ts
|
|
55
|
+
import fs from "node:fs";
|
|
53
56
|
import { Client } from "@zizq-labs/zizq";
|
|
54
57
|
|
|
55
|
-
const client = new Client({
|
|
58
|
+
const client = new Client({
|
|
59
|
+
url: "https://zizq.your.network:7890",
|
|
60
|
+
tls: {
|
|
61
|
+
ca: fs.readFileSync("/path/to/server-ca-cert.pem"),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
56
65
|
|
|
57
|
-
|
|
66
|
+
For mutual TLS, add `cert` and `key` to the `tls` object (both PEM-encoded
|
|
67
|
+
strings or `Buffer`s).
|
|
68
|
+
|
|
69
|
+
> [!CAUTION]
|
|
70
|
+
> If your server is exposed directly to the internet, it should require
|
|
71
|
+
> mutual TLS — otherwise anybody can talk to it.
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
> [!TIP]
|
|
76
|
+
> This README is an overview. The
|
|
77
|
+
> [full documentation](https://zizq.io/docs/clients/node/) covers each
|
|
78
|
+
> feature in depth — handler patterns, job querying, unique jobs, and more.
|
|
79
|
+
|
|
80
|
+
### Enqueuing jobs
|
|
81
|
+
|
|
82
|
+
The simplest enqueue takes a `type`, `queue`, and `payload`. The Zizq server
|
|
83
|
+
returns the created job, with its `id`, `status`, `readyAt` and other
|
|
84
|
+
metadata:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const job = await client.enqueue({
|
|
58
88
|
type: "send_email",
|
|
59
89
|
queue: "emails",
|
|
60
|
-
payload: {
|
|
90
|
+
payload: { userId: 42, template: "welcome" },
|
|
91
|
+
});
|
|
92
|
+
job.id; // "03fu0wm75gxgmfyfplwvazhex"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Per-call options override server defaults — set `priority`, `readyAt` (to
|
|
96
|
+
schedule a job in the future), `retryLimit`, `backoff`, or `retention`
|
|
97
|
+
inline. `client.enqueueBulk(inputs)` submits many jobs atomically in a
|
|
98
|
+
single HTTP request, across queues and types:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
await client.enqueueBulk([
|
|
102
|
+
{ type: "send_email", queue: "emails", payload: { userId: 1 } },
|
|
103
|
+
{ type: "send_email", queue: "emails", payload: { userId: 2 } },
|
|
104
|
+
]);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Defining handlers
|
|
108
|
+
|
|
109
|
+
A handler is an `async` function that accepts a `job` and either resolves
|
|
110
|
+
(the worker acks it as successful) or throws (the worker reports a failure
|
|
111
|
+
and the server retries per the backoff policy). The simplest version is a
|
|
112
|
+
`switch` on `job.type`:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
async function handler(job) {
|
|
116
|
+
switch (job.type) {
|
|
117
|
+
case "send_email":
|
|
118
|
+
return sendEmail(job.payload);
|
|
119
|
+
case "generate_report":
|
|
120
|
+
return generateReport(job.payload);
|
|
121
|
+
default:
|
|
122
|
+
throw new Error(`unexpected job type: ${job.type}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
For a more composable style, the client ships `buildHandler` — pass in your
|
|
128
|
+
job functions and you get back a handler that dispatches on the function
|
|
129
|
+
name. Each function can also carry `zizqOptions` with its own defaults
|
|
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" },
|
|
61
153
|
});
|
|
62
154
|
```
|
|
63
155
|
|
|
64
|
-
|
|
156
|
+
### Running a worker
|
|
157
|
+
|
|
158
|
+
A `Worker` streams jobs from the server, dispatches them through your
|
|
159
|
+
handler with bounded concurrency, batches acks, and reconnects on transient
|
|
160
|
+
failures. `worker.run()` blocks until the worker stops:
|
|
65
161
|
|
|
66
162
|
```ts
|
|
67
163
|
import { Client, Worker } from "@zizq-labs/zizq";
|
|
@@ -71,16 +167,8 @@ const client = new Client({ url: "http://localhost:7890" });
|
|
|
71
167
|
const worker = new Worker({
|
|
72
168
|
client,
|
|
73
169
|
concurrency: 25,
|
|
74
|
-
queues: ["emails"],
|
|
75
|
-
handler
|
|
76
|
-
switch (job.type) {
|
|
77
|
-
case "send_email":
|
|
78
|
-
console.log(`sending email using payload ${JSON.stringify(job.payload)}`);
|
|
79
|
-
break;
|
|
80
|
-
default:
|
|
81
|
-
throw new Error(`unknown job type: ${job.type}`);
|
|
82
|
-
}
|
|
83
|
-
},
|
|
170
|
+
queues: ["emails", "payments"],
|
|
171
|
+
handler,
|
|
84
172
|
});
|
|
85
173
|
|
|
86
174
|
process.on("SIGINT", () => worker.stop());
|
|
@@ -89,6 +177,48 @@ process.on("SIGTERM", () => worker.stop());
|
|
|
89
177
|
await worker.run();
|
|
90
178
|
```
|
|
91
179
|
|
|
180
|
+
`worker.stop()` waits for in-flight handlers to settle and flushes pending
|
|
181
|
+
acks before returning. To put a deadline on shutdown, schedule
|
|
182
|
+
`worker.kill()` after a timeout — any handlers still running continue to
|
|
183
|
+
completion (Node can't cancel promises), but their acks are not flushed, so
|
|
184
|
+
the server re-dispatches those jobs to another worker. No jobs are lost.
|
|
185
|
+
|
|
186
|
+
### Recurring jobs (cron)
|
|
187
|
+
|
|
188
|
+
Define a cron schedule somewhere in your application startup.
|
|
189
|
+
Registrations are idempotent — every process can safely call the same
|
|
190
|
+
`register()`, and Zizq keeps the server-side schedule in sync by adding,
|
|
191
|
+
replacing, and removing entries as the definition changes. Cron requires
|
|
192
|
+
a Pro license on the server.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { sendDailyDigest } from "./handlers";
|
|
196
|
+
|
|
197
|
+
await client.cron("maintenance").register({
|
|
198
|
+
timezone: "Europe/London",
|
|
199
|
+
entries: [
|
|
200
|
+
{
|
|
201
|
+
name: "refresh_warehouse",
|
|
202
|
+
expression: "*/15 * * * *",
|
|
203
|
+
type: "refresh_warehouse",
|
|
204
|
+
queue: "data_warehouse",
|
|
205
|
+
payload: { incremental: true },
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "daily_digest",
|
|
209
|
+
expression: "0 9 * * *",
|
|
210
|
+
type: sendDailyDigest, // Job Functions are accepted directly
|
|
211
|
+
payload: {},
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Once defined, schedules can be inspected and managed via
|
|
218
|
+
`client.cron("maintenance")` — `get()` to read the current state,
|
|
219
|
+
`pause()`/`resume()` at the schedule or per-entry level, and `delete()`
|
|
220
|
+
to remove a schedule entirely.
|
|
221
|
+
|
|
92
222
|
## Resources
|
|
93
223
|
|
|
94
224
|
* [Node.js Client Docs](https://zizq.io/docs/clients/node/)
|
|
@@ -103,3 +233,7 @@ If you need help using Zizq,
|
|
|
103
233
|
[create an issue](https://github.com/zizq-labs/zizq-node/issues) on the
|
|
104
234
|
[zizq-node](https://github.com/zizq-labs/zizq-node) repo. Feedback is very
|
|
105
235
|
welcome.
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT — see [LICENSE](LICENSE).
|
package/dist/client.d.ts
CHANGED
|
@@ -82,6 +82,35 @@ export interface ClientOptions {
|
|
|
82
82
|
format?: Format;
|
|
83
83
|
/** TLS configuration for HTTPS connections. */
|
|
84
84
|
tls?: TlsOptions;
|
|
85
|
+
/**
|
|
86
|
+
* Connect timeout in milliseconds — deadline for the TCP/TLS
|
|
87
|
+
* handshake. Applies to every pool.
|
|
88
|
+
*
|
|
89
|
+
* Default: 10000 (10s).
|
|
90
|
+
*/
|
|
91
|
+
connectTimeout?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Per-read timeout in milliseconds for API traffic (enqueue,
|
|
94
|
+
* queries, mutations — anything that isn't the long-lived
|
|
95
|
+
* `take()` stream). Bounds the time between consecutive bytes
|
|
96
|
+
* received from the server.
|
|
97
|
+
*
|
|
98
|
+
* Default: 30000 (30s).
|
|
99
|
+
*/
|
|
100
|
+
readTimeout?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Per-read timeout in milliseconds for the long-lived
|
|
103
|
+
* `take()` stream consumed by {@link Worker}. The server's
|
|
104
|
+
* heartbeats (default ~3s) reset the clock on each frame, so this
|
|
105
|
+
* only fires when the connection has actually gone silent.
|
|
106
|
+
* Triggers a Worker reconnect via the standard error path.
|
|
107
|
+
*
|
|
108
|
+
* Should be comfortably above the server's heartbeat interval to
|
|
109
|
+
* avoid false-positive disconnects.
|
|
110
|
+
*
|
|
111
|
+
* Default: 30000 (30s).
|
|
112
|
+
*/
|
|
113
|
+
streamIdleTimeout?: number;
|
|
85
114
|
/** @internal For testing — override the HTTP dispatcher. */
|
|
86
115
|
dispatcher?: Dispatcher;
|
|
87
116
|
}
|
|
@@ -280,6 +309,27 @@ export declare class Client {
|
|
|
280
309
|
* ```
|
|
281
310
|
*/
|
|
282
311
|
deleteAllJobs(options?: DeleteAllJobsOptions): Promise<number>;
|
|
312
|
+
/**
|
|
313
|
+
* Wipe *every* cron group and *every* job on the server.
|
|
314
|
+
*
|
|
315
|
+
* Equivalent to calling {@link deleteAllCrons} followed by
|
|
316
|
+
* {@link deleteAllJobs} (no filter), but in a single request. Useful
|
|
317
|
+
* primarily as a setup/teardown step in tests where you want a
|
|
318
|
+
* known-empty server between scenarios.
|
|
319
|
+
*
|
|
320
|
+
* **Destructive.** No filters, no escape hatch, no confirmation —
|
|
321
|
+
* the server-side operation simply returns once everything is gone.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* await client.reset();
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
reset(): Promise<void>;
|
|
329
|
+
/**
|
|
330
|
+
* Alias for {@link reset}.
|
|
331
|
+
*/
|
|
332
|
+
eraseAllData(): Promise<void>;
|
|
283
333
|
/**
|
|
284
334
|
* Count jobs matching the given filters.
|
|
285
335
|
*
|
|
@@ -493,6 +543,17 @@ export declare class Client {
|
|
|
493
543
|
* @throws {NotFoundError} If the cron group is not found.
|
|
494
544
|
*/
|
|
495
545
|
deleteCronGroup(name: string): Promise<void>;
|
|
546
|
+
/**
|
|
547
|
+
* Delete every cron group on the server in a single call.
|
|
548
|
+
*
|
|
549
|
+
* Returns the number of cron groups removed.
|
|
550
|
+
*
|
|
551
|
+
* **Destructive.** This deletes *every cron group on the server*. For
|
|
552
|
+
* granular deletes, use {@link deleteCronGroup} with a specific name.
|
|
553
|
+
*
|
|
554
|
+
* Requires a Pro license on the server.
|
|
555
|
+
*/
|
|
556
|
+
deleteAllCrons(): Promise<number>;
|
|
496
557
|
/**
|
|
497
558
|
* Fetch a single cron entry within a group.
|
|
498
559
|
*
|
package/dist/client.js
CHANGED
|
@@ -151,29 +151,54 @@ export class Client {
|
|
|
151
151
|
this.contentType = CONTENT_TYPES[this.format];
|
|
152
152
|
this.accept = CONTENT_TYPES[this.format];
|
|
153
153
|
this.streamAccept = STREAM_ACCEPT[this.format];
|
|
154
|
+
const connectTimeout = options.connectTimeout ?? 10_000;
|
|
155
|
+
const readTimeout = options.readTimeout ?? 30_000;
|
|
156
|
+
const streamIdleTimeout = options.streamIdleTimeout ?? 30_000;
|
|
154
157
|
if (options.dispatcher) {
|
|
155
158
|
// Testing: use the same dispatcher for both.
|
|
156
159
|
this.http = options.dispatcher;
|
|
157
160
|
this.streamHttp = options.dispatcher;
|
|
158
161
|
}
|
|
159
162
|
else {
|
|
160
|
-
const connectOpts =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const connectOpts = {
|
|
164
|
+
...(options.tls ? {
|
|
165
|
+
ca: options.tls.ca,
|
|
166
|
+
cert: options.tls.cert,
|
|
167
|
+
key: options.tls.key,
|
|
168
|
+
} : {}),
|
|
169
|
+
timeout: connectTimeout,
|
|
170
|
+
};
|
|
165
171
|
// HTTP/2 for request/response traffic (multiplexed acks, enqueues).
|
|
172
|
+
// `bodyTimeout` is per-chunk inactivity — reset on each byte —
|
|
173
|
+
// so it acts as the read timeout; `headersTimeout` caps
|
|
174
|
+
// "server accepted the request but didn't respond" cases.
|
|
166
175
|
this.http = new Pool(this.url, {
|
|
167
176
|
allowH2: true,
|
|
168
177
|
connect: connectOpts,
|
|
178
|
+
headersTimeout: readTimeout,
|
|
179
|
+
bodyTimeout: readTimeout,
|
|
169
180
|
});
|
|
170
181
|
// HTTP/1.1 for the long-lived take stream. HTTP/2 adds framing
|
|
171
182
|
// overhead and flow control with no multiplexing benefit on a
|
|
172
183
|
// single long-lived response, resulting in measurably lower
|
|
173
184
|
// throughput compared to HTTP/1.1 chunked transfer.
|
|
185
|
+
//
|
|
186
|
+
// `bodyTimeout` here uses `streamIdleTimeout` rather than the
|
|
187
|
+
// normal `readTimeout`: server heartbeats reset it on each frame,
|
|
188
|
+
// so this only fires when the connection has actually gone
|
|
189
|
+
// silent. The Worker's reconnect path handles the resulting
|
|
190
|
+
// BodyTimeoutError.
|
|
191
|
+
//
|
|
192
|
+
// `headersTimeout` stays on `readTimeout`: the response headers
|
|
193
|
+
// are a one-shot request/response pair like any other API call,
|
|
194
|
+
// only the body streams. Falling through to undici's 5-minute
|
|
195
|
+
// default would leave a server stuck pre-headers blocking the
|
|
196
|
+
// worker far longer than the user-configured timeout would suggest.
|
|
174
197
|
this.streamHttp = new Pool(this.url, {
|
|
175
198
|
allowH2: false,
|
|
176
199
|
connect: connectOpts,
|
|
200
|
+
headersTimeout: readTimeout,
|
|
201
|
+
bodyTimeout: streamIdleTimeout,
|
|
177
202
|
});
|
|
178
203
|
}
|
|
179
204
|
}
|
|
@@ -342,6 +367,34 @@ export class Client {
|
|
|
342
367
|
const data = await this.handleResponse(await this.request("DELETE", path));
|
|
343
368
|
return data.deleted;
|
|
344
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Wipe *every* cron group and *every* job on the server.
|
|
372
|
+
*
|
|
373
|
+
* Equivalent to calling {@link deleteAllCrons} followed by
|
|
374
|
+
* {@link deleteAllJobs} (no filter), but in a single request. Useful
|
|
375
|
+
* primarily as a setup/teardown step in tests where you want a
|
|
376
|
+
* known-empty server between scenarios.
|
|
377
|
+
*
|
|
378
|
+
* **Destructive.** No filters, no escape hatch, no confirmation —
|
|
379
|
+
* the server-side operation simply returns once everything is gone.
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* ```ts
|
|
383
|
+
* await client.reset();
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
async reset() {
|
|
387
|
+
const res = await this.request("POST", "/reset");
|
|
388
|
+
if (res.statusCode !== 204) {
|
|
389
|
+
await this.throwOnError(res);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Alias for {@link reset}.
|
|
394
|
+
*/
|
|
395
|
+
async eraseAllData() {
|
|
396
|
+
return this.reset();
|
|
397
|
+
}
|
|
345
398
|
/**
|
|
346
399
|
* Count jobs matching the given filters.
|
|
347
400
|
*
|
|
@@ -645,6 +698,20 @@ export class Client {
|
|
|
645
698
|
await this.throwOnError(res);
|
|
646
699
|
}
|
|
647
700
|
}
|
|
701
|
+
/**
|
|
702
|
+
* Delete every cron group on the server in a single call.
|
|
703
|
+
*
|
|
704
|
+
* Returns the number of cron groups removed.
|
|
705
|
+
*
|
|
706
|
+
* **Destructive.** This deletes *every cron group on the server*. For
|
|
707
|
+
* granular deletes, use {@link deleteCronGroup} with a specific name.
|
|
708
|
+
*
|
|
709
|
+
* Requires a Pro license on the server.
|
|
710
|
+
*/
|
|
711
|
+
async deleteAllCrons() {
|
|
712
|
+
const data = await this.handleResponse(await this.request("DELETE", "/crons"));
|
|
713
|
+
return data.deleted;
|
|
714
|
+
}
|
|
648
715
|
/**
|
|
649
716
|
* Fetch a single cron entry within a group.
|
|
650
717
|
*
|