bunqueue-client 0.1.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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/connection-types.d.ts +24 -0
- package/dist/connection-types.js +2 -0
- package/dist/connection.d.ts +44 -0
- package/dist/connection.js +229 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +25 -0
- package/dist/flow-types.d.ts +34 -0
- package/dist/flow-types.js +2 -0
- package/dist/flow.d.ts +31 -0
- package/dist/flow.js +186 -0
- package/dist/frame.d.ts +16 -0
- package/dist/frame.js +57 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +12 -0
- package/dist/job.d.ts +56 -0
- package/dist/job.js +151 -0
- package/dist/queue-admin.d.ts +63 -0
- package/dist/queue-admin.js +143 -0
- package/dist/queue-control.d.ts +44 -0
- package/dist/queue-control.js +85 -0
- package/dist/queue-query.d.ts +58 -0
- package/dist/queue-query.js +165 -0
- package/dist/queue.d.ts +50 -0
- package/dist/queue.js +77 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.js +50 -0
- package/dist/worker-base.d.ts +51 -0
- package/dist/worker-base.js +122 -0
- package/dist/worker-types.d.ts +27 -0
- package/dist/worker-types.js +5 -0
- package/dist/worker.d.ts +20 -0
- package/dist/worker.js +151 -0
- package/package.json +64 -0
- package/src/connection-types.ts +21 -0
- package/src/connection.ts +242 -0
- package/src/errors.ts +26 -0
- package/src/flow-types.ts +40 -0
- package/src/flow.ts +236 -0
- package/src/frame.ts +62 -0
- package/src/index.ts +41 -0
- package/src/job.ts +194 -0
- package/src/queue-admin.ts +213 -0
- package/src/queue-control.ts +145 -0
- package/src/queue-query.ts +220 -0
- package/src/queue.ts +112 -0
- package/src/types.ts +133 -0
- package/src/worker-base.ts +132 -0
- package/src/worker-types.ts +32 -0
- package/src/worker.ts +174 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Egeo Minotti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# bunqueue-client — TypeScript SDK (Node / Bun / Deno)
|
|
2
|
+
|
|
3
|
+
Cross-runtime TypeScript client for [bunqueue](https://github.com/egeominotti/bunqueue),
|
|
4
|
+
the high-performance job queue server. Talks the native TCP protocol
|
|
5
|
+
(msgpack, pipelined, port 6789) — feature parity with the built-in Bun client,
|
|
6
|
+
but runs on **any** modern JS/TS runtime.
|
|
7
|
+
|
|
8
|
+
The bunqueue **server** runs on Bun (binary or Docker). This SDK lets any
|
|
9
|
+
Node, Bun, or Deno service produce and consume jobs on it: *one queue, any
|
|
10
|
+
language, any runtime*.
|
|
11
|
+
|
|
12
|
+
## Runtime support
|
|
13
|
+
|
|
14
|
+
| Runtime | Status | Notes |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Node.js ≥ 20 | ✅ tested (58/58 e2e + 8/8 integration) | ESM; TS files run directly on Node ≥ 22 via `--experimental-strip-types` |
|
|
17
|
+
| Bun | ✅ tested (58/58 e2e + 8/8 integration) | works out of the box |
|
|
18
|
+
| Deno ≥ 2 | ✅ tested (58/58 e2e + 8/8 integration) | `node:` builtins + npm `msgpackr` |
|
|
19
|
+
| tsx / ts-node / vitest / jest | ✅ | they run on Node underneath |
|
|
20
|
+
| Cloudflare Workers | ✅ tested (11/11 e2e inside workerd) | needs `nodejs_compat` flag; no long-lived worker loops (request-scoped runtime) — consume via Cron Triggers / Durable Object alarms with batch pulls (covered by the suite); TLS requires a publicly trusted cert |
|
|
21
|
+
| Browser | ❌ | no raw TCP sockets — use the server's HTTP API instead |
|
|
22
|
+
|
|
23
|
+
The rule that makes this possible: the SDK uses **only `node:*` builtins**
|
|
24
|
+
(`net`, `tls`, `events`, `crypto`, `os`) — no `Bun.*` globals, no
|
|
25
|
+
`bun:`/`deno:` imports. Single runtime dependency: `msgpackr`.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install bunqueue-client # or: bun add / pnpm add / deno add npm:bunqueue-client
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Producer
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { Queue } from 'bunqueue-client';
|
|
37
|
+
|
|
38
|
+
const queue = new Queue('emails', { host: 'localhost', port: 6789 });
|
|
39
|
+
|
|
40
|
+
await queue.add('send', { to: 'user@example.com' }, { priority: 5, attempts: 3 });
|
|
41
|
+
await queue.addBulk([
|
|
42
|
+
{ name: 'send', data: { to: 'a@b.c' } },
|
|
43
|
+
{ name: 'send', data: { to: 'x@y.z' }, opts: { delay: 5000 } },
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const counts = await queue.getJobCounts();
|
|
47
|
+
queue.close();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Worker
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { Worker, UnrecoverableError } from 'bunqueue-client';
|
|
54
|
+
|
|
55
|
+
const worker = new Worker(
|
|
56
|
+
'emails',
|
|
57
|
+
async (job) => {
|
|
58
|
+
await job.updateProgress(50);
|
|
59
|
+
if (job.data.invalid) throw new UnrecoverableError('bad payload'); // no retries → DLQ
|
|
60
|
+
return { sent: true };
|
|
61
|
+
},
|
|
62
|
+
{ host: 'localhost', port: 6789, concurrency: 10 }
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
worker.on('completed', (job, result) => console.log(job.id, result));
|
|
66
|
+
worker.on('failed', (job, err) => console.error(job.id, err.message));
|
|
67
|
+
|
|
68
|
+
// later: await worker.close(); // graceful — waits for in-flight jobs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Retry, backoff, DLQ, stall detection, priorities and rate limiting all run
|
|
72
|
+
**server-side** — the worker only pulls, heartbeats and acks.
|
|
73
|
+
|
|
74
|
+
## Flows
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { FlowProducer } from 'bunqueue-client';
|
|
78
|
+
|
|
79
|
+
const flow = new FlowProducer({ host: 'localhost', port: 6789 });
|
|
80
|
+
|
|
81
|
+
// sequential chain: step1 → step2 → step3
|
|
82
|
+
await flow.addChain([
|
|
83
|
+
{ name: 'step1', queueName: 'pipeline' },
|
|
84
|
+
{ name: 'step2', queueName: 'pipeline' },
|
|
85
|
+
{ name: 'step3', queueName: 'pipeline' },
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
// fan-in: parallel jobs converge into a final job that reads their results
|
|
89
|
+
const { finalId } = await flow.addBulkThen(
|
|
90
|
+
[
|
|
91
|
+
{ name: 'part1', queueName: 'pipeline' },
|
|
92
|
+
{ name: 'part2', queueName: 'pipeline' },
|
|
93
|
+
],
|
|
94
|
+
{ name: 'merge', queueName: 'pipeline' }
|
|
95
|
+
);
|
|
96
|
+
// inside the 'merge' processor: await job.getChildrenValues()
|
|
97
|
+
|
|
98
|
+
// parent/child tree (children run BEFORE the parent)
|
|
99
|
+
const node = await flow.add({
|
|
100
|
+
name: 'assemble', queueName: 'orders',
|
|
101
|
+
children: [
|
|
102
|
+
{ name: 'reserve-stock', queueName: 'orders' },
|
|
103
|
+
{ name: 'charge-card', queueName: 'orders' },
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Schedulers (cron)
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
await queue.addCron('daily-report', '0 9 * * *', { type: 'report' });
|
|
112
|
+
await queue.every('health-ping', 30_000, { type: 'ping' });
|
|
113
|
+
await queue.removeJobScheduler('daily-report');
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## TLS + Auth
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const queue = new Queue('emails', {
|
|
120
|
+
host: 'queue.example.com',
|
|
121
|
+
port: 6789,
|
|
122
|
+
token: process.env.BUNQUEUE_TOKEN,
|
|
123
|
+
tls: { caFile: './ca.pem' }, // or `true` for system CAs
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Feature surface
|
|
128
|
+
|
|
129
|
+
- **Queue** — add/addBulk with full `JobOptions` (priority, delay, attempts,
|
|
130
|
+
backoff, ttl, timeout, jobId, deduplication, dependsOn, tags, groupId, lifo,
|
|
131
|
+
removeOnComplete/Fail, durable, repeat, debounce, …)
|
|
132
|
+
- **Query** — getJob, getJobByCustomId, getJobs + per-state helpers, state,
|
|
133
|
+
result, progress, waitForJob, counts (+ per priority), children values, logs
|
|
134
|
+
- **Control** — pause/resume/drain/obliterate/clean, remove, discard, promote,
|
|
135
|
+
retryJob/retryJobs, move to wait/delayed, change priority/delay, update
|
|
136
|
+
data, extend lock
|
|
137
|
+
- **DLQ** — getDlq, retryDlq, purgeDlq, DLQ config
|
|
138
|
+
- **Admin** — rate limit, global concurrency, stall config, webhooks,
|
|
139
|
+
stats/metrics/listQueues/getWorkers
|
|
140
|
+
- **Worker events** — `ready`, `active`, `completed`, `failed`, `progress`,
|
|
141
|
+
`drained`, `error`, `closed`; automatic lock heartbeats (jobs longer than
|
|
142
|
+
the lock TTL survive)
|
|
143
|
+
|
|
144
|
+
Not applicable outside Bun (by design): embedded mode, sandboxed workers,
|
|
145
|
+
`QueueEvents` (in-process subscription — use webhooks or the HTTP SSE/WS
|
|
146
|
+
endpoints instead).
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
bun install
|
|
152
|
+
bun run build # tsc → dist/
|
|
153
|
+
bun run check # biome check
|
|
154
|
+
|
|
155
|
+
# Test suites (each spawns a real bunqueue server from the repo root)
|
|
156
|
+
bun tests/integration.ts # smoke
|
|
157
|
+
bun tests/e2e.ts # full surface + edge cases + realistic load
|
|
158
|
+
node --experimental-strip-types tests/e2e.ts # same file on Node ≥22
|
|
159
|
+
deno run -A tests/e2e.ts # same file on Deno ≥2
|
|
160
|
+
bun run test:workers # full suite INSIDE workerd (Cloudflare Workers)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Style rules: Biome, max 250 lines per file, relative imports with explicit
|
|
164
|
+
`.js` extension (NodeNext resolution — required for Node ESM). See `CLAUDE.md`
|
|
165
|
+
for the full development guide and wire-protocol gotchas.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Connection option and message types. */
|
|
2
|
+
export type TlsOption = boolean | {
|
|
3
|
+
caFile?: string;
|
|
4
|
+
rejectUnauthorized?: boolean;
|
|
5
|
+
} | undefined;
|
|
6
|
+
export interface ConnectionOptions {
|
|
7
|
+
host?: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
token?: string;
|
|
10
|
+
tls?: TlsOption;
|
|
11
|
+
connectTimeoutMs?: number;
|
|
12
|
+
commandTimeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
export type Command = Record<string, unknown> & {
|
|
15
|
+
cmd: string;
|
|
16
|
+
};
|
|
17
|
+
export type Response = Record<string, unknown> & {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
};
|
|
20
|
+
export interface Pending {
|
|
21
|
+
resolve: (response: Response) => void;
|
|
22
|
+
reject: (error: Error) => void;
|
|
23
|
+
timer: ReturnType<typeof setTimeout>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TCP connection to a bunqueue server — cross-runtime (Node.js, Bun, Deno).
|
|
3
|
+
*
|
|
4
|
+
* Requests carry a `reqId` string; the server echoes it back, enabling
|
|
5
|
+
* pipelining (many in-flight commands per socket). Uses only `node:`
|
|
6
|
+
* builtins (net/tls), which Node, Bun and Deno all support.
|
|
7
|
+
*/
|
|
8
|
+
import type { Command, ConnectionOptions, Response, TlsOption } from './connection-types.js';
|
|
9
|
+
export type { Command, ConnectionOptions, Response, TlsOption } from './connection-types.js';
|
|
10
|
+
/** A single pipelined TCP connection to a bunqueue server. */
|
|
11
|
+
export declare class Connection {
|
|
12
|
+
readonly host: string;
|
|
13
|
+
readonly port: number;
|
|
14
|
+
readonly token: string | undefined;
|
|
15
|
+
readonly tls: TlsOption;
|
|
16
|
+
readonly connectTimeoutMs: number;
|
|
17
|
+
readonly commandTimeoutMs: number;
|
|
18
|
+
private socket;
|
|
19
|
+
private connected;
|
|
20
|
+
private closed;
|
|
21
|
+
private connecting;
|
|
22
|
+
private pending;
|
|
23
|
+
private reqCounter;
|
|
24
|
+
private parser;
|
|
25
|
+
constructor(options?: ConnectionOptions);
|
|
26
|
+
get isConnected(): boolean;
|
|
27
|
+
/** Open the socket (and authenticate) if not already connected. */
|
|
28
|
+
connect(): Promise<void>;
|
|
29
|
+
private doConnect;
|
|
30
|
+
private openSocket;
|
|
31
|
+
/**
|
|
32
|
+
* Send a command and await its response. Rejects with CommandError when
|
|
33
|
+
* the server answers ok=false. Reconnects lazily if the link was lost.
|
|
34
|
+
*/
|
|
35
|
+
call(command: Command, timeoutMs?: number): Promise<Response>;
|
|
36
|
+
/** Ping the server; returns true when it answers pong. */
|
|
37
|
+
ping(): Promise<boolean>;
|
|
38
|
+
/** Protocol negotiation; returns server name/version/protocolVersion. */
|
|
39
|
+
hello(): Promise<Response>;
|
|
40
|
+
/** Close permanently; in-flight commands reject. */
|
|
41
|
+
close(): void;
|
|
42
|
+
private handleData;
|
|
43
|
+
private teardown;
|
|
44
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TCP connection to a bunqueue server — cross-runtime (Node.js, Bun, Deno).
|
|
3
|
+
*
|
|
4
|
+
* Requests carry a `reqId` string; the server echoes it back, enabling
|
|
5
|
+
* pipelining (many in-flight commands per socket). Uses only `node:`
|
|
6
|
+
* builtins (net/tls), which Node, Bun and Deno all support.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { connect as netConnect } from 'node:net';
|
|
10
|
+
import { connect as tlsConnect } from 'node:tls';
|
|
11
|
+
import { pack, unpack } from 'msgpackr';
|
|
12
|
+
import { AuthError, CommandError, CommandTimeoutError, ConnectionClosedError } from './errors.js';
|
|
13
|
+
import { compact, FrameParser, frame, PROTOCOL_VERSION } from './frame.js';
|
|
14
|
+
/** A single pipelined TCP connection to a bunqueue server. */
|
|
15
|
+
export class Connection {
|
|
16
|
+
host;
|
|
17
|
+
port;
|
|
18
|
+
token;
|
|
19
|
+
tls;
|
|
20
|
+
connectTimeoutMs;
|
|
21
|
+
commandTimeoutMs;
|
|
22
|
+
socket = null;
|
|
23
|
+
connected = false;
|
|
24
|
+
closed = false;
|
|
25
|
+
connecting = null;
|
|
26
|
+
pending = new Map();
|
|
27
|
+
reqCounter = 0;
|
|
28
|
+
parser = new FrameParser();
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
this.host = options.host ?? 'localhost';
|
|
31
|
+
this.port = options.port ?? 6789;
|
|
32
|
+
this.token = options.token;
|
|
33
|
+
this.tls = options.tls;
|
|
34
|
+
this.connectTimeoutMs = options.connectTimeoutMs ?? 5000;
|
|
35
|
+
this.commandTimeoutMs = options.commandTimeoutMs ?? 10_000;
|
|
36
|
+
}
|
|
37
|
+
get isConnected() {
|
|
38
|
+
return this.connected;
|
|
39
|
+
}
|
|
40
|
+
/** Open the socket (and authenticate) if not already connected. */
|
|
41
|
+
async connect() {
|
|
42
|
+
if (this.connected)
|
|
43
|
+
return;
|
|
44
|
+
if (this.closed)
|
|
45
|
+
throw new ConnectionClosedError('connection closed by client');
|
|
46
|
+
if (this.connecting)
|
|
47
|
+
return this.connecting;
|
|
48
|
+
this.connecting = this.doConnect().finally(() => {
|
|
49
|
+
this.connecting = null;
|
|
50
|
+
});
|
|
51
|
+
return this.connecting;
|
|
52
|
+
}
|
|
53
|
+
async doConnect() {
|
|
54
|
+
const socket = await this.openSocket();
|
|
55
|
+
socket.setNoDelay(true);
|
|
56
|
+
this.parser.clear();
|
|
57
|
+
this.socket = socket;
|
|
58
|
+
socket.on('data', (chunk) => this.handleData(chunk));
|
|
59
|
+
socket.on('error', () => this.teardown());
|
|
60
|
+
socket.on('close', () => this.teardown());
|
|
61
|
+
this.connected = true;
|
|
62
|
+
if (this.token) {
|
|
63
|
+
try {
|
|
64
|
+
await this.call({ cmd: 'Auth', token: this.token });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
this.teardown();
|
|
68
|
+
if (err instanceof CommandError)
|
|
69
|
+
throw new AuthError(err.message);
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
openSocket() {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let settled = false;
|
|
77
|
+
const timer = setTimeout(() => {
|
|
78
|
+
if (settled)
|
|
79
|
+
return;
|
|
80
|
+
settled = true;
|
|
81
|
+
socket.destroy();
|
|
82
|
+
reject(new ConnectionClosedError(`connect timeout to ${this.host}:${this.port}`));
|
|
83
|
+
}, this.connectTimeoutMs);
|
|
84
|
+
const onError = (err) => {
|
|
85
|
+
if (settled)
|
|
86
|
+
return;
|
|
87
|
+
settled = true;
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
reject(new ConnectionClosedError(`connect failed: ${err.message}`));
|
|
90
|
+
};
|
|
91
|
+
const onReady = () => {
|
|
92
|
+
if (settled)
|
|
93
|
+
return;
|
|
94
|
+
settled = true;
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
socket.off('error', onError);
|
|
97
|
+
resolve(socket);
|
|
98
|
+
};
|
|
99
|
+
let socket;
|
|
100
|
+
if (this.tls) {
|
|
101
|
+
const tlsOpts = { host: this.host, port: this.port };
|
|
102
|
+
if (typeof this.tls === 'object') {
|
|
103
|
+
if (this.tls.caFile)
|
|
104
|
+
tlsOpts.ca = readFileSync(this.tls.caFile);
|
|
105
|
+
if (this.tls.rejectUnauthorized === false)
|
|
106
|
+
tlsOpts.rejectUnauthorized = false;
|
|
107
|
+
}
|
|
108
|
+
socket = tlsConnect(tlsOpts, onReady);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
socket = netConnect({ host: this.host, port: this.port }, onReady);
|
|
112
|
+
}
|
|
113
|
+
socket.on('error', onError);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Send a command and await its response. Rejects with CommandError when
|
|
118
|
+
* the server answers ok=false. Reconnects lazily if the link was lost.
|
|
119
|
+
*/
|
|
120
|
+
async call(command, timeoutMs) {
|
|
121
|
+
if (!this.connected)
|
|
122
|
+
await this.connect();
|
|
123
|
+
const socket = this.socket;
|
|
124
|
+
if (!socket)
|
|
125
|
+
throw new ConnectionClosedError('not connected');
|
|
126
|
+
this.reqCounter = (this.reqCounter + 1) & 0x7fffffff;
|
|
127
|
+
const reqId = String(this.reqCounter);
|
|
128
|
+
const payload = pack({ ...compact(command), reqId });
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
this.pending.delete(reqId);
|
|
132
|
+
reject(new CommandTimeoutError(`no response for ${command.cmd} within timeout`));
|
|
133
|
+
}, timeoutMs ?? this.commandTimeoutMs);
|
|
134
|
+
this.pending.set(reqId, {
|
|
135
|
+
resolve: (response) => {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
reject(new CommandError(String(response.error ?? 'unknown server error')));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
resolve(response);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
reject: (err) => {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
reject(err);
|
|
147
|
+
},
|
|
148
|
+
timer,
|
|
149
|
+
});
|
|
150
|
+
socket.write(frame(payload), (err) => {
|
|
151
|
+
if (err) {
|
|
152
|
+
const entry = this.pending.get(reqId);
|
|
153
|
+
this.pending.delete(reqId);
|
|
154
|
+
entry?.reject(new ConnectionClosedError(`send failed: ${err.message}`));
|
|
155
|
+
this.teardown();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/** Ping the server; returns true when it answers pong. */
|
|
161
|
+
async ping() {
|
|
162
|
+
try {
|
|
163
|
+
const response = await this.call({ cmd: 'Ping' });
|
|
164
|
+
const data = response.data;
|
|
165
|
+
return data?.pong === true;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Protocol negotiation; returns server name/version/protocolVersion. */
|
|
172
|
+
hello() {
|
|
173
|
+
return this.call({
|
|
174
|
+
cmd: 'Hello',
|
|
175
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
176
|
+
capabilities: ['pipelining'],
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Close permanently; in-flight commands reject. */
|
|
180
|
+
close() {
|
|
181
|
+
this.closed = true;
|
|
182
|
+
this.teardown();
|
|
183
|
+
}
|
|
184
|
+
handleData(chunk) {
|
|
185
|
+
let frames;
|
|
186
|
+
try {
|
|
187
|
+
frames = this.parser.addData(chunk);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
this.teardown();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
for (const framePayload of frames) {
|
|
194
|
+
let message;
|
|
195
|
+
try {
|
|
196
|
+
message = unpack(framePayload);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
continue; // skip unparseable frame; a desynced stream dies via socket error
|
|
200
|
+
}
|
|
201
|
+
if (typeof message !== 'object' || message === null)
|
|
202
|
+
continue;
|
|
203
|
+
const response = message;
|
|
204
|
+
const reqId = response.reqId;
|
|
205
|
+
if (reqId === undefined || reqId === null)
|
|
206
|
+
continue; // server-push unsupported
|
|
207
|
+
const entry = this.pending.get(String(reqId));
|
|
208
|
+
if (entry) {
|
|
209
|
+
this.pending.delete(String(reqId));
|
|
210
|
+
entry.resolve(response);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
teardown() {
|
|
215
|
+
this.connected = false;
|
|
216
|
+
const socket = this.socket;
|
|
217
|
+
this.socket = null;
|
|
218
|
+
if (socket) {
|
|
219
|
+
socket.removeAllListeners();
|
|
220
|
+
socket.destroy();
|
|
221
|
+
}
|
|
222
|
+
const pending = this.pending;
|
|
223
|
+
this.pending = new Map();
|
|
224
|
+
for (const entry of pending.values()) {
|
|
225
|
+
clearTimeout(entry.timer);
|
|
226
|
+
entry.reject(new ConnectionClosedError('connection lost'));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Exception hierarchy for the bunqueue cross-runtime SDK. */
|
|
2
|
+
export declare class BunqueueError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/** The TCP connection is closed or was lost mid-command. */
|
|
6
|
+
export declare class ConnectionClosedError extends BunqueueError {
|
|
7
|
+
}
|
|
8
|
+
/** No response received for a command within the timeout. */
|
|
9
|
+
export declare class CommandTimeoutError extends BunqueueError {
|
|
10
|
+
}
|
|
11
|
+
/** The server answered ok=false; message carries the server error. */
|
|
12
|
+
export declare class CommandError extends BunqueueError {
|
|
13
|
+
}
|
|
14
|
+
/** Authentication with the server failed. */
|
|
15
|
+
export declare class AuthError extends BunqueueError {
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Throw inside a Worker processor to fail the job terminally: skips all
|
|
19
|
+
* remaining retry attempts and sends the job straight to the DLQ.
|
|
20
|
+
*/
|
|
21
|
+
export declare class UnrecoverableError extends BunqueueError {
|
|
22
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Exception hierarchy for the bunqueue cross-runtime SDK. */
|
|
2
|
+
export class BunqueueError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** The TCP connection is closed or was lost mid-command. */
|
|
9
|
+
export class ConnectionClosedError extends BunqueueError {
|
|
10
|
+
}
|
|
11
|
+
/** No response received for a command within the timeout. */
|
|
12
|
+
export class CommandTimeoutError extends BunqueueError {
|
|
13
|
+
}
|
|
14
|
+
/** The server answered ok=false; message carries the server error. */
|
|
15
|
+
export class CommandError extends BunqueueError {
|
|
16
|
+
}
|
|
17
|
+
/** Authentication with the server failed. */
|
|
18
|
+
export class AuthError extends BunqueueError {
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Throw inside a Worker processor to fail the job terminally: skips all
|
|
22
|
+
* remaining retry attempts and sends the job straight to the DLQ.
|
|
23
|
+
*/
|
|
24
|
+
export class UnrecoverableError extends BunqueueError {
|
|
25
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** FlowProducer types. */
|
|
2
|
+
import type { Connection, TlsOption } from './connection.js';
|
|
3
|
+
import type { Job } from './job.js';
|
|
4
|
+
import type { JobOptions } from './types.js';
|
|
5
|
+
export interface FlowJob<T = unknown> {
|
|
6
|
+
name: string;
|
|
7
|
+
queueName: string;
|
|
8
|
+
data?: T;
|
|
9
|
+
opts?: JobOptions;
|
|
10
|
+
children?: FlowJob<T>[];
|
|
11
|
+
}
|
|
12
|
+
export interface JobNode<T = unknown> {
|
|
13
|
+
job: Job<T>;
|
|
14
|
+
children?: JobNode<T>[];
|
|
15
|
+
}
|
|
16
|
+
export interface FlowStep<T = unknown> {
|
|
17
|
+
name: string;
|
|
18
|
+
queueName: string;
|
|
19
|
+
data?: T;
|
|
20
|
+
opts?: JobOptions;
|
|
21
|
+
}
|
|
22
|
+
export interface FlowProducerOptions {
|
|
23
|
+
host?: string;
|
|
24
|
+
port?: number;
|
|
25
|
+
token?: string;
|
|
26
|
+
tls?: TlsOption;
|
|
27
|
+
connection?: Connection;
|
|
28
|
+
}
|
|
29
|
+
export interface GetFlowOptions {
|
|
30
|
+
id: string;
|
|
31
|
+
queueName?: string;
|
|
32
|
+
depth?: number;
|
|
33
|
+
maxChildren?: number;
|
|
34
|
+
}
|
package/dist/flow.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlowProducer: parent/children job trees, chains and fan-in flows.
|
|
3
|
+
* Mirrors the official client's TCP flow logic (bottom-up creation,
|
|
4
|
+
* UpdateParent fix-up, rollback via Cancel on failure).
|
|
5
|
+
*/
|
|
6
|
+
import { Connection } from './connection.js';
|
|
7
|
+
import type { FlowJob, FlowProducerOptions, FlowStep, GetFlowOptions, JobNode } from './flow-types.js';
|
|
8
|
+
export declare class FlowProducer {
|
|
9
|
+
readonly connection: Connection;
|
|
10
|
+
private readonly ownsConnection;
|
|
11
|
+
constructor(opts?: FlowProducerOptions);
|
|
12
|
+
/** Add a flow tree. Children are created (and processed) BEFORE their parent. */
|
|
13
|
+
add<T = unknown>(flow: FlowJob<T>): Promise<JobNode<T>>;
|
|
14
|
+
addBulk<T = unknown>(flows: FlowJob<T>[]): Promise<JobNode<T>[]>;
|
|
15
|
+
/** Fetch a flow tree starting from a job id (recursive over childrenIds). */
|
|
16
|
+
getFlow<T = unknown>(opts: GetFlowOptions): Promise<JobNode<T> | null>;
|
|
17
|
+
/** Add a sequential chain: step[0] → step[1] → ... via dependsOn. */
|
|
18
|
+
addChain<T = unknown>(steps: FlowStep<T>[]): Promise<{
|
|
19
|
+
jobIds: string[];
|
|
20
|
+
}>;
|
|
21
|
+
/** Parallel jobs converging into a final fan-in job. */
|
|
22
|
+
addBulkThen<T = unknown>(parallel: FlowStep<T>[], final: FlowStep<T>): Promise<{
|
|
23
|
+
parallelIds: string[];
|
|
24
|
+
finalId: string;
|
|
25
|
+
}>;
|
|
26
|
+
close(): void;
|
|
27
|
+
private addNode;
|
|
28
|
+
private pushWithParent;
|
|
29
|
+
private fetchNode;
|
|
30
|
+
private rollback;
|
|
31
|
+
}
|