fix(readme): document socket transport and clarify stdio/socket differences in README
This commit is contained in:
11
changelog.md
11
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
|
||||
|
||||
|
||||
174
readme.md
174
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<TMyCommands>({
|
||||
@@ -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<TMyCommands>({
|
||||
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/<name>.sock` or `$XDG_RUNTIME_DIR/<name>.sock` | `/var/run/my-daemon.sock` |
|
||||
| **macOS** | `/var/run/<name>.sock` | `/var/run/my-daemon.sock` |
|
||||
| **Windows** | `\\.\pipe\<name>` | `\\.\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<TMyCommands>({
|
||||
});
|
||||
```
|
||||
|
||||
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<TMyCommands>({
|
||||
| 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:<name>` | `any` | Custom event from Rust (e.g. `management:configChanged`) |
|
||||
|
||||
### Custom Logger 📝
|
||||
@@ -263,7 +338,9 @@ const bridge = new RustBridge<TMyCommands>({
|
||||
|
||||
### 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<T> │
|
||||
│ (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<TCommands>`
|
||||
@@ -375,11 +486,12 @@ fn main() {
|
||||
| Method / Property | Signature | Description |
|
||||
|---|---|---|
|
||||
| `constructor` | `new RustBridge<T>(options: IRustBridgeOptions)` | Create a new bridge instance |
|
||||
| `spawn()` | `Promise<boolean>` | Spawn the binary and wait for ready; returns `false` on failure |
|
||||
| `spawn()` | `Promise<boolean>` | **Stdio mode**: Spawn the binary and wait for ready; returns `false` on failure |
|
||||
| `connect(socketPath, options?)` | `Promise<boolean>` | **Socket mode**: Connect to a running daemon; returns `false` on failure |
|
||||
| `sendCommand(method, params)` | `Promise<TCommands[K]['result']>` | Send a typed command and await the response |
|
||||
| `sendCommandStreaming(method, params)` | `StreamingResponse<TChunk, TResult>` | 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<TChunk, TResult>`
|
||||
|
||||
@@ -396,13 +508,43 @@ fn main() {
|
||||
| `findBinary()` | `Promise<string \| null>` | 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<void>` | Spawn the child process |
|
||||
| `write(data)` | `Promise<void>` | 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<void>` | Connect to the Unix socket / named pipe |
|
||||
| `write(data)` | `Promise<void>` | 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 }` |
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user