SABCOM
High-performance inter-thread communication using SharedArrayBuffer with atomic operations. Bidirectional channels with synchronous and asynchronous APIs. Faster than postMessage structured clone and transfer.
What sabcom Does
sabcom is a ring-buffer protocol over SharedArrayBuffer. It handles:
- Bidirectional communication on a single SharedArrayBuffer
- Synchronization between endpoints via Atomics
- Segmented transfer for payloads larger than the ring capacity
- Timeout detection and close semantics
sabcom does NOT:
- Create worker threads (you create them with
worker_threadsornew Worker()) - Transfer the SharedArrayBuffer between threads (you pass it via
workerDataorpostMessage) - Serialize data (you encode to
Uint8Arraybefore calling write, e.g., withTextEncoderorJSON.stringify)
Features
- Bidirectional - both endpoints can read and write on a single buffer
- Async and Sync APIs -
write/read(non-blocking) andwriteSync/readSync(blocking) - Zero-copy reads - single-segment messages return a view into the SharedArrayBuffer
- Segmented transfer - payloads larger than the ring are split automatically
- Close and reuse - channels can be closed and buffers reused
- Configurable timeouts - per-operation timeout support
- Type-safe - full TypeScript support
Installation
npm install sabcom
Complete Example
worker.ts
import { workerData } from 'worker_threads';
import { open } from 'sabcom';
const ch = open(workerData as SharedArrayBuffer);
const data = ch.readSync();
const message = new TextDecoder().decode(data);
console.log('Worker received:', message);
ch.writeSync(new TextEncoder().encode(message.toUpperCase()));
ch.close();
main.ts
import { Worker } from 'worker_threads';
import { createBuffer, open } from 'sabcom';
const buffer = createBuffer(65536);
const worker = new Worker('./worker.js', { workerData: buffer });
const ch = open(buffer);
const text = 'Hello from the main thread!';
await ch.write(new TextEncoder().encode(text));
const reply = await ch.read();
console.log('Reply:', new TextDecoder().decode(reply));
ch.close();
await worker.terminate();
API
createBuffer(byteLength: number): SharedArrayBuffer
Creates and initializes a SharedArrayBuffer for use as a channel. byteLength must be a multiple of 4 and at least 4096.
open(buffer: SharedArrayBuffer): Channel
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.
reset(buffer: SharedArrayBuffer): void
Resets a closed buffer so it can be reopened. Both endpoints must be closed first.
Channel
interface Channel {
write(data: Uint8Array, options?: Options): Promise<void>;
read(options?: Options): Promise<Uint8Array>;
writeSync(data: Uint8Array, options?: Options): void;
readSync(options?: Options): Uint8Array;
close(): void;
}
write/writeSync- send data to the peer endpointread/readSync- receive data from the peer endpointclose- close this endpoint and notify the peer
Options
interface Options {
timeout?: number; // milliseconds, default: 5000
}
Zero-Copy Reads
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.
If you need to keep the data beyond the next call, copy it:
const view = ch.readSync();
const copy = view.slice(); // safe to hold indefinitely
Messages that wrap the ring boundary or span multiple segments return an owned copy automatically.
Buffer Sizing
- Minimum: 4096 bytes
- Multiple of 4 required
- Larger buffers reduce segmentation overhead for large payloads
- Recommendation: 4KB-64KB depending on average payload size
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.
Multi-Worker Architecture
Each worker needs its own SharedArrayBuffer. A single buffer supports exactly two endpoints.
import { Worker } from 'worker_threads';
import { createBuffer, open } from 'sabcom';
async function spawnWorker(task: string) {
const buffer = createBuffer(65536);
const worker = new Worker('./worker.js', { workerData: buffer });
const ch = open(buffer);
ch.writeSync(new TextEncoder().encode(task));
const result = await ch.read();
ch.close();
await worker.terminate();
return new TextDecoder().decode(result);
}
const results = await Promise.all([
spawnWorker('task-a'),
spawnWorker('task-b'),
spawnWorker('task-c'),
]);
FAQ
How do I send JSON or objects?
sabcom transfers raw bytes. Serialize before sending:
const obj = { hello: 'world', count: 42 };
ch.writeSync(new TextEncoder().encode(JSON.stringify(obj)));
const data = ch.readSync();
const parsed = JSON.parse(new TextDecoder().decode(data));
Does sabcom work in browsers?
Yes, with Web Workers. SharedArrayBuffer requires cross-origin isolation headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Can I reuse the buffer after closing?
Yes. After both endpoints close, call reset(buffer) and reopen:
ch.close();
// ... peer also closes ...
reset(buffer);
const ch2 = open(buffer);
Or pass the buffer to new workers - open auto-resets buffers where both endpoints have closed.
How do I handle errors?
try {
ch.writeSync(data, { timeout: 5000 });
} catch (err) {
if (err.message.includes('timeout')) {
console.error('Peer did not respond in time');
} else if (err.message.includes('closed')) {
console.error('Peer closed the channel');
}
}
Development
pnpm install
pnpm build
pnpm test
pnpm bench
License
Apache-2.0 (c) Ivan Zakharchanka