A high-throughput, reliable, ordered stream abstraction over any postMessage boundary — iframe, web worker, service worker, MessageChannel.
Drop it into your existing postMessage wiring. Get stream semantics with backpressure, ordering, typed errors, and feature-detected fast paths.
# npm
npm install postwire
# pnpm
pnpm add postwire
# JSR
npx jsr add @sandwich/postwire
# or: deno add jsr:@sandwich/postwire// main.ts — initiator side (parent page / main thread)
import { createChannel, createStream, createWorkerEndpoint } from "postwire";
const worker = new Worker("./worker.js", { type: "module" });
const endpoint = createWorkerEndpoint(worker);
const channel = createChannel(endpoint);
await channel.capabilityReady;
const { writable } = createStream(channel);
const writer = writable.getWriter();
for (const chunk of chunks) {
await writer.write(chunk);
}
await writer.close();// worker.ts — responder side
import { createChannel, createStream, createWorkerEndpoint } from "postwire";
const endpoint = createWorkerEndpoint(self as DedicatedWorkerGlobalScope);
const channel = createChannel(endpoint, { role: "responder" });
channel.onStream(() => {
const { readable } = createStream(channel);
readable.pipeTo(new WritableStream({
write(chunk) { console.log("received", chunk); }
}));
});- Reliable + ordered delivery — credit-based flow control with a reorder buffer; out-of-order frames are reassembled before surfacing to the consumer
- Backpressure end-to-end — WHATWG Streams
desiredSizewired to the credit window;pipeTo/pipeThroughstall the writer when the reader is slow - Three API surfaces — low-level
send/onChunk/close, Node-style EventEmitter, or WHATWG{ readable, writable }pair - Four endpoint adapters —
Worker,MessagePort,Window(cross-origin iframe),ServiceWorker/Client - Feature-detected fast paths — transferable
ArrayBuffer(zero-copy), structured-clone fallback, opt-inSharedArrayBufferring (cross-origin-isolated only) - Relay topology —
createRelayBridgeforwards frames between two channels without reassembly; credits propagate end-to-end - Multiplex mode — multiple concurrent logical streams over one endpoint; per-stream credit windows are independent
- Strict CSP compatible — no
eval, nonew Function; WASM path is opt-in with explicit caller CSP relaxation - Lifecycle safety — BFCache (
pagehide), heartbeat for service workers, endpoint teardown (CHANNEL_DEAD/CHANNEL_FROZEN/CHANNEL_CLOSED) - Typed errors — every failure is a
StreamErrorwith a stable.codediscriminant - Zero runtime dependencies
| Document | Contents |
|---|---|
| API · low-level | createLowLevelStream — the primitive all adapters compose on |
| API · EventEmitter | createEmitterStream — Node-style EventEmitter wrapper |
| API · WHATWG Streams | createStream — { readable, writable } pair |
| Endpoints | Worker, MessagePort, Window, ServiceWorker adapters |
| Topology | Two-party, relay bridge, multiplex mode |
| Errors | All StreamError.code values with recovery patterns |
| Security | Origin validation, strict CSP, COOP/COEP, trust boundaries |
| Benchmarks | Throughput/latency table from benchmarks/results/baseline.json |
| Decisions | Architecture decision log |
| Example | Description |
|---|---|
| 01 · parent ↔ iframe | Parent sends 1 MB blob to sandboxed iframe via createStream |
| 02 · main ↔ worker | Main thread streams data to a Worker; delivery rate logged |
| 03 · three-hop relay | Worker → main relay → strict-CSP iframe; live chunk counter |
| 04 · multiplex | Two concurrent streams over one MessageChannel |
| 05 · strict CSP | Sandboxed iframe receives 512 KB payload under script-src 'self' |
Run any example:
git clone https://github.com/sandwichfarm/postwire
cd postwire/examples/01-parent-iframe
pnpm install && pnpm devEnvironment: Node 22.22.1 · MessageChannel (node) · commit d32e87c · 2026-04-21T18:27:10.870Z
| Scenario | Payload | Throughput (MB/s) | p50 (ms) | p99 (ms) | Samples |
|---|---|---|---|---|---|
| library (transferable) | 1 KB | 13.35 | 0.07 | 0.11 | 26,081 |
| library (transferable) | 64 KB | 721.75 | 0.09 | 0.16 | 22,027 |
| library (transferable) | 1 MB | 2222.94 | 0.44 | 0.77 | 4,240 |
| library (transferable) | 16 MB | 1923.45 | 8.63 | 10.69 | 230 |
| library (SAB) | 1 KB | 3.29 | 0.25 | 0.96 | 6,431 |
| library (SAB) | 64 KB | 207.96 | 0.28 | 1.09 | 6,347 |
| library (SAB) | 1 MB | 1197.53 | 0.98 | 1.50 | 2,285 |
| library (SAB) | 16 MB | 1296.44 | 14.80 | 19.37 | 155 |
| library (structured-clone) | 1 KB | 13.96 | 0.07 | 0.14 | 27,263 |
| library (structured-clone) | 64 KB | 140.58 | 0.44 | 0.75 | 4,291 |
| library (structured-clone) | 1 MB | 119.11 | 8.39 | 12.27 | 228 |
| library (structured-clone) | 16 MB | 64.76 | 256.31 | 321.51 | 10 |
| naive postMessage | 1 KB | 63.88 | 0.02 | 0.03 | 124,768 |
| naive postMessage | 64 KB | 1600.62 | 0.04 | 0.10 | 48,848 |
| naive postMessage | 1 MB | 2519.02 | 0.38 | 1.91 | 4,805 |
| naive postMessage | 16 MB | 4511.95 | 3.53 | 5.29 | 538 |
On the
naive postMessagerow. This baseline is a single rawArrayBuffertransfer per message — no framing, no ordering, no backpressure, no multiplexing, no relay. It measures the ceiling of the underlying transport, not a comparable alternative. postwire layers stream semantics on top of that transport (ordered delivery, credit-window backpressure, multiplexed streams, multi-hop relay via structured-clone-only hops) that rawpostMessagecannot provide. The honest question is "does this overhead fit my budget for the features I need?" — not "is it faster than the transport it is built on?" It is not, and cannot be.
Generated by scripts/bench-to-readme.mjs — do not edit by hand.
MIT © 2026 Sandwich Farm LLC