bunqueue-client 0.1.3 → 0.1.4
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 +1 -1
- package/README.md +77 -9
- package/dist/bunqueue/bunqueue.js +3 -1
- package/dist/bunqueue/rate-gate.d.ts +6 -0
- package/dist/bunqueue/rate-gate.js +13 -0
- package/package.json +2 -2
- package/src/bunqueue/bunqueue.ts +4 -1
- package/src/bunqueue/rate-gate.ts +14 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -8,11 +8,11 @@ The bunqueue server runs on Bun, distributed as a binary or a Docker image. This
|
|
|
8
8
|
|
|
9
9
|
| Runtime | Status | Notes |
|
|
10
10
|
|---|---|---|
|
|
11
|
-
| Node.js 20 or later | Supported,
|
|
12
|
-
| Bun | Supported,
|
|
13
|
-
| Deno 2 or later | Supported,
|
|
11
|
+
| Node.js 20 or later | Supported, 81/81 e2e and 8/8 integration tests | ESM. TypeScript files run directly on Node 22 or later via `--experimental-strip-types` |
|
|
12
|
+
| Bun | Supported, 81/81 e2e and 8/8 integration tests | No additional configuration required |
|
|
13
|
+
| Deno 2 or later | Supported, 81/81 e2e and 8/8 integration tests | Uses `node:` builtins and the npm `msgpackr` package |
|
|
14
14
|
| tsx, ts-node, vitest, jest | Supported | These environments execute on Node.js |
|
|
15
|
-
| Cloudflare Workers | Supported,
|
|
15
|
+
| Cloudflare Workers | Supported, 16/16 e2e tests inside workerd, including Simple Mode and the full API surface | Requires the `nodejs_compat` compatibility flag. The runtime is request scoped, so long lived worker loops are not available: consume in batches from Cron Triggers or Durable Object alarms, a pattern covered by the test suite. TLS connections require a publicly trusted certificate |
|
|
16
16
|
| Browser | Not supported | Raw TCP sockets are unavailable. Use the server HTTP API instead |
|
|
17
17
|
|
|
18
18
|
Portability is guaranteed by design: the client relies exclusively on `node:*` builtins (`net`, `tls`, `events`, `crypto`, `os`), uses no `Bun.*` globals and no runtime specific imports, and carries a single runtime dependency, `msgpackr`.
|
|
@@ -24,15 +24,37 @@ npm install bunqueue-client
|
|
|
24
24
|
# or: bun add bunqueue-client / pnpm add bunqueue-client / deno add npm:bunqueue-client
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
## Quick start
|
|
27
|
+
## Quick start, step by step
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Every step from zero to a production ready queue.
|
|
30
|
+
|
|
31
|
+
### Step 1. Run the bunqueue server
|
|
32
|
+
|
|
33
|
+
The server is the only component that requires [Bun](https://bun.sh). Pick one:
|
|
30
34
|
|
|
31
35
|
```bash
|
|
36
|
+
# Option A: one command, no install (requires Bun)
|
|
32
37
|
bunx bunqueue start
|
|
38
|
+
|
|
39
|
+
# Option B: Docker, with persistent data
|
|
40
|
+
docker run -d --name bunqueue \
|
|
41
|
+
-p 6789:6789 -p 6790:6790 \
|
|
42
|
+
-v bunqueue-data:/app/data \
|
|
43
|
+
ghcr.io/egeominotti/bunqueue:latest
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Port 6789 is the TCP protocol (what this client uses), port 6790 is the HTTP API with `/health`, `/metrics`, and dashboard endpoints.
|
|
47
|
+
|
|
48
|
+
### Step 2. Install the client
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install bunqueue-client
|
|
52
|
+
# or: bun add bunqueue-client / pnpm add bunqueue-client / deno add npm:bunqueue-client
|
|
33
53
|
```
|
|
34
54
|
|
|
35
|
-
Step
|
|
55
|
+
### Step 3. Add your first job and process it
|
|
56
|
+
|
|
57
|
+
Create `app.ts`, one file for the sake of the demo:
|
|
36
58
|
|
|
37
59
|
```typescript
|
|
38
60
|
import { Queue, Worker } from 'bunqueue-client';
|
|
@@ -51,7 +73,7 @@ await queue.add('greet', { name: 'world' });
|
|
|
51
73
|
queue.close();
|
|
52
74
|
```
|
|
53
75
|
|
|
54
|
-
|
|
76
|
+
Run it with the runtime you already use:
|
|
55
77
|
|
|
56
78
|
```bash
|
|
57
79
|
node --experimental-strip-types app.ts # Node 22 or later
|
|
@@ -66,7 +88,53 @@ processing { name: 'world' }
|
|
|
66
88
|
completed 019f40a5-... { greeted: 'world' }
|
|
67
89
|
```
|
|
68
90
|
|
|
69
|
-
|
|
91
|
+
Defaults are `host: 'localhost'` and `port: 6789`, so constructors need no options on a local setup.
|
|
92
|
+
|
|
93
|
+
### Step 4. Split producer and worker
|
|
94
|
+
|
|
95
|
+
In production the producer and the worker are separate services, often in different languages. The producer is typically an API endpoint:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// api-service: adds jobs, no processing
|
|
99
|
+
import { Queue } from 'bunqueue-client';
|
|
100
|
+
const queue = new Queue('emails', { host: 'queue.internal', port: 6789 });
|
|
101
|
+
await queue.add('welcome', { to: 'user@example.com' }, { attempts: 3 });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The worker is a long running process:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// worker-service: processes jobs, no HTTP
|
|
108
|
+
import { Worker } from 'bunqueue-client';
|
|
109
|
+
new Worker('emails', sendEmail, { host: 'queue.internal', port: 6789, concurrency: 10 });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The [Python client](https://github.com/egeominotti/bunqueue/tree/main/sdk/python) speaks the same protocol against the same queue, so the worker can be a Python service instead.
|
|
113
|
+
|
|
114
|
+
### Step 5. Observe and operate
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
await queue.getJobCounts(); // { waiting, active, completed, failed, delayed, ... }
|
|
118
|
+
await queue.getDlq(); // jobs that exhausted their retries
|
|
119
|
+
await queue.retryDlq(); // send them back to the queue
|
|
120
|
+
await queue.getWorkers(); // connected workers
|
|
121
|
+
await queue.getStats(); // throughput and totals
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Or hit the HTTP side: `curl http://localhost:6790/health`.
|
|
125
|
+
|
|
126
|
+
### Step 6. Go to production
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const queue = new Queue('emails', {
|
|
130
|
+
host: 'queue.example.com',
|
|
131
|
+
port: 6789,
|
|
132
|
+
token: process.env.BUNQUEUE_TOKEN, // server started with AUTH_TOKENS=...
|
|
133
|
+
tls: true, // or { caFile: './ca.pem' }
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Checklist: set `AUTH_TOKENS` on the server, enable TLS (`TLS_CERT_FILE`/`TLS_KEY_FILE`), mount a volume for the SQLite data path, monitor `/health` and `/metrics`, and size worker `concurrency` to your workload. Full guide: [bunqueue.dev/guide/deployment](https://bunqueue.dev/guide/deployment/).
|
|
70
138
|
|
|
71
139
|
## Producing jobs
|
|
72
140
|
|
|
@@ -105,8 +105,10 @@ export class Bunqueue {
|
|
|
105
105
|
}
|
|
106
106
|
// ------------------------------------------------- core processing pipeline
|
|
107
107
|
async processJob(job) {
|
|
108
|
-
if (this.rateGate)
|
|
108
|
+
if (this.rateGate) {
|
|
109
|
+
this.rateGate.prune(); // evict fully-expired groups (high-cardinality groupKey)
|
|
109
110
|
await this.rateGate.acquire(this.rateGate.groupFor(job.data));
|
|
111
|
+
}
|
|
110
112
|
// Circuit breaker check
|
|
111
113
|
if (this.cb?.isOpen()) {
|
|
112
114
|
throw new Error('Circuit breaker is open');
|
|
@@ -18,4 +18,10 @@ export declare class RateGate {
|
|
|
18
18
|
groupFor(data: unknown): string;
|
|
19
19
|
/** Wait until the group's window has room, then record the start. */
|
|
20
20
|
acquire(group: string): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Drop groups whose window is fully expired. Called on each acquire cycle
|
|
23
|
+
* boundary by the owner; without it a high-cardinality groupKey (e.g. one
|
|
24
|
+
* group per user id) grows the map forever.
|
|
25
|
+
*/
|
|
26
|
+
prune(): void;
|
|
21
27
|
}
|
|
@@ -45,4 +45,17 @@ export class RateGate {
|
|
|
45
45
|
await sleep(Math.max(oldest + this.duration - now, 10));
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Drop groups whose window is fully expired. Called on each acquire cycle
|
|
50
|
+
* boundary by the owner; without it a high-cardinality groupKey (e.g. one
|
|
51
|
+
* group per user id) grows the map forever.
|
|
52
|
+
*/
|
|
53
|
+
prune() {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
for (const [group, window] of this.windows) {
|
|
56
|
+
if (window.length === 0 || now - window[window.length - 1] >= this.duration) {
|
|
57
|
+
this.windows.delete(group);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
48
61
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunqueue-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Cross-runtime TypeScript client for the bunqueue job queue server — Node.js, Bun, Deno and Cloudflare Workers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"lint": "biome lint src tests",
|
|
30
30
|
"format": "biome format --write src tests",
|
|
31
31
|
"check": "biome check src tests",
|
|
32
|
-
"test:workers": "bun pm pack --destination tests/workers && cd tests/workers && mv bunqueue-client-*.tgz bunqueue-client.tgz && bun install && node run.mjs"
|
|
32
|
+
"test:workers": "bun pm pack --destination tests/workers && cd tests/workers && mv bunqueue-client-*.tgz bunqueue-client.tgz && rm -rf node_modules/bunqueue-client bun.lock && bun install --force && node run.mjs"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"queue",
|
package/src/bunqueue/bunqueue.ts
CHANGED
|
@@ -120,7 +120,10 @@ export class Bunqueue<T = unknown, R = unknown> {
|
|
|
120
120
|
// ------------------------------------------------- core processing pipeline
|
|
121
121
|
|
|
122
122
|
private async processJob(job: Job<T>): Promise<R> {
|
|
123
|
-
if (this.rateGate)
|
|
123
|
+
if (this.rateGate) {
|
|
124
|
+
this.rateGate.prune(); // evict fully-expired groups (high-cardinality groupKey)
|
|
125
|
+
await this.rateGate.acquire(this.rateGate.groupFor(job.data));
|
|
126
|
+
}
|
|
124
127
|
// Circuit breaker check
|
|
125
128
|
if (this.cb?.isOpen()) {
|
|
126
129
|
throw new Error('Circuit breaker is open');
|
|
@@ -50,4 +50,18 @@ export class RateGate {
|
|
|
50
50
|
await sleep(Math.max(oldest + this.duration - now, 10));
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Drop groups whose window is fully expired. Called on each acquire cycle
|
|
56
|
+
* boundary by the owner; without it a high-cardinality groupKey (e.g. one
|
|
57
|
+
* group per user id) grows the map forever.
|
|
58
|
+
*/
|
|
59
|
+
prune(): void {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
for (const [group, window] of this.windows) {
|
|
62
|
+
if (window.length === 0 || now - window[window.length - 1] >= this.duration) {
|
|
63
|
+
this.windows.delete(group);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
53
67
|
}
|