diff --git a/changelog.md b/changelog.md index 8c4c2ca..8fca7e1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 2026-02-26 - 1.3.1 - fix(readme) +document socket transport and clarify stdio/socket differences in README + +- Add 'Two Transport Modes' section documenting stdio (spawn) and socket (connect) modes +- Add examples for connect(), socket usage, and auto-reconnect with exponential backoff +- Clarify protocol is transport-agnostic and update ready/stream/event descriptions +- Update event docs: mark stderr as stdio-only and add 'reconnected' event for socket transports +- Clarify kill() behavior for both stdio and socket transports +- Add API reference entries for SocketTransport, StdioTransport, ISocketConnectOptions, IRustTransport, and LineScanner +- Add platform notes, architecture diagram, and minimal Rust/socket usage guidance + ## 2026-02-26 - 1.3.0 - feat(transport) introduce transport abstraction and socket-mode support for RustBridge diff --git a/readme.md b/readme.md index 251511b..d56024a 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # @push.rocks/smartrust -A type-safe, production-ready bridge between TypeScript and Rust binaries via JSON-over-stdin/stdout IPC — with support for request/response, streaming, and event patterns. +A type-safe, production-ready bridge between TypeScript and Rust binaries — with support for **stdio** (child process) and **socket** (Unix socket / Windows named pipe) transports, request/response, streaming, and event patterns. ## Issue Reporting and Security @@ -16,25 +16,34 @@ pnpm install @push.rocks/smartrust ## Overview 🔭 -`@push.rocks/smartrust` provides a complete bridge for TypeScript applications that need to communicate with Rust binaries. It handles the entire lifecycle — binary discovery, process spawning, request/response correlation, **streaming responses**, event pub/sub, and graceful shutdown — so you can focus on your command definitions instead of IPC plumbing. +`@push.rocks/smartrust` provides a complete bridge for TypeScript applications that need to communicate with Rust binaries. It handles the entire lifecycle — binary discovery, process spawning **or socket connection**, request/response correlation, **streaming responses**, event pub/sub, and graceful shutdown — so you can focus on your command definitions instead of IPC plumbing. + +### Two Transport Modes 🔌 + +| Mode | Method | Use Case | +|------|--------|----------| +| **Stdio** | `bridge.spawn()` | Spawn the Rust binary as a child process. Communicate via stdin/stdout. | +| **Socket** | `bridge.connect(path)` | Connect to an **already-running** Rust daemon via Unix socket or Windows named pipe. | + +The JSON protocol is identical in both modes — only the transport layer changes. Socket mode enables use cases where the Rust binary runs as a **privileged system service** (e.g., a VPN daemon needing root for TUN devices, a network proxy binding to privileged ports) while the TypeScript app connects to it unprivileged. ### Why? 🤔 If you're integrating Rust into a Node.js project, you'll inevitably need: - A way to **find** the compiled Rust binary across different environments (dev, CI, production, platform packages) -- A way to **spawn** it and establish reliable two-way communication +- A way to **spawn it** or **connect to it** and establish reliable two-way communication - **Type-safe** request/response patterns with proper error handling - **Streaming responses** for progressive data processing, log tailing, or chunked transfers - **Event streaming** from Rust to TypeScript -- **Graceful lifecycle management** (ready detection, clean shutdown, force kill) +- **Graceful lifecycle management** (ready detection, clean shutdown, auto-reconnection) -`smartrust` wraps all of this into three classes: `RustBridge`, `RustBinaryLocator`, and `StreamingResponse`. +`smartrust` wraps all of this into a clean API: `RustBridge`, `RustBinaryLocator`, `StreamingResponse`, and pluggable transports. ## Usage 🚀 ### The IPC Protocol -`smartrust` uses a simple, newline-delimited JSON protocol over stdin/stdout: +`smartrust` uses a simple, newline-delimited JSON protocol: | Direction | Format | Description | |-----------|--------|-------------| @@ -44,7 +53,7 @@ If you're integrating Rust into a Node.js project, you'll inevitably need: | **Rust → TS** (Stream Chunk) | `{"id": "req_1", "stream": true, "data": {...}}` | Intermediate chunk (zero or more) | | **Rust → TS** (Event) | `{"event": "ready", "data": {...}}` | Unsolicited event (no ID) | -Your Rust binary reads JSON lines from stdin and writes JSON lines to stdout. That's it. Stderr is free for logging. +This protocol works identically over stdio and socket transports. Your Rust binary reads JSON lines from one end and writes JSON lines to the other. That's it. ### Defining Your Commands @@ -62,7 +71,9 @@ type TMyCommands = { }; ``` -### Creating and Using the Bridge +### Stdio Mode — Spawn a Child Process + +This is the classic mode. The bridge spawns the Rust binary and communicates via stdin/stdout: ```typescript const bridge = new RustBridge({ @@ -90,10 +101,62 @@ bridge.on('management:configChanged', (data) => { console.log('Config was changed:', data); }); -// Clean shutdown +// Clean shutdown (SIGTERM → SIGKILL after 5s) bridge.kill(); ``` +### Socket Mode — Connect to a Running Daemon 🔗 + +When the Rust binary runs as a system service (e.g., via `systemd`, `launchd`, or a Windows Service), use `connect()` to talk to it over a Unix socket or named pipe: + +```typescript +const bridge = new RustBridge({ + binaryName: 'my-daemon', // used for logging / error messages +}); + +// Connect to the daemon's management socket +const ok = await bridge.connect('/var/run/my-daemon.sock'); +if (!ok) { + console.error('Failed to connect to daemon'); + process.exit(1); +} + +// Same API as stdio mode — completely transparent! +const { pid } = await bridge.sendCommand('start', { port: 8080, host: '0.0.0.0' }); +const metrics = await bridge.sendCommand('getMetrics', {}); + +// kill() closes the socket — it does NOT kill the daemon +bridge.kill(); +``` + +#### Auto-Reconnect + +For long-running applications, enable automatic reconnection with exponential backoff: + +```typescript +const ok = await bridge.connect('/var/run/my-daemon.sock', { + autoReconnect: true, // reconnect on unexpected disconnect + reconnectBaseDelayMs: 100, // initial retry delay (doubles each attempt) + reconnectMaxDelayMs: 30000, // max retry delay cap + maxReconnectAttempts: 10, // give up after 10 attempts +}); + +// Listen for reconnection events +bridge.on('reconnected', () => { + console.log('Reconnected to daemon!'); +}); +``` + +#### Platform Notes + +| Platform | Socket Path Format | Example | +|----------|-------------------|---------| +| **Linux** | `/var/run/.sock` or `$XDG_RUNTIME_DIR/.sock` | `/var/run/my-daemon.sock` | +| **macOS** | `/var/run/.sock` | `/var/run/my-daemon.sock` | +| **Windows** | `\\.\pipe\` | `\\.\pipe\my-daemon` | + +Node.js `net.connect()` handles all formats transparently — no platform-specific code needed. + ### Streaming Commands 🌊 For commands where the Rust binary sends a series of chunks before a final result, use `sendCommandStreaming`. This is perfect for progressive data processing, log tailing, search results, or any scenario where you want incremental output. @@ -231,6 +294,17 @@ const bridge = new RustBridge({ }); ``` +Socket connection options (passed to `bridge.connect()`): + +```typescript +interface ISocketConnectOptions { + autoReconnect?: boolean; // default: false + reconnectBaseDelayMs?: number; // default: 100 + reconnectMaxDelayMs?: number; // default: 30000 + maxReconnectAttempts?: number; // default: 10 +} +``` + ### Events 📡 `RustBridge` extends `EventEmitter` and emits the following events: @@ -238,8 +312,9 @@ const bridge = new RustBridge({ | Event | Payload | Description | |-------|---------|-------------| | `ready` | — | Bridge connected and binary reported ready | -| `exit` | `(code, signal)` | Rust process exited | -| `stderr` | `string` | A line from the binary's stderr | +| `exit` | `(code, signal)` | Transport closed (process exited or socket disconnected) | +| `stderr` | `string` | A line from the binary's stderr (stdio mode only) | +| `reconnected` | — | Socket transport reconnected after unexpected disconnect | | `management:` | `any` | Custom event from Rust (e.g. `management:configChanged`) | ### Custom Logger 📝 @@ -263,7 +338,9 @@ const bridge = new RustBridge({ ### Writing the Rust Side 🦀 -Your Rust binary needs to implement a simple protocol: +Your Rust binary needs to implement a simple protocol. The transport (stdio or socket) doesn't change the message format — only how connections are established. + +#### Stdio Mode (Child Process) 1. **On startup**, write a ready event to stdout: ``` @@ -280,7 +357,14 @@ Your Rust binary needs to implement a simple protocol: 6. **Use stderr** for logging — it won't interfere with the IPC protocol -Here's a minimal Rust skeleton: +#### Socket Mode (Daemon) + +1. **Listen** on a Unix socket (e.g., `/var/run/my-daemon.sock`) or Windows named pipe +2. **On each new client connection**, send the `{"event":"ready","data":{...}}\n` event +3. Read/write JSON lines on the socket (same protocol as stdio) +4. Support multiple concurrent clients — each connection is independent + +Here's a minimal Rust skeleton (stdio mode): ```rust use serde::{Deserialize, Serialize}; @@ -368,6 +452,33 @@ fn main() { } ``` +## Architecture 🏗️ + +``` +┌──────────────────────────────────────────────────────┐ +│ RustBridge │ +│ (protocol layer: handleLine, sendCommand, events) │ +├──────────────┬───────────────────────────────────────┤ +│ StdioTransport │ SocketTransport │ +│ spawn() + │ net.connect() + auto-reconnect │ +│ stdin/stdout │ Unix socket / named pipe │ +├──────────────┴───────────────────────────────────────┤ +│ IRustTransport interface │ +│ connect() / write() / disconnect() │ +├──────────────────────────────────────────────────────┤ +│ LineScanner (shared newline scanner) │ +├──────────────────────────────────────────────────────┤ +│ RustBinaryLocator │ +│ (binary search — stdio mode only) │ +└──────────────────────────────────────────────────────┘ +``` + +- **`RustBridge`** — The main class. Protocol-level logic (JSON parsing, request correlation, streaming, events) is transport-agnostic. +- **`StdioTransport`** — Spawns a child process, manages stdin/stdout/stderr, handles SIGTERM/SIGKILL. +- **`SocketTransport`** — Connects to an existing Unix socket or named pipe, with optional auto-reconnect and exponential backoff. +- **`LineScanner`** — Shared buffer-based newline scanner used by both transports for efficient message framing. +- **`RustBinaryLocator`** — Priority-ordered binary search (used by stdio mode only). + ## API Reference 📖 ### `RustBridge` @@ -375,11 +486,12 @@ fn main() { | Method / Property | Signature | Description | |---|---|---| | `constructor` | `new RustBridge(options: IRustBridgeOptions)` | Create a new bridge instance | -| `spawn()` | `Promise` | Spawn the binary and wait for ready; returns `false` on failure | +| `spawn()` | `Promise` | **Stdio mode**: Spawn the binary and wait for ready; returns `false` on failure | +| `connect(socketPath, options?)` | `Promise` | **Socket mode**: Connect to a running daemon; returns `false` on failure | | `sendCommand(method, params)` | `Promise` | Send a typed command and await the response | | `sendCommandStreaming(method, params)` | `StreamingResponse` | Send a streaming command; returns immediately | -| `kill()` | `void` | SIGTERM the process, reject pending requests, force SIGKILL after 5s | -| `running` | `boolean` | Whether the bridge is currently connected | +| `kill()` | `void` | Stdio: SIGTERM the process, SIGKILL after 5s. Socket: close the connection (daemon stays alive) | +| `running` | `boolean` | Whether the bridge is currently connected and ready | ### `StreamingResponse` @@ -396,13 +508,43 @@ fn main() { | `findBinary()` | `Promise` | Find the binary using the priority search; result is cached | | `clearCache()` | `void` | Clear the cached path to force a fresh search | +### `StdioTransport` + +| Method / Property | Signature | Description | +|---|---|---| +| `constructor` | `new StdioTransport(options: IStdioTransportOptions)` | Create a stdio transport | +| `connect()` | `Promise` | Spawn the child process | +| `write(data)` | `Promise` | Write to stdin with backpressure handling | +| `disconnect()` | `void` | Kill the process (SIGTERM → SIGKILL after 5s) | +| `connected` | `boolean` | Whether the process is running | + +### `SocketTransport` + +| Method / Property | Signature | Description | +|---|---|---| +| `constructor` | `new SocketTransport(options: ISocketTransportOptions)` | Create a socket transport | +| `connect()` | `Promise` | Connect to the Unix socket / named pipe | +| `write(data)` | `Promise` | Write to socket with backpressure handling | +| `disconnect()` | `void` | Close the socket (does not kill the daemon) | +| `connected` | `boolean` | Whether the socket is connected | + +### `LineScanner` + +| Method / Property | Signature | Description | +|---|---|---| +| `constructor` | `new LineScanner(maxPayloadSize, logger)` | Create a line scanner | +| `push(chunk, onLine)` | `void` | Feed a `Buffer` chunk; calls `onLine` for each complete line | +| `clear()` | `void` | Reset the internal buffer | + ### Exported Interfaces & Types | Interface / Type | Description | |---|---| | `IRustBridgeOptions` | Full configuration for `RustBridge` | | `IBinaryLocatorOptions` | Configuration for `RustBinaryLocator` | +| `ISocketConnectOptions` | Socket connection options (reconnect settings) | | `IRustBridgeLogger` | Logger interface: `{ log(level, message, data?) }` | +| `IRustTransport` | Transport interface (extends `EventEmitter`) | | `IManagementRequest` | IPC request shape: `{ id, method, params }` | | `IManagementResponse` | IPC response shape: `{ id, success, result?, error? }` | | `IManagementEvent` | IPC event shape: `{ event, data }` | diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 8dfdf52..c6755dc 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartrust', - version: '1.3.0', + version: '1.3.1', description: 'a bridge between JS engines and rust' }