Architecture
Architecture
Section titled “Architecture”Procwire is a dual-channel parent↔child IPC library.
Dual-Channel Architecture
Section titled “Dual-Channel Architecture”Procwire uses a binary protocol for the data plane and JSON-RPC for the control plane — a high-performance wire format where it matters, and a simple text protocol where it doesn’t.
| Channel | Transport | Protocol | Use Case |
|---|---|---|---|
| Control Plane | stdio | JSON-RPC 2.0 | Handshake, heartbeat, lifecycle |
| Data Plane | Named Pipe / Unix Socket | Binary (11-byte header) | User data, high throughput |
Why a binary data plane?
Section titled “Why a binary data plane?”JSON-RPC on the data channel would cause severe performance issues:
- 5x larger payload (JSON text vs binary)
- ~500ms serialization overhead per 1MB of data
- ~30 MB/s throughput vs ~2.5 GB/s with binary protocol
Wire Format (Data Plane)
Section titled “Wire Format (Data Plane)”Every data-plane message is a frame: an 11-byte header followed by the codec-encoded payload.
+----------+-------+----------+----------+----------------------+| Method ID| Flags | Req ID | Length | Payload || 2 bytes | 1 byte| 4 bytes | 4 bytes | N bytes |+----------+-------+----------+----------+----------------------+| Field | Size | Type | Meaning |
|---|---|---|---|
| Method ID | 2 bytes | uint16 BE | Method/event ID assigned during handshake; 0xFFFF is the abort signal |
| Flags | 1 byte | bitfield | Direction, response/error, stream chunk/end, ack (bits 6–7 reserved) |
| Request ID | 4 bytes | uint32 BE | Correlation ID; 0 means fire-and-forget or unsolicited (events) |
| Length | 4 bytes | uint32 BE | Payload length in bytes |
| Payload | N bytes | codec output | Serialized by the method’s codec (msgpack, Arrow, raw, …) |
All integers are big-endian. Method names never travel on the data plane — only the numeric IDs exchanged in the handshake.
Control-Plane Messages
Section titled “Control-Plane Messages”The control plane is newline-delimited JSON-RPC 2.0 over the child’s stdio:
| Message | Direction | Purpose |
|---|---|---|
$init | child → parent | Handshake: carries the data-plane pipe path and the child’s method/event schema |
$ping | parent → child | Heartbeat probe (only when spawnPolicy.heartbeat is enabled) |
$pong | child → parent | Heartbeat reply; a missed reply within timeoutMs kills the child |
$shutdown | parent → child | Graceful stop: the child closes its pipe server and exits on its own |
Reliability
Section titled “Reliability”- Per-module shutdown —
manager.shutdown(name)stops a single module: pending crash-restarts are cancelled,$shutdownis sent, and only a child that fails to exit within 5 seconds is force-killed. - Unregister & retry —
manager.unregister(name)removes a module from the registry (cancelling any pending restart/retry timers) so the name can be re-registered with a freshly builtModule— e.g. a supervisor retrying after a terminalSpawnErrorwith new CLI args. A running module requires{ force: true }, which shuts it down first;register(module, { replace: true })swaps a non-running module in one call. - Orphan prevention — the child treats stdin EOF as parent death and shuts itself down, so a crashed parent never leaves worker processes running forever.
- Exception-safe receive path — a framing error (e.g. an oversized payload) drops the connection instead of throwing from the socket handler, and a corrupt payload rejects only the affected request or stream. A bad frame cannot crash the parent — the supervisor of every module.