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 CHANGED
@@ -6,14 +6,15 @@
6
6
  [![Coverage Status][codecov-image]][codecov-url]
7
7
  [![Snyk][snyk-image]][snyk-url]
8
8
 
9
- A TypeScript/Node.js library for high-performance inter-thread communication using `SharedArrayBuffer` with atomic operations. It provides both synchronous and asynchronous APIs to transfer byte data between threads (e.g., Main thread and Worker threads) without the overhead of structured cloning or memory copying.
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 layer** for SharedArrayBuffer. It handles:
14
- - Synchronization between reader and writer via Atomics
15
- - Chunking large data that exceeds buffer size
16
- - Timeout detection to prevent deadlocks
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
- - **Thread-safe communication** using `Atomics` for synchronization.
26
- - **Async and Sync APIs** to suit different architectural needs.
27
- - **Chunked Data Transfer** allows sending payloads larger than the buffer size.
28
- - **Byte-only API** for explicit serialization control.
29
- - **Configurable Timeouts** to prevent deadlocks.
30
- - **Generator-based Low-level API** for custom flow control implementations.
31
- - **Type-safe** with full TypeScript support.
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 { readSync, writeSync } from 'sabcom';
45
+ import { open } from 'sabcom';
48
46
 
49
- const buffer = workerData as SharedArrayBuffer;
47
+ const ch = open(workerData as SharedArrayBuffer);
50
48
 
51
- try {
52
- console.log('Worker: Waiting for data...');
53
- const receivedData = readSync(buffer);
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
- const reply = new TextEncoder().encode(message.toUpperCase());
58
- writeSync(reply, buffer);
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 { write, read } from 'sabcom';
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
- // 2. Start the worker and pass the buffer via workerData
77
- const worker = new Worker(path.resolve(__dirname, 'worker.ts'), { workerData: buffer });
62
+ const buffer = createBuffer(65536);
63
+ const worker = new Worker('./worker.js', { workerData: buffer });
64
+ const ch = open(buffer);
78
65
 
79
- // 3. Prepare data
80
- const text = "Hello from the main thread! ".repeat(500); // Larger than buffer
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
- console.log(`Main: Sending ${data.byteLength} bytes...`);
69
+ const reply = await ch.read();
70
+ console.log('Reply:', new TextDecoder().decode(reply));
84
71
 
85
- // 4. Write data to the shared buffer
86
- // The 'read' operation in the worker will pick this up.
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
- ## Usage
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
- // Writer
105
- await write(data, buffer);
78
+ ### `createBuffer(byteLength: number): SharedArrayBuffer`
106
79
 
107
- // Reader
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
- ### Sync API
112
- Best for CPU-bound workers where blocking is acceptable or preferred.
82
+ ### `open(buffer: SharedArrayBuffer): Channel`
113
83
 
114
- ```typescript
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
- // Writer
118
- writeSync(data, buffer);
119
-
120
- // Reader
121
- const result = readSync(buffer);
122
- ```
86
+ ### `reset(buffer: SharedArrayBuffer): void`
123
87
 
124
- ### Options
88
+ Resets a closed buffer so it can be reopened. Both endpoints must be closed first.
125
89
 
126
- All functions accept an optional options object:
90
+ ### Channel
127
91
 
128
92
  ```typescript
129
- await write(data, buffer, {
130
- timeout: 10000 // Timeout in milliseconds (default: 5000)
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
- ## Buffer Sizing & Requirements
135
-
136
- 1. **Multiple of 4**: The `byteLength` of the `SharedArrayBuffer` **must** be a multiple of 4 (e.g., 1024, 4096).
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
- When multiple workers need to communicate with the main thread simultaneously, create a separate `SharedArrayBuffer` for each worker. Each buffer is an independent communication channel.
106
+ ### Options
146
107
 
147
- **main.ts**
148
108
  ```typescript
149
- import { Worker } from 'worker_threads';
150
- import { write, read } from 'sabcom';
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
- ### Key Points for Multi-Worker Setup
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
- 3. **Parallel operations** - Use `Promise.all()` with async API (`write`/`read`) to communicate with multiple workers concurrently.
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
- 4. **Buffer pool pattern** - For dynamic worker counts, maintain a pool of buffers:
118
+ If you need to keep the data beyond the next call, copy it:
222
119
 
223
120
  ```typescript
224
- class BufferPool {
225
- private available: SharedArrayBuffer[] = [];
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
- ## Advanced: Generators
125
+ Messages that wrap the ring boundary or span multiple segments return an owned copy automatically.
238
126
 
239
- If you need fine-grained control over the transfer process (e.g., to implement a progress bar, cancellation, or custom scheduling), you can use the generator functions directly.
127
+ ## Buffer Sizing
240
128
 
241
- ```typescript
242
- import { writeGenerator } from 'sabcom';
243
-
244
- const gen = writeGenerator(data, buffer);
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
- ## API Reference
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
- ### `write(data: Uint8Array, buffer: SharedArrayBuffer, options?: Options): Promise<void>`
262
- Writes bytes to the buffer. Resolves when the reader has received all data.
136
+ ## Multi-Worker Architecture
263
137
 
264
- ### `read(buffer: SharedArrayBuffer, options?: Options): Promise<Uint8Array>`
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
- ### `writeSync(data: Uint8Array, buffer: SharedArrayBuffer, options?: Options): void`
268
- Synchronous version of `write`. Blocks until completion.
140
+ ```typescript
141
+ import { Worker } from 'worker_threads';
142
+ import { createBuffer, open } from 'sabcom';
269
143
 
270
- ### `readSync(buffer: SharedArrayBuffer, options?: Options): Uint8Array`
271
- Synchronous version of `read`. Blocks until data is received.
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
- ## Protocol Details
149
+ ch.writeSync(new TextEncoder().encode(task));
150
+ const result = await ch.read();
274
151
 
275
- The communication follows a strict handshake:
276
- 1. **Writer** acquires lock, writes metadata (total size, chunk count) -> `HANDSHAKE`.
277
- 2. **Reader** acknowledges -> `READY`.
278
- 3. **Writer** writes chunk -> `PAYLOAD`.
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
- *Note: The `SharedArrayBuffer` is reusable after a successful transfer.*
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 only. Serialize before sending:
168
+ sabcom transfers raw bytes. Serialize before sending:
301
169
 
302
170
  ```typescript
303
- // Writer
304
171
  const obj = { hello: 'world', count: 42 };
305
- const json = JSON.stringify(obj);
306
- await write(new TextEncoder().encode(json), buffer);
172
+ ch.writeSync(new TextEncoder().encode(JSON.stringify(obj)));
307
173
 
308
- // Reader
309
- const data = await read(buffer);
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. However, `SharedArrayBuffer` requires cross-origin isolation headers on your server:
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
- ### How do I handle errors?
187
+ ### Can I reuse the buffer after closing?
323
188
 
324
- ```typescript
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
- const gen = writeGenerator(data, buffer);
340
- for (const request of gen) {
341
- if (shouldCancel) break; // finally block resets buffer to READY
342
- const result = Atomics.wait(request.target, request.index, request.value, request.timeout);
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
- ### Can I reuse the buffer after a transfer?
198
+ Or pass the buffer to new workers - `open` auto-resets buffers where both endpoints have closed.
348
199
 
349
- Yes. After a successful transfer (or error), the buffer resets to `READY` state and can be used again:
200
+ ### How do I handle errors?
350
201
 
351
202
  ```typescript
352
- const buffer = new SharedArrayBuffer(4096);
353
-
354
- // First transfer
355
- await write(data1, buffer);
356
- const result1 = await read(buffer);
357
-
358
- // Second transfer - same buffer
359
- await write(data2, buffer);
360
- const result2 = await read(buffer);
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 © [Ivan Zakharchanka](https://linkedin.com/in/3axap4eHko)
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