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
|
# 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)
|
## 2026-02-26 - 1.3.0 - feat(transport)
|
||||||
introduce transport abstraction and socket-mode support for RustBridge
|
introduce transport abstraction and socket-mode support for RustBridge
|
||||||
|
|
||||||
|
|||||||
174
readme.md
174
readme.md
@@ -1,6 +1,6 @@
|
|||||||
# @push.rocks/smartrust
|
# @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
|
## Issue Reporting and Security
|
||||||
|
|
||||||
@@ -16,25 +16,34 @@ pnpm install @push.rocks/smartrust
|
|||||||
|
|
||||||
## Overview 🔭
|
## 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? 🤔
|
### Why? 🤔
|
||||||
|
|
||||||
If you're integrating Rust into a Node.js project, you'll inevitably need:
|
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 **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
|
- **Type-safe** request/response patterns with proper error handling
|
||||||
- **Streaming responses** for progressive data processing, log tailing, or chunked transfers
|
- **Streaming responses** for progressive data processing, log tailing, or chunked transfers
|
||||||
- **Event streaming** from Rust to TypeScript
|
- **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 🚀
|
## Usage 🚀
|
||||||
|
|
||||||
### The IPC Protocol
|
### 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 |
|
| 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** (Stream Chunk) | `{"id": "req_1", "stream": true, "data": {...}}` | Intermediate chunk (zero or more) |
|
||||||
| **Rust → TS** (Event) | `{"event": "ready", "data": {...}}` | Unsolicited event (no ID) |
|
| **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
|
### 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
|
```typescript
|
||||||
const bridge = new RustBridge<TMyCommands>({
|
const bridge = new RustBridge<TMyCommands>({
|
||||||
@@ -90,10 +101,62 @@ bridge.on('management:configChanged', (data) => {
|
|||||||
console.log('Config was changed:', data);
|
console.log('Config was changed:', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean shutdown
|
// Clean shutdown (SIGTERM → SIGKILL after 5s)
|
||||||
bridge.kill();
|
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 🌊
|
### 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.
|
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 📡
|
### Events 📡
|
||||||
|
|
||||||
`RustBridge` extends `EventEmitter` and emits the following events:
|
`RustBridge` extends `EventEmitter` and emits the following events:
|
||||||
@@ -238,8 +312,9 @@ const bridge = new RustBridge<TMyCommands>({
|
|||||||
| Event | Payload | Description |
|
| Event | Payload | Description |
|
||||||
|-------|---------|-------------|
|
|-------|---------|-------------|
|
||||||
| `ready` | — | Bridge connected and binary reported ready |
|
| `ready` | — | Bridge connected and binary reported ready |
|
||||||
| `exit` | `(code, signal)` | Rust process exited |
|
| `exit` | `(code, signal)` | Transport closed (process exited or socket disconnected) |
|
||||||
| `stderr` | `string` | A line from the binary's stderr |
|
| `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`) |
|
| `management:<name>` | `any` | Custom event from Rust (e.g. `management:configChanged`) |
|
||||||
|
|
||||||
### Custom Logger 📝
|
### Custom Logger 📝
|
||||||
@@ -263,7 +338,9 @@ const bridge = new RustBridge<TMyCommands>({
|
|||||||
|
|
||||||
### Writing the Rust Side 🦀
|
### 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:
|
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
|
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
|
```rust
|
||||||
use serde::{Deserialize, Serialize};
|
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 📖
|
## API Reference 📖
|
||||||
|
|
||||||
### `RustBridge<TCommands>`
|
### `RustBridge<TCommands>`
|
||||||
@@ -375,11 +486,12 @@ fn main() {
|
|||||||
| Method / Property | Signature | Description |
|
| Method / Property | Signature | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `constructor` | `new RustBridge<T>(options: IRustBridgeOptions)` | Create a new bridge instance |
|
| `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 |
|
| `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 |
|
| `sendCommandStreaming(method, params)` | `StreamingResponse<TChunk, TResult>` | Send a streaming command; returns immediately |
|
||||||
| `kill()` | `void` | SIGTERM the process, reject pending requests, force SIGKILL after 5s |
|
| `kill()` | `void` | Stdio: SIGTERM the process, SIGKILL after 5s. Socket: close the connection (daemon stays alive) |
|
||||||
| `running` | `boolean` | Whether the bridge is currently connected |
|
| `running` | `boolean` | Whether the bridge is currently connected and ready |
|
||||||
|
|
||||||
### `StreamingResponse<TChunk, TResult>`
|
### `StreamingResponse<TChunk, TResult>`
|
||||||
|
|
||||||
@@ -396,13 +508,43 @@ fn main() {
|
|||||||
| `findBinary()` | `Promise<string \| null>` | Find the binary using the priority search; result is cached |
|
| `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 |
|
| `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
|
### Exported Interfaces & Types
|
||||||
|
|
||||||
| Interface / Type | Description |
|
| Interface / Type | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `IRustBridgeOptions` | Full configuration for `RustBridge` |
|
| `IRustBridgeOptions` | Full configuration for `RustBridge` |
|
||||||
| `IBinaryLocatorOptions` | Configuration for `RustBinaryLocator` |
|
| `IBinaryLocatorOptions` | Configuration for `RustBinaryLocator` |
|
||||||
|
| `ISocketConnectOptions` | Socket connection options (reconnect settings) |
|
||||||
| `IRustBridgeLogger` | Logger interface: `{ log(level, message, data?) }` |
|
| `IRustBridgeLogger` | Logger interface: `{ log(level, message, data?) }` |
|
||||||
|
| `IRustTransport` | Transport interface (extends `EventEmitter`) |
|
||||||
| `IManagementRequest` | IPC request shape: `{ id, method, params }` |
|
| `IManagementRequest` | IPC request shape: `{ id, method, params }` |
|
||||||
| `IManagementResponse` | IPC response shape: `{ id, success, result?, error? }` |
|
| `IManagementResponse` | IPC response shape: `{ id, success, result?, error? }` |
|
||||||
| `IManagementEvent` | IPC event shape: `{ event, data }` |
|
| `IManagementEvent` | IPC event shape: `{ event, data }` |
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartrust',
|
name: '@push.rocks/smartrust',
|
||||||
version: '1.3.0',
|
version: '1.3.1',
|
||||||
description: 'a bridge between JS engines and rust'
|
description: 'a bridge between JS engines and rust'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user