sabcom 0.1.140 → 0.1.141
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 +108 -264
- package/build/index.cjs +526 -193
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +9 -176
- package/build/index.js +521 -161
- package/build/index.js.map +1 -1
- package/package.json +7 -3
- package/src/index.ts +614 -305
package/README.md
CHANGED
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
[![Coverage Status][codecov-image]][codecov-url]
|
|
7
7
|
[![Snyk][snyk-image]][snyk-url]
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
High-performance inter-thread communication using `SharedArrayBuffer` with atomic operations. Bidirectional channels with synchronous and asynchronous APIs. Faster than `postMessage` structured clone and transfer.
|
|
10
10
|
|
|
11
11
|
## What sabcom Does
|
|
12
12
|
|
|
13
|
-
sabcom is a **protocol
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
13
|
+
sabcom is a **ring-buffer protocol** over SharedArrayBuffer. It handles:
|
|
14
|
+
- Bidirectional communication on a single SharedArrayBuffer
|
|
15
|
+
- Synchronization between endpoints via Atomics
|
|
16
|
+
- Segmented transfer for payloads larger than the ring capacity
|
|
17
|
+
- Timeout detection and close semantics
|
|
17
18
|
|
|
18
19
|
sabcom does **NOT**:
|
|
19
20
|
- Create worker threads (you create them with `worker_threads` or `new Worker()`)
|
|
@@ -22,13 +23,13 @@ sabcom does **NOT**:
|
|
|
22
23
|
|
|
23
24
|
## Features
|
|
24
25
|
|
|
25
|
-
- **
|
|
26
|
-
- **Async and Sync APIs**
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **Type-safe**
|
|
26
|
+
- **Bidirectional** - both endpoints can read and write on a single buffer
|
|
27
|
+
- **Async and Sync APIs** - `write`/`read` (non-blocking) and `writeSync`/`readSync` (blocking)
|
|
28
|
+
- **Zero-copy reads** - single-segment messages return a view into the SharedArrayBuffer
|
|
29
|
+
- **Segmented transfer** - payloads larger than the ring are split automatically
|
|
30
|
+
- **Close and reuse** - channels can be closed and buffers reused
|
|
31
|
+
- **Configurable timeouts** - per-operation timeout support
|
|
32
|
+
- **Type-safe** - full TypeScript support
|
|
32
33
|
|
|
33
34
|
## Installation
|
|
34
35
|
|
|
@@ -38,347 +39,190 @@ npm install sabcom
|
|
|
38
39
|
|
|
39
40
|
## Complete Example
|
|
40
41
|
|
|
41
|
-
Here is a complete example demonstrating communication between a main thread and a worker thread using Node.js `worker_threads`.
|
|
42
|
-
The `SharedArrayBuffer` is passed once via `workerData` so no `postMessage` is needed for data transfer.
|
|
43
|
-
|
|
44
42
|
**worker.ts**
|
|
45
43
|
```typescript
|
|
46
44
|
import { workerData } from 'worker_threads';
|
|
47
|
-
import {
|
|
45
|
+
import { open } from 'sabcom';
|
|
48
46
|
|
|
49
|
-
const
|
|
47
|
+
const ch = open(workerData as SharedArrayBuffer);
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const message = new TextDecoder().decode(receivedData);
|
|
55
|
-
console.log('Worker: Received message:', message);
|
|
49
|
+
const data = ch.readSync();
|
|
50
|
+
const message = new TextDecoder().decode(data);
|
|
51
|
+
console.log('Worker received:', message);
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
} catch (err) {
|
|
60
|
-
console.error('Worker: Error', err);
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
53
|
+
ch.writeSync(new TextEncoder().encode(message.toUpperCase()));
|
|
54
|
+
ch.close();
|
|
63
55
|
```
|
|
64
56
|
|
|
65
57
|
**main.ts**
|
|
66
58
|
```typescript
|
|
67
59
|
import { Worker } from 'worker_threads';
|
|
68
|
-
import {
|
|
69
|
-
import path from 'path';
|
|
70
|
-
|
|
71
|
-
async function main() {
|
|
72
|
-
// 1. Create a SharedArrayBuffer (must be multiple of 4)
|
|
73
|
-
// 4KB buffer
|
|
74
|
-
const buffer = new SharedArrayBuffer(4096);
|
|
60
|
+
import { createBuffer, open } from 'sabcom';
|
|
75
61
|
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
const buffer = createBuffer(65536);
|
|
63
|
+
const worker = new Worker('./worker.js', { workerData: buffer });
|
|
64
|
+
const ch = open(buffer);
|
|
78
65
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const data = new TextEncoder().encode(text);
|
|
66
|
+
const text = 'Hello from the main thread!';
|
|
67
|
+
await ch.write(new TextEncoder().encode(text));
|
|
82
68
|
|
|
83
|
-
|
|
69
|
+
const reply = await ch.read();
|
|
70
|
+
console.log('Reply:', new TextDecoder().decode(reply));
|
|
84
71
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
await write(data, buffer);
|
|
88
|
-
|
|
89
|
-
const reply = await read(buffer);
|
|
90
|
-
console.log('Main: Reply:', new TextDecoder().decode(reply));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
main().catch(console.error);
|
|
72
|
+
ch.close();
|
|
73
|
+
await worker.terminate();
|
|
94
74
|
```
|
|
95
75
|
|
|
96
|
-
##
|
|
97
|
-
|
|
98
|
-
### Async API
|
|
99
|
-
Best for non-blocking operations in the main thread or event-loop driven workers.
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
import { write, read } from 'sabcom';
|
|
76
|
+
## API
|
|
103
77
|
|
|
104
|
-
|
|
105
|
-
await write(data, buffer);
|
|
78
|
+
### `createBuffer(byteLength: number): SharedArrayBuffer`
|
|
106
79
|
|
|
107
|
-
|
|
108
|
-
const result = await read(buffer);
|
|
109
|
-
```
|
|
80
|
+
Creates and initializes a SharedArrayBuffer for use as a channel. `byteLength` must be a multiple of 4 and at least 4096.
|
|
110
81
|
|
|
111
|
-
###
|
|
112
|
-
Best for CPU-bound workers where blocking is acceptable or preferred.
|
|
82
|
+
### `open(buffer: SharedArrayBuffer): Channel`
|
|
113
83
|
|
|
114
|
-
|
|
115
|
-
import { writeSync, readSync } from 'sabcom';
|
|
84
|
+
Opens a channel on the buffer. Two endpoints can open the same buffer (one per thread). Returns the same handle if called twice in the same thread.
|
|
116
85
|
|
|
117
|
-
|
|
118
|
-
writeSync(data, buffer);
|
|
119
|
-
|
|
120
|
-
// Reader
|
|
121
|
-
const result = readSync(buffer);
|
|
122
|
-
```
|
|
86
|
+
### `reset(buffer: SharedArrayBuffer): void`
|
|
123
87
|
|
|
124
|
-
|
|
88
|
+
Resets a closed buffer so it can be reopened. Both endpoints must be closed first.
|
|
125
89
|
|
|
126
|
-
|
|
90
|
+
### Channel
|
|
127
91
|
|
|
128
92
|
```typescript
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
93
|
+
interface Channel {
|
|
94
|
+
write(data: Uint8Array, options?: Options): Promise<void>;
|
|
95
|
+
read(options?: Options): Promise<Uint8Array>;
|
|
96
|
+
writeSync(data: Uint8Array, options?: Options): void;
|
|
97
|
+
readSync(options?: Options): Uint8Array;
|
|
98
|
+
close(): void;
|
|
99
|
+
}
|
|
132
100
|
```
|
|
133
101
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
2. **Header Overhead**: The library uses a small portion of the buffer for a header. The buffer must be larger than `HEADER_SIZE` (exported).
|
|
138
|
-
3. **Performance Trade-off**:
|
|
139
|
-
* **Larger Buffer**: Fewer chunks, less synchronization overhead, faster for large data.
|
|
140
|
-
* **Smaller Buffer**: Less memory usage, more context switches/atomic operations.
|
|
141
|
-
* **Recommendation**: Start with 4KB - 64KB (`4096` - `65536`) depending on your average payload size.
|
|
142
|
-
|
|
143
|
-
## Multi-Worker Architecture
|
|
102
|
+
- `write` / `writeSync` - send data to the peer endpoint
|
|
103
|
+
- `read` / `readSync` - receive data from the peer endpoint
|
|
104
|
+
- `close` - close this endpoint and notify the peer
|
|
144
105
|
|
|
145
|
-
|
|
106
|
+
### Options
|
|
146
107
|
|
|
147
|
-
**main.ts**
|
|
148
108
|
```typescript
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
interface WorkerChannel {
|
|
153
|
-
worker: Worker;
|
|
154
|
-
buffer: SharedArrayBuffer;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function createWorker(id: number): Promise<WorkerChannel> {
|
|
158
|
-
const buffer = new SharedArrayBuffer(4096);
|
|
159
|
-
const worker = new Worker('./worker.js', {
|
|
160
|
-
workerData: { id, buffer }
|
|
161
|
-
});
|
|
162
|
-
return { worker, buffer };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function main() {
|
|
166
|
-
// Create multiple workers, each with its own buffer
|
|
167
|
-
const channels: WorkerChannel[] = await Promise.all([
|
|
168
|
-
createWorker(0),
|
|
169
|
-
createWorker(1),
|
|
170
|
-
createWorker(2),
|
|
171
|
-
]);
|
|
172
|
-
|
|
173
|
-
// Send data to all workers in parallel
|
|
174
|
-
const tasks = ['task-a', 'task-b', 'task-c'];
|
|
175
|
-
await Promise.all(
|
|
176
|
-
channels.map((ch, i) =>
|
|
177
|
-
write(new TextEncoder().encode(tasks[i]), ch.buffer)
|
|
178
|
-
)
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
// Read responses from all workers in parallel
|
|
182
|
-
const responses = await Promise.all(
|
|
183
|
-
channels.map(ch => read(ch.buffer))
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
responses.forEach((data, i) => {
|
|
187
|
-
console.log(`Worker ${i}: ${new TextDecoder().decode(data)}`);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// Cleanup
|
|
191
|
-
await Promise.all(channels.map(ch => ch.worker.terminate()));
|
|
109
|
+
interface Options {
|
|
110
|
+
timeout?: number; // milliseconds, default: 5000
|
|
192
111
|
}
|
|
193
|
-
|
|
194
|
-
main();
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
**worker.ts**
|
|
198
|
-
```typescript
|
|
199
|
-
import { workerData, parentPort } from 'worker_threads';
|
|
200
|
-
import { readSync, writeSync } from 'sabcom';
|
|
201
|
-
|
|
202
|
-
const { id, buffer } = workerData as { id: number; buffer: SharedArrayBuffer };
|
|
203
|
-
|
|
204
|
-
// Receive task from main
|
|
205
|
-
const task = new TextDecoder().decode(readSync(buffer));
|
|
206
|
-
console.log(`Worker ${id} received: ${task}`);
|
|
207
|
-
|
|
208
|
-
// Process and respond
|
|
209
|
-
const result = `${task.toUpperCase()}-done`;
|
|
210
|
-
writeSync(new TextEncoder().encode(result), buffer);
|
|
211
112
|
```
|
|
212
113
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
1. **One buffer per worker** - Each worker needs its own `SharedArrayBuffer`. A single buffer can only handle one reader-writer pair at a time.
|
|
216
|
-
|
|
217
|
-
2. **Buffer ownership** - Each buffer represents a bidirectional channel between main thread and one worker. Don't share a buffer between multiple workers.
|
|
114
|
+
## Zero-Copy Reads
|
|
218
115
|
|
|
219
|
-
|
|
116
|
+
When a message fits in a single ring segment and does not wrap the ring boundary, `readSync` and `read` return a `Uint8Array` view directly into the SharedArrayBuffer - no allocation or copy. The view is valid until the next method call on the same channel (`read`, `readSync`, `write`, `writeSync`, or `close`). After that, the underlying memory may be overwritten by the peer.
|
|
220
117
|
|
|
221
|
-
|
|
118
|
+
If you need to keep the data beyond the next call, copy it:
|
|
222
119
|
|
|
223
120
|
```typescript
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
acquire(size = 4096): SharedArrayBuffer {
|
|
228
|
-
return this.available.pop() ?? new SharedArrayBuffer(size);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
release(buffer: SharedArrayBuffer): void {
|
|
232
|
-
this.available.push(buffer);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
121
|
+
const view = ch.readSync();
|
|
122
|
+
const copy = view.slice(); // safe to hold indefinitely
|
|
235
123
|
```
|
|
236
124
|
|
|
237
|
-
|
|
125
|
+
Messages that wrap the ring boundary or span multiple segments return an owned copy automatically.
|
|
238
126
|
|
|
239
|
-
|
|
127
|
+
## Buffer Sizing
|
|
240
128
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
let result = gen.next();
|
|
246
|
-
|
|
247
|
-
while (!result.done) {
|
|
248
|
-
// Perform custom logic here (e.g. check for cancellation)
|
|
249
|
-
|
|
250
|
-
// Wait for the reader signal
|
|
251
|
-
const request = result.value;
|
|
252
|
-
const waitResult = Atomics.wait(request.target, request.index, request.value, request.timeout);
|
|
253
|
-
|
|
254
|
-
// Resume generator
|
|
255
|
-
result = gen.next(waitResult);
|
|
256
|
-
}
|
|
257
|
-
```
|
|
129
|
+
- **Minimum**: 4096 bytes
|
|
130
|
+
- **Multiple of 4** required
|
|
131
|
+
- **Larger buffers** reduce segmentation overhead for large payloads
|
|
132
|
+
- **Recommendation**: 4KB-64KB depending on average payload size
|
|
258
133
|
|
|
259
|
-
|
|
134
|
+
The buffer is split into two ring directions (one per endpoint). Each direction contains control metadata, message descriptors, and a data ring. Larger buffers allocate more data ring space.
|
|
260
135
|
|
|
261
|
-
|
|
262
|
-
Writes bytes to the buffer. Resolves when the reader has received all data.
|
|
136
|
+
## Multi-Worker Architecture
|
|
263
137
|
|
|
264
|
-
|
|
265
|
-
Waits for and reads bytes from the buffer. Resolves with the complete data.
|
|
138
|
+
Each worker needs its own SharedArrayBuffer. A single buffer supports exactly two endpoints.
|
|
266
139
|
|
|
267
|
-
|
|
268
|
-
|
|
140
|
+
```typescript
|
|
141
|
+
import { Worker } from 'worker_threads';
|
|
142
|
+
import { createBuffer, open } from 'sabcom';
|
|
269
143
|
|
|
270
|
-
|
|
271
|
-
|
|
144
|
+
async function spawnWorker(task: string) {
|
|
145
|
+
const buffer = createBuffer(65536);
|
|
146
|
+
const worker = new Worker('./worker.js', { workerData: buffer });
|
|
147
|
+
const ch = open(buffer);
|
|
272
148
|
|
|
273
|
-
|
|
149
|
+
ch.writeSync(new TextEncoder().encode(task));
|
|
150
|
+
const result = await ch.read();
|
|
274
151
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
4. **Reader** reads chunk, acknowledges -> `READY`.
|
|
280
|
-
5. Repeat 3-4 until done.
|
|
152
|
+
ch.close();
|
|
153
|
+
await worker.terminate();
|
|
154
|
+
return new TextDecoder().decode(result);
|
|
155
|
+
}
|
|
281
156
|
|
|
282
|
-
|
|
157
|
+
const results = await Promise.all([
|
|
158
|
+
spawnWorker('task-a'),
|
|
159
|
+
spawnWorker('task-b'),
|
|
160
|
+
spawnWorker('task-c'),
|
|
161
|
+
]);
|
|
162
|
+
```
|
|
283
163
|
|
|
284
164
|
## FAQ
|
|
285
165
|
|
|
286
|
-
### What is the minimum buffer size?
|
|
287
|
-
|
|
288
|
-
`HEADER_SIZE` is 16 bytes. Your buffer must be larger to have usable payload space:
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
import { HEADER_SIZE } from 'sabcom';
|
|
292
|
-
|
|
293
|
-
// Minimum: HEADER_SIZE + at least 1 byte for payload
|
|
294
|
-
// Practical minimum: 1024 bytes (1KB)
|
|
295
|
-
const buffer = new SharedArrayBuffer(1024);
|
|
296
|
-
```
|
|
297
|
-
|
|
298
166
|
### How do I send JSON or objects?
|
|
299
167
|
|
|
300
|
-
sabcom transfers raw bytes
|
|
168
|
+
sabcom transfers raw bytes. Serialize before sending:
|
|
301
169
|
|
|
302
170
|
```typescript
|
|
303
|
-
// Writer
|
|
304
171
|
const obj = { hello: 'world', count: 42 };
|
|
305
|
-
|
|
306
|
-
await write(new TextEncoder().encode(json), buffer);
|
|
172
|
+
ch.writeSync(new TextEncoder().encode(JSON.stringify(obj)));
|
|
307
173
|
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
const obj = JSON.parse(new TextDecoder().decode(data));
|
|
174
|
+
const data = ch.readSync();
|
|
175
|
+
const parsed = JSON.parse(new TextDecoder().decode(data));
|
|
311
176
|
```
|
|
312
177
|
|
|
313
178
|
### Does sabcom work in browsers?
|
|
314
179
|
|
|
315
|
-
Yes, with Web Workers.
|
|
180
|
+
Yes, with Web Workers. `SharedArrayBuffer` requires cross-origin isolation headers:
|
|
316
181
|
|
|
317
182
|
```
|
|
318
183
|
Cross-Origin-Opener-Policy: same-origin
|
|
319
184
|
Cross-Origin-Embedder-Policy: require-corp
|
|
320
185
|
```
|
|
321
186
|
|
|
322
|
-
###
|
|
187
|
+
### Can I reuse the buffer after closing?
|
|
323
188
|
|
|
324
|
-
|
|
325
|
-
try {
|
|
326
|
-
await write(data, buffer, { timeout: 5000 });
|
|
327
|
-
} catch (err) {
|
|
328
|
-
if (err.message.includes('timeout')) {
|
|
329
|
-
console.error('Reader did not respond in time');
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
### Can I cancel a transfer mid-way?
|
|
335
|
-
|
|
336
|
-
Use generators with `for...of` - breaking out automatically triggers cleanup:
|
|
189
|
+
Yes. After both endpoints close, call `reset(buffer)` and reopen:
|
|
337
190
|
|
|
338
191
|
```typescript
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (result === 'timed-out') break;
|
|
344
|
-
}
|
|
192
|
+
ch.close();
|
|
193
|
+
// ... peer also closes ...
|
|
194
|
+
reset(buffer);
|
|
195
|
+
const ch2 = open(buffer);
|
|
345
196
|
```
|
|
346
197
|
|
|
347
|
-
|
|
198
|
+
Or pass the buffer to new workers - `open` auto-resets buffers where both endpoints have closed.
|
|
348
199
|
|
|
349
|
-
|
|
200
|
+
### How do I handle errors?
|
|
350
201
|
|
|
351
202
|
```typescript
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
203
|
+
try {
|
|
204
|
+
ch.writeSync(data, { timeout: 5000 });
|
|
205
|
+
} catch (err) {
|
|
206
|
+
if (err.message.includes('timeout')) {
|
|
207
|
+
console.error('Peer did not respond in time');
|
|
208
|
+
} else if (err.message.includes('closed')) {
|
|
209
|
+
console.error('Peer closed the channel');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
361
212
|
```
|
|
362
213
|
|
|
363
214
|
## Development
|
|
364
215
|
|
|
365
216
|
```bash
|
|
366
|
-
# Install dependencies
|
|
367
217
|
pnpm install
|
|
368
|
-
|
|
369
|
-
# Build
|
|
370
218
|
pnpm build
|
|
371
|
-
|
|
372
|
-
# Run tests
|
|
373
219
|
pnpm test
|
|
374
|
-
|
|
375
|
-
# Lint
|
|
376
|
-
pnpm lint
|
|
220
|
+
pnpm bench
|
|
377
221
|
```
|
|
378
222
|
|
|
379
223
|
## License
|
|
380
224
|
|
|
381
|
-
Apache-2.0
|
|
225
|
+
Apache-2.0 (c) [Ivan Zakharchanka](https://linkedin.com/in/3axap4eHko)
|
|
382
226
|
|
|
383
227
|
[npm-url]: https://www.npmjs.com/package/sabcom
|
|
384
228
|
[downloads-image]: https://img.shields.io/npm/dw/sabcom.svg?maxAge=43200
|