@zizq-labs/zizq 0.4.0 → 0.4.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 CHANGED
@@ -153,6 +153,34 @@ await client.enqueue({
153
153
  });
154
154
  ```
155
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
159
+ optional `fallback` catches anything unmatched:
160
+
161
+ ```ts
162
+ import { Router } from "@zizq-labs/zizq";
163
+
164
+ const router = new Router()
165
+ .route("send_email", async (payload, job) => {
166
+ await mailer.send(payload.to, payload.subject);
167
+ })
168
+ .route("generate_report", async (payload) => {
169
+ await reports.generate(payload.id);
170
+ })
171
+ .fallback(async (job) => {
172
+ console.warn(`Unhandled job type: ${job.type}`);
173
+ });
174
+
175
+ const worker = new Worker({ client, handler: router.build() });
176
+ ```
177
+
178
+ Routes overwrite on re-registration, which makes it natural to compose
179
+ routers — e.g. start from one that supplies defaults and selectively
180
+ override individual routes. If no route matches and no fallback is
181
+ registered, the router throws `UnknownJobTypeError`, which the worker
182
+ treats like any other handler failure (retries, eventually dead-lettered).
183
+
156
184
  ### Running a worker
157
185
 
158
186
  A `Worker` streams jobs from the server, dispatches them through your
package/dist/client.d.ts CHANGED
@@ -111,6 +111,20 @@ export interface ClientOptions {
111
111
  * Default: 30000 (30s).
112
112
  */
113
113
  streamIdleTimeout?: number;
114
+ /**
115
+ * Maximum number of concurrent HTTP/2 streams per connection that
116
+ * undici will request from the server via the
117
+ * `SETTINGS_MAX_CONCURRENT_STREAMS` frame. The effective ceiling
118
+ * is the minimum of this value and what the server advertises in
119
+ * its own SETTINGS — so raising this client-side only helps if
120
+ * the server is also configured to allow more streams.
121
+ *
122
+ * Applies to the request/response pool only. The long-lived
123
+ * `take()` stream uses HTTP/1.1 and is unaffected.
124
+ *
125
+ * Default: 1024.
126
+ */
127
+ maxConcurrentStreams?: number;
114
128
  /** @internal For testing — override the HTTP dispatcher. */
115
129
  dispatcher?: Dispatcher;
116
130
  }
package/dist/client.js CHANGED
@@ -168,12 +168,24 @@ export class Client {
168
168
  } : {}),
169
169
  timeout: connectTimeout,
170
170
  };
171
- // HTTP/2 for request/response traffic (multiplexed acks, enqueues).
171
+ // HTTP/2 for request/response traffic (multiplexed acks,
172
+ // enqueues, queries). `useH2c: true` lets undici speak HTTP/2
173
+ // over cleartext via prior knowledge, so http:// URLs get h2
174
+ // multiplexing too — not just https://. The Zizq server's hyper
175
+ // stack already accepts h2 prior-knowledge on any connection
176
+ // via its auto-protocol detector.
177
+ //
172
178
  // `bodyTimeout` is per-chunk inactivity — reset on each byte —
173
179
  // so it acts as the read timeout; `headersTimeout` caps
174
180
  // "server accepted the request but didn't respond" cases.
175
181
  this.http = new Pool(this.url, {
176
182
  allowH2: true,
183
+ // `useH2c` is supported at runtime in undici >=8 but isn't
184
+ // present in the public type definitions yet. Remove the
185
+ // expect-error once undici ships the type.
186
+ // @ts-expect-error -- runtime-supported, untyped
187
+ useH2c: true,
188
+ maxConcurrentStreams: options.maxConcurrentStreams ?? 1024,
177
189
  connect: connectOpts,
178
190
  headersTimeout: readTimeout,
179
191
  bodyTimeout: readTimeout,
package/dist/handler.js CHANGED
@@ -1,5 +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
+ import { Router } from "./router.js";
3
4
  /**
4
5
  * Build a `JobHandler` that dispatches incoming jobs to the matching
5
6
  * function from an array, looking up by `zizqOptions.type` (or `.name`).
@@ -24,22 +25,18 @@
24
25
  * functions resolve to the same type.
25
26
  */
26
27
  export function buildHandler(jobs) {
27
- const handlers = new Map();
28
+ const seen = new Set();
29
+ const router = new Router();
28
30
  for (const fn of jobs) {
29
31
  const typeName = fn.zizqOptions?.type ?? fn.name;
30
32
  if (!typeName) {
31
33
  throw new Error("Job function must have a name or zizqOptions.type");
32
34
  }
33
- if (handlers.has(typeName)) {
35
+ if (seen.has(typeName)) {
34
36
  throw new Error(`Duplicate job type registered: "${typeName}"`);
35
37
  }
36
- handlers.set(typeName, fn);
38
+ seen.add(typeName);
39
+ router.route(typeName, fn);
37
40
  }
38
- return async (job) => {
39
- const fn = handlers.get(job.type);
40
- if (!fn) {
41
- throw new Error(`No handler registered for job type: ${job.type}`);
42
- }
43
- await fn(job.payload, job);
44
- };
41
+ return router.build();
45
42
  }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ export { Worker } from "./worker.ts";
6
6
  export type { WorkerOptions, Logger, RequestRetryOptions } from "./worker.ts";
7
7
  export { buildHandler } from "./handler.ts";
8
8
  export type { JobFunction, JobHandler, ZizqOptions, EnqueueTransform, } from "./handler.ts";
9
+ export { Router, UnknownJobTypeError } from "./router.ts";
10
+ export type { RouteHandler } from "./router.ts";
9
11
  export { Lazy, ErrorQuery, JobQuery } from "./query.ts";
10
12
  export type { ErrorQueryOptions, JobQueryOptions } from "./query.ts";
11
13
  export { enqueue, enqueueBulk } from "./enqueue.ts";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  export { Client, Job, JobPage, ErrorRecord, ErrorPage, CronGroup, CronEntry, ZizqError, ConnectionError, ResponseError, ClientError, NotFoundError, ServerError, } from "./client.js";
4
4
  export { Worker } from "./worker.js";
5
5
  export { buildHandler } from "./handler.js";
6
+ export { Router, UnknownJobTypeError } from "./router.js";
6
7
  export { Lazy, ErrorQuery, JobQuery } from "./query.js";
7
8
  export { enqueue, enqueueBulk } from "./enqueue.js";
8
9
  export { CronHandle, CronEntryHandle } from "./cron.js";
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Type-based job dispatch.
3
+ *
4
+ * @module
5
+ */
6
+ import type { Job } from "./resources.ts";
7
+ import type { JobHandler } from "./handler.ts";
8
+ /**
9
+ * Handler for a specific job type.
10
+ *
11
+ * Receives the unwrapped `payload` (the JSON body of the job) along with the
12
+ * full `Job` for access to metadata like `id`, `type`, or `attempts`. Most
13
+ * handlers will only need the payload — JS lets you omit the second arg.
14
+ */
15
+ export type RouteHandler = (
16
+ /**
17
+ * The payload of the job to be performed, equivalent to `job.payload`.
18
+ */
19
+ payload: unknown,
20
+ /**
21
+ * The full `Job` resource received from the server.
22
+ */
23
+ job: Job) => Promise<void> | void;
24
+ /**
25
+ * Thrown when a job arrives with no registered route and no fallback.
26
+ *
27
+ * Caught by the worker's normal failure path: the job is nacked for retry,
28
+ * and eventually dead-lettered once the retry limit is hit.
29
+ */
30
+ export declare class UnknownJobTypeError extends Error {
31
+ /**
32
+ * The name of the unhandled job type.
33
+ */
34
+ readonly type: string;
35
+ constructor(type: string);
36
+ }
37
+ /**
38
+ * Dispatch jobs by `type` string to registered handler functions.
39
+ *
40
+ * Designed for cross-language workflows: payloads are plain JSON values
41
+ * (objects / arrays / strings / numbers), `type` is a string the producer
42
+ * agrees on with the consumer, and routes are registered explicitly.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { Worker, Router } from "@zizq-labs/zizq";
47
+ *
48
+ * const router = new Router()
49
+ * .route("send_email", async (payload, job) => {
50
+ * await mailer.send(payload.to);
51
+ * })
52
+ * .route("generate_report", async (payload) => {
53
+ * await reports.generate(payload.id);
54
+ * })
55
+ * .fallback(async (job) => {
56
+ * console.warn(`Unhandled job type: ${job.type}`);
57
+ * });
58
+ *
59
+ * const worker = new Worker({ client, handler: router.build() });
60
+ * ```
61
+ *
62
+ * If no route matches and no fallback is registered, the dispatcher throws
63
+ * `UnknownJobTypeError`, which the worker treats as a normal job failure
64
+ * (retried per the backoff policy, eventually dead-lettered).
65
+ */
66
+ export declare class Router {
67
+ private readonly routes;
68
+ private fallbackHandler;
69
+ /**
70
+ * Register `handler` for jobs whose `type` matches.
71
+ *
72
+ * Returns `this` for chaining.
73
+ *
74
+ * Re-registering a type replaces the previous handler. This supports
75
+ * builder-style composition — e.g. starting from a router that supplies
76
+ * defaults and selectively overriding individual routes.
77
+ */
78
+ route(type: string, handler: RouteHandler): this;
79
+ /**
80
+ * Register a fallback handler invoked when no route matches.
81
+ *
82
+ * Receives the full `Job` (not split into a payload/job pair). Since a
83
+ * fallback has the same signature as a `JobHandler`, you can delegate
84
+ * straight to another router's compiled handler:
85
+ *
86
+ * ```ts
87
+ * router.fallback(otherRouter.build());
88
+ * ```
89
+ *
90
+ * Returns `this` for chaining. Calling `fallback` again replaces the
91
+ * previous handler.
92
+ */
93
+ fallback(handler: JobHandler): this;
94
+ /**
95
+ * Compile this router into a `JobHandler` suitable for the worker.
96
+ *
97
+ * The returned function dispatches each incoming job: routes match by
98
+ * `job.type`, then the fallback (if any), otherwise throws
99
+ * `UnknownJobTypeError`.
100
+ */
101
+ build(): JobHandler;
102
+ }
package/dist/router.js ADDED
@@ -0,0 +1,106 @@
1
+ // Copyright (c) 2026 Chris Corbyn <chris@zizq.io>
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+ /**
4
+ * Thrown when a job arrives with no registered route and no fallback.
5
+ *
6
+ * Caught by the worker's normal failure path: the job is nacked for retry,
7
+ * and eventually dead-lettered once the retry limit is hit.
8
+ */
9
+ export class UnknownJobTypeError extends Error {
10
+ /**
11
+ * The name of the unhandled job type.
12
+ */
13
+ type;
14
+ constructor(type) {
15
+ super(`No handler registered for job type "${type}"`);
16
+ this.name = "UnknownJobTypeError";
17
+ this.type = type;
18
+ }
19
+ }
20
+ /**
21
+ * Dispatch jobs by `type` string to registered handler functions.
22
+ *
23
+ * Designed for cross-language workflows: payloads are plain JSON values
24
+ * (objects / arrays / strings / numbers), `type` is a string the producer
25
+ * agrees on with the consumer, and routes are registered explicitly.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { Worker, Router } from "@zizq-labs/zizq";
30
+ *
31
+ * const router = new Router()
32
+ * .route("send_email", async (payload, job) => {
33
+ * await mailer.send(payload.to);
34
+ * })
35
+ * .route("generate_report", async (payload) => {
36
+ * await reports.generate(payload.id);
37
+ * })
38
+ * .fallback(async (job) => {
39
+ * console.warn(`Unhandled job type: ${job.type}`);
40
+ * });
41
+ *
42
+ * const worker = new Worker({ client, handler: router.build() });
43
+ * ```
44
+ *
45
+ * If no route matches and no fallback is registered, the dispatcher throws
46
+ * `UnknownJobTypeError`, which the worker treats as a normal job failure
47
+ * (retried per the backoff policy, eventually dead-lettered).
48
+ */
49
+ export class Router {
50
+ routes = new Map();
51
+ fallbackHandler = null;
52
+ /**
53
+ * Register `handler` for jobs whose `type` matches.
54
+ *
55
+ * Returns `this` for chaining.
56
+ *
57
+ * Re-registering a type replaces the previous handler. This supports
58
+ * builder-style composition — e.g. starting from a router that supplies
59
+ * defaults and selectively overriding individual routes.
60
+ */
61
+ route(type, handler) {
62
+ this.routes.set(type, handler);
63
+ return this;
64
+ }
65
+ /**
66
+ * Register a fallback handler invoked when no route matches.
67
+ *
68
+ * Receives the full `Job` (not split into a payload/job pair). Since a
69
+ * fallback has the same signature as a `JobHandler`, you can delegate
70
+ * straight to another router's compiled handler:
71
+ *
72
+ * ```ts
73
+ * router.fallback(otherRouter.build());
74
+ * ```
75
+ *
76
+ * Returns `this` for chaining. Calling `fallback` again replaces the
77
+ * previous handler.
78
+ */
79
+ fallback(handler) {
80
+ this.fallbackHandler = handler;
81
+ return this;
82
+ }
83
+ /**
84
+ * Compile this router into a `JobHandler` suitable for the worker.
85
+ *
86
+ * The returned function dispatches each incoming job: routes match by
87
+ * `job.type`, then the fallback (if any), otherwise throws
88
+ * `UnknownJobTypeError`.
89
+ */
90
+ build() {
91
+ const routes = this.routes;
92
+ const fallback = this.fallbackHandler;
93
+ return async (job) => {
94
+ const handler = routes.get(job.type);
95
+ if (handler) {
96
+ await handler(job.payload, job);
97
+ return;
98
+ }
99
+ if (fallback) {
100
+ await fallback(job);
101
+ return;
102
+ }
103
+ throw new UnknownJobTypeError(job.type);
104
+ };
105
+ }
106
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zizq-labs/zizq",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Node.js client for the Zizq job queue server",
5
5
  "homepage": "https://zizq.io",
6
6
  "repository": {
@@ -46,6 +46,6 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^25.5.2",
49
- "typescript": "^5.8.0"
49
+ "typescript": "^6.0.3"
50
50
  }
51
51
  }