@zizq-labs/zizq 0.3.1 → 0.3.2
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 +29 -0
- package/dist/client.js +30 -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
|
}
|
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
|
}
|