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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Egeo Minotti
3
+ Copyright (c) 2026 Egeo Minotti
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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, 72/72 e2e and 8/8 integration tests | ESM. TypeScript files run directly on Node 22 or later via `--experimental-strip-types` |
12
- | Bun | Supported, 72/72 e2e and 8/8 integration tests | No additional configuration required |
13
- | Deno 2 or later | Supported, 72/72 e2e and 8/8 integration tests | Uses `node:` builtins and the npm `msgpackr` package |
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, 11/11 e2e tests inside workerd | 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 |
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
- Sixty seconds from zero to a working queue. Step 1, start the server (requires [Bun](https://bun.sh), or use the Docker image):
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 2, create `app.ts`: add a job and process it, in the same file for the sake of the demo:
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
- Step 3, run it with the runtime you already use:
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
- That is the whole model: the server owns state, retries, and scheduling, your code only adds and processes. In production the producer and the worker are separate services, often in different languages: the [Python client](https://github.com/egeominotti/bunqueue/tree/main/sdk/python) speaks the same protocol against the same queue. Defaults are `host: 'localhost'`, `port: 6789`, so constructors need no options on a local setup.
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",
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",
@@ -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) await this.rateGate.acquire(this.rateGate.groupFor(job.data));
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
  }