@zero.sc/bridge
v0.1.2
Published
Call Python, Rust, Go and Swift from Next.js over a warm worker pool — typed, with binary framing, no process per call.
Maintainers
Readme
The problem this solves
Some work does not belong in Node. Embeddings, image decoding, numerical code, anything with
a mature library in another language. The usual answers are to stand up a microservice — HTTP
server, health check, deployment, another thing that can be down — or to spawn a process
per call and pay start-up cost every time. Python costs tens of milliseconds to boot before
it has done anything.
There is a third option that people write badly by hand: keep the process alive and talk to it over stdio. It is badly written because the naive version delimits messages with newlines, and the first payload containing a newline desynchronises the stream permanently. The symptom appears three calls later, in an unrelated function.
This package is that third option, written once and carefully. Frames carry their length, so a partial write cannot desynchronise anything.
Install
npm install @zero.sc/bridgeNo runtime dependencies. The language runtimes you actually call need to exist on the machine — this package does not install Python for you.
Sixty seconds
import { defineBridge, pythonWorker } from '@zero.sc/bridge';
interface Contract {
embed(text: string): number[];
slugify(text: string): string;
countdown(n: number): AsyncGenerator<{ tick: number }>;
}
const py = defineBridge<Contract>({
worker: pythonWorker({ cwd: './workers/embed' }),
functions: ['embed', 'slugify', 'countdown'],
streams: ['countdown'],
workers: 4,
timeoutMs: 20_000,
});
const vector = await py.embed('hello');
for await (const tick of py.countdown(3)) {
console.log(tick);
}
await py.$close();The Python side is a small worker script; a ready-made one ships in the package for each runtime, so the first call works before you have written any protocol code.
What's inside
| Piece | What it does |
|-------|--------------|
| defineBridge | Turns a TypeScript interface into a callable client |
| defineSharedBridge | One pool shared across modules instead of one per import |
| BridgePool | Warm workers, queueing, restart on death |
| protocol | Length-prefixed frames, version, size ceiling, corruption detection |
| presets | pythonWorker, rustWorker, goWorker, swiftWorker, nodeWorker |
| next | Helpers for calling from route handlers without leaking pools on reload |
| zero-bridge | A CLI for scaffolding and inspecting a worker |
Presets do the obvious local thing: Python prefers a .venv if one exists; Rust prefers a
release binary and warns when it falls back to a debug build, because a debug build can be
an order of magnitude slower and that difference gets misread as the bridge being slow.
Failure has six names
A single BridgeError would be easy to write and useless to handle. These are separate
because the correct response differs:
| Error | Means | You should |
|-------|-------|------------|
| BridgeTimeoutError | Deadline passed — carries which phase | Retry, maybe with a longer deadline |
| BridgeCallError | The worker raised | Fix the input; do not retry |
| BridgeContractError | Shape did not match the declared type | Fix the contract |
| BridgeOverloadError | Queue is full | Shed load or add workers |
| BridgeUnavailableError | No worker could start | Check the toolchain |
| BridgeProtocolError | The frame was malformed | Version mismatch |
The contract it keeps
- Frames carry their length. Not newline-delimited, so binary payloads and multi-line strings are ordinary data.
- Workers stay warm. Start-up is paid at pool creation, not per call.
- A dead worker does not take the pool with it. It is replaced; queued calls proceed.
- Streams are first class. A generator on the far side is an async generator here, so a long result arrives incrementally instead of as one large buffer.
- The tests spawn real processes. Mocking the boundary would leave nothing verified, since the boundary is the entire product.
Honest limits
- The toolchain has to be there. Calling Rust needs
cargo, calling Go needsgo, calling Swift needs a Swift toolchain. Missing ones surface asBridgeUnavailableErrorrather than something mysterious, but they still fail. - Local processes only. There is no remote transport. A pool is on this machine.
- Payloads are JSON, plus binary as byte arrays. No shared memory, no zero-copy — a large array is serialised, and above some size a real IPC mechanism would beat this.
- The pool is per process. Several Node workers mean several pools, and
workers: 4means four per Node process. - Swift support is the least exercised. It works and it is tested; it has seen the least real use of the five.
- This is 0.1.x. The protocol is versioned and a mismatch is detected rather than misparsed, but the version can still move.
Around it
| | |
|---|---|
| Docs | kit.zero.sc |
| Events | @zero.sc/events |
| Calling services | @zero.sc/sdk |
| Everything | @zero.sc |
License
MIT OR Zero License v1.0 — take whichever you prefer. Choosing MIT is enough; nothing further is required of you.
The Zero name, marks and logos are not covered — build anything you like with this code, just don't present it as a Zero product.
Copyright (c) 2026 Zero. Source Code begins at Zero.
