Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd3fc7518b | |||
| 1b462e3a35 | |||
| 4ed42945fc | |||
| a0638b5364 | |||
| 32f3c63fca | |||
| f1534ad531 |
28
changelog.md
28
changelog.md
@@ -1,5 +1,33 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-28 - 2.1.3 - fix(classes.ipcchannel)
|
||||||
|
Normalize heartbeatThrowOnTimeout option parsing and allow registering 'heartbeatTimeout' via IpcChannel.on
|
||||||
|
|
||||||
|
- Normalize heartbeatThrowOnTimeout to boolean (accepts 'true'/'false' strings and other truthy/falsey values) to be defensive for JS consumers
|
||||||
|
- Expose 'heartbeatTimeout' as a special channel event so handlers registered via IpcChannel.on('heartbeatTimeout', ...) will be called
|
||||||
|
|
||||||
|
## 2025-08-26 - 2.1.2 - fix(core)
|
||||||
|
Improve heartbeat handling and transport routing; forward heartbeat timeout events; include clientId routing and probe improvements
|
||||||
|
|
||||||
|
- IpcChannel: add heartbeatInitialGracePeriod handling — delay heartbeat timeout checks until the grace period elapses and use a minimum check interval (>= 1000ms)
|
||||||
|
- IpcChannel: add heartbeatGraceTimer and ensure stopHeartbeat clears the grace timer to avoid repeated events
|
||||||
|
- IpcChannel / Client / Server: forward heartbeatTimeout events instead of only throwing when configured (heartbeatThrowOnTimeout = false) so consumers can handle timeouts via events
|
||||||
|
- IpcClient: include clientId in registration request headers to enable proper routing on the server/transport side
|
||||||
|
- UnixSocketTransport: track socket <-> clientId mappings, clean them up on socket close, and update mappings when __register__ or messages containing clientId are received
|
||||||
|
- UnixSocketTransport: route messages to a specific client when headers.clientId is present (fallback to broadcasting when no target is found), and emit both clientMessage and message for parsed client messages
|
||||||
|
- ts/index.waitForServer: use SmartIpc.createClient for probing, shorten probe register timeout, and use a slightly longer retry delay between probes for stability
|
||||||
|
|
||||||
|
## 2025-08-25 - 2.1.1 - fix(readme)
|
||||||
|
Update README: expand docs, examples, server readiness, heartbeat, and testing utilities
|
||||||
|
|
||||||
|
- Rewrite introduction and overall tone to emphasize zero-dependency, reliability, and TypeScript support
|
||||||
|
- Replace several Quick Start examples to use socketPath and show autoCleanupSocketFile usage
|
||||||
|
- Add Server readiness detection docs and SmartIpc.waitForServer example
|
||||||
|
- Document smart connection retry options (connectRetry) and registerTimeoutMs usage
|
||||||
|
- Clarify heartbeat configuration and add heartbeatThrowOnTimeout option to emit events instead of throwing
|
||||||
|
- Add sections for automatic socket cleanup, broadcasting, testing utilities (waitForServer, spawnAndConnect), and metrics
|
||||||
|
- Various formatting and copy improvements throughout README
|
||||||
|
|
||||||
## 2025-08-25 - 2.1.0 - feat(core)
|
## 2025-08-25 - 2.1.0 - feat(core)
|
||||||
Add heartbeat grace/timeout options, client retry/wait-for-ready, server readiness and socket cleanup, transport socket options, helper utilities, and tests
|
Add heartbeat grace/timeout options, client retry/wait-for-ready, server readiness and socket cleanup, transport socket options, helper utilities, and tests
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartipc",
|
"name": "@push.rocks/smartipc",
|
||||||
"version": "2.1.0",
|
"version": "2.1.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A library for node inter process communication, providing an easy-to-use API for IPC.",
|
"description": "A library for node inter process communication, providing an easy-to-use API for IPC.",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
626
readme.md
626
readme.md
@@ -1,34 +1,33 @@
|
|||||||
# @push.rocks/smartipc 🚀
|
# @push.rocks/smartipc 🚀
|
||||||
|
|
||||||
**Lightning-fast, type-safe IPC for modern Node.js applications**
|
**Rock-solid IPC for Node.js with zero dependencies**
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/@push.rocks/smartipc)
|
[](https://www.npmjs.com/package/@push.rocks/smartipc)
|
||||||
[](https://www.typescriptlang.org/)
|
[](https://www.typescriptlang.org/)
|
||||||
[](./license)
|
[](./license)
|
||||||
|
|
||||||
SmartIPC is a production-grade Inter-Process Communication library that brings enterprise-level messaging patterns to Node.js. Built with TypeScript from the ground up, it offers zero-dependency native IPC with automatic reconnection, type-safe messaging, and built-in observability.
|
SmartIPC delivers bulletproof Inter-Process Communication for Node.js applications. Built for real-world production use, it handles all the edge cases that make IPC tricky - automatic reconnection, race conditions, heartbeat monitoring, and clean shutdowns. All with **zero external dependencies** and full TypeScript support.
|
||||||
|
|
||||||
## Why SmartIPC?
|
## 🎯 Why SmartIPC?
|
||||||
|
|
||||||
- **🎯 Zero External Dependencies** - Pure Node.js implementation using native `net` module
|
- **Zero Dependencies** - Pure Node.js implementation using native modules
|
||||||
- **🔒 Type-Safe** - Full TypeScript support with generics for compile-time safety
|
- **Battle-tested Reliability** - Automatic reconnection, graceful degradation, and timeout handling
|
||||||
- **🔄 Auto-Reconnect** - Built-in exponential backoff and circuit breaker patterns
|
- **Type-Safe** - Full TypeScript support with generics for compile-time safety
|
||||||
- **📊 Observable** - Real-time metrics and connection tracking
|
- **CI/Test Ready** - Built-in helpers and race condition prevention for testing
|
||||||
- **⚡ High Performance** - Length-prefixed framing, backpressure handling, and optimized buffers
|
- **Observable** - Real-time metrics, connection tracking, and health monitoring
|
||||||
- **🎭 Multiple Patterns** - Request/Response, Pub/Sub, and Fire-and-Forget messaging
|
- **Multiple Patterns** - Request/Response, Pub/Sub, and Fire-and-Forget messaging
|
||||||
- **🛡️ Production Ready** - Message size limits, heartbeat monitoring, and graceful shutdown
|
|
||||||
|
|
||||||
## Installation
|
## 📦 Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
npm install @push.rocks/smartipc
|
||||||
|
# or
|
||||||
pnpm add @push.rocks/smartipc
|
pnpm add @push.rocks/smartipc
|
||||||
# or
|
# or
|
||||||
npm install @push.rocks/smartipc
|
yarn add @push.rocks/smartipc
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Simple TCP Server & Client
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SmartIpc } from '@push.rocks/smartipc';
|
import { SmartIpc } from '@push.rocks/smartipc';
|
||||||
@@ -36,34 +35,42 @@ import { SmartIpc } from '@push.rocks/smartipc';
|
|||||||
// Create a server
|
// Create a server
|
||||||
const server = SmartIpc.createServer({
|
const server = SmartIpc.createServer({
|
||||||
id: 'my-service',
|
id: 'my-service',
|
||||||
host: 'localhost',
|
socketPath: '/tmp/my-service.sock',
|
||||||
port: 9876
|
autoCleanupSocketFile: true // Clean up stale sockets automatically
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle incoming messages
|
// Handle incoming messages
|
||||||
server.onMessage('hello', async (data, clientId) => {
|
server.onMessage('greet', async (data, clientId) => {
|
||||||
console.log(`Client ${clientId} says:`, data);
|
console.log(`Client ${clientId} says:`, data.message);
|
||||||
return { response: 'Hello back!' };
|
return { response: `Hello ${data.name}!` };
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.start();
|
// Start the server
|
||||||
|
await server.start({ readyWhen: 'accepting' }); // Wait until fully ready
|
||||||
|
console.log('Server is ready to accept connections! ✨');
|
||||||
|
|
||||||
// Create a client
|
// Create a client
|
||||||
const client = SmartIpc.createClient({
|
const client = SmartIpc.createClient({
|
||||||
id: 'my-service',
|
id: 'my-service',
|
||||||
host: 'localhost',
|
socketPath: '/tmp/my-service.sock',
|
||||||
port: 9876,
|
connectRetry: {
|
||||||
clientId: 'client-1'
|
enabled: true,
|
||||||
|
maxAttempts: 10
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Connect with automatic retry
|
||||||
await client.connect();
|
await client.connect();
|
||||||
|
|
||||||
// Send a message and get response
|
// Send a request and get a response
|
||||||
const response = await client.request('hello', { message: 'Hi server!' });
|
const response = await client.request('greet', {
|
||||||
console.log('Server responded:', response);
|
name: 'World',
|
||||||
|
message: 'Hi there!'
|
||||||
|
});
|
||||||
|
console.log('Server said:', response.response); // "Hello World!"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Core Concepts
|
## 🎮 Core Concepts
|
||||||
|
|
||||||
### Transport Types
|
### Transport Types
|
||||||
|
|
||||||
@@ -84,348 +91,394 @@ const unixServer = SmartIpc.createServer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Windows Named Pipe (Windows optimal)
|
// Windows Named Pipe (Windows optimal)
|
||||||
const pipeServer = SmartIpc.createServer({
|
// Automatically used on Windows when socketPath is provided
|
||||||
|
const windowsServer = SmartIpc.createServer({
|
||||||
id: 'pipe-service',
|
id: 'pipe-service',
|
||||||
pipeName: 'my-app-pipe'
|
socketPath: '\\\\.\\pipe\\my-app-pipe'
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Message Patterns
|
### Message Patterns
|
||||||
|
|
||||||
#### 🔥 Fire and Forget
|
#### 🔥 Fire and Forget
|
||||||
Fast, one-way messaging when you don't need a response:
|
Send messages without waiting for a response:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Server
|
// Server
|
||||||
server.onMessage('log', (data, clientId) => {
|
server.onMessage('log', (data, clientId) => {
|
||||||
console.log(`[${clientId}]:`, data.message);
|
console.log(`[${clientId}] ${data.level}:`, data.message);
|
||||||
// No return value needed
|
// No return needed
|
||||||
});
|
});
|
||||||
|
|
||||||
// Client
|
// Client
|
||||||
await client.sendMessage('log', {
|
await client.sendMessage('log', {
|
||||||
|
level: 'info',
|
||||||
message: 'User logged in',
|
message: 'User logged in',
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 📞 Request/Response
|
#### 📞 Request/Response
|
||||||
RPC-style communication with timeouts and type safety:
|
RPC-style communication with type safety:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Server - Define your handler with types
|
interface UserRequest {
|
||||||
interface CalculateRequest {
|
userId: string;
|
||||||
operation: 'add' | 'multiply';
|
fields?: string[];
|
||||||
values: number[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CalculateResponse {
|
interface UserResponse {
|
||||||
result: number;
|
id: string;
|
||||||
computedAt: number;
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
createdAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
server.onMessage<CalculateRequest, CalculateResponse>('calculate', async (data) => {
|
// Server
|
||||||
const result = data.operation === 'add'
|
server.onMessage<UserRequest, UserResponse>('getUser', async (data) => {
|
||||||
? data.values.reduce((a, b) => a + b, 0)
|
const user = await db.getUser(data.userId);
|
||||||
: data.values.reduce((a, b) => a * b, 1);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result,
|
id: user.id,
|
||||||
computedAt: Date.now()
|
name: user.name,
|
||||||
|
email: data.fields?.includes('email') ? user.email : undefined,
|
||||||
|
createdAt: user.createdAt
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Client - Type-safe request
|
// Client - with timeout
|
||||||
const response = await client.request<CalculateRequest, CalculateResponse>(
|
const user = await client.request<UserRequest, UserResponse>(
|
||||||
'calculate',
|
'getUser',
|
||||||
{ operation: 'add', values: [1, 2, 3, 4, 5] },
|
{ userId: '123', fields: ['email'] },
|
||||||
{ timeout: 5000 }
|
{ timeout: 5000 }
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Sum is ${response.result}`);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 📢 Pub/Sub Pattern
|
#### 📢 Pub/Sub Pattern
|
||||||
Topic-based message broadcasting:
|
Topic-based message broadcasting:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Server automatically handles subscriptions
|
// Subscribers
|
||||||
const publisher = SmartIpc.createClient({
|
|
||||||
id: 'events-service',
|
|
||||||
clientId: 'publisher'
|
|
||||||
});
|
|
||||||
|
|
||||||
const subscriber1 = SmartIpc.createClient({
|
const subscriber1 = SmartIpc.createClient({
|
||||||
id: 'events-service',
|
id: 'events-service',
|
||||||
clientId: 'subscriber-1'
|
socketPath: '/tmp/events.sock'
|
||||||
});
|
});
|
||||||
|
|
||||||
const subscriber2 = SmartIpc.createClient({
|
await subscriber1.connect();
|
||||||
id: 'events-service',
|
|
||||||
clientId: 'subscriber-2'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Subscribe to topics
|
|
||||||
await subscriber1.subscribe('user.login', (data) => {
|
await subscriber1.subscribe('user.login', (data) => {
|
||||||
console.log('User logged in:', data);
|
console.log('User logged in:', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
await subscriber2.subscribe('user.*', (data) => {
|
// Publisher
|
||||||
console.log('User event:', data);
|
const publisher = SmartIpc.createClient({
|
||||||
|
id: 'events-service',
|
||||||
|
socketPath: '/tmp/events.sock'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Publish events
|
await publisher.connect();
|
||||||
await publisher.publish('user.login', {
|
await publisher.publish('user.login', {
|
||||||
userId: '123',
|
userId: '123',
|
||||||
|
ip: '192.168.1.1',
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Advanced Features
|
## 💪 Advanced Features
|
||||||
|
|
||||||
### 🔄 Auto-Reconnection with Exponential Backoff
|
### 🏁 Server Readiness Detection
|
||||||
|
|
||||||
Clients automatically reconnect on connection loss:
|
Eliminate race conditions in tests and production:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const server = SmartIpc.createServer({
|
||||||
|
id: 'my-service',
|
||||||
|
socketPath: '/tmp/my-service.sock',
|
||||||
|
autoCleanupSocketFile: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Option 1: Wait for full readiness
|
||||||
|
await server.start({ readyWhen: 'accepting' });
|
||||||
|
// Server is now FULLY ready to accept connections
|
||||||
|
|
||||||
|
// Option 2: Use ready event
|
||||||
|
server.on('ready', () => {
|
||||||
|
console.log('Server is ready!');
|
||||||
|
startClients();
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.start();
|
||||||
|
|
||||||
|
// Option 3: Check readiness state
|
||||||
|
if (server.getIsReady()) {
|
||||||
|
console.log('Ready to rock! 🎸');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔄 Smart Connection Retry
|
||||||
|
|
||||||
|
Never lose messages due to temporary connection issues:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const client = SmartIpc.createClient({
|
const client = SmartIpc.createClient({
|
||||||
id: 'resilient-service',
|
id: 'resilient-client',
|
||||||
clientId: 'auto-reconnect-client',
|
socketPath: '/tmp/service.sock',
|
||||||
reconnect: {
|
connectRetry: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
initialDelay: 1000, // Start with 1 second
|
initialDelay: 100, // Start with 100ms
|
||||||
maxDelay: 30000, // Cap at 30 seconds
|
maxDelay: 1500, // Cap at 1.5 seconds
|
||||||
factor: 2, // Double each time
|
maxAttempts: 20, // Try 20 times
|
||||||
maxAttempts: Infinity // Keep trying forever
|
totalTimeout: 15000 // Give up after 15 seconds total
|
||||||
}
|
},
|
||||||
|
registerTimeoutMs: 8000 // Registration handshake timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
// Monitor connection state
|
// Will retry automatically if server isn't ready yet
|
||||||
client.on('connected', () => console.log('Connected! 🟢'));
|
await client.connect({
|
||||||
client.on('disconnected', () => console.log('Connection lost! 🔴'));
|
waitForReady: true, // Wait for server to exist
|
||||||
client.on('reconnecting', (attempt) => console.log(`Reconnecting... Attempt ${attempt} 🟡`));
|
waitTimeout: 10000 // Wait up to 10 seconds
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### 💓 Heartbeat Monitoring
|
### 💓 Graceful Heartbeat Monitoring
|
||||||
|
|
||||||
Keep connections alive and detect failures quickly:
|
Keep connections alive without crashing on timeouts:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const server = SmartIpc.createServer({
|
const server = SmartIpc.createServer({
|
||||||
id: 'monitored-service',
|
id: 'monitored-service',
|
||||||
heartbeat: {
|
socketPath: '/tmp/monitored.sock',
|
||||||
enabled: true,
|
heartbeat: true,
|
||||||
interval: 5000, // Send heartbeat every 5 seconds
|
heartbeatInterval: 3000,
|
||||||
timeout: 15000 // Consider dead after 15 seconds
|
heartbeatTimeout: 10000,
|
||||||
}
|
heartbeatInitialGracePeriodMs: 5000, // Grace period for startup
|
||||||
|
heartbeatThrowOnTimeout: false // Emit event instead of throwing
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clients automatically respond to heartbeats
|
server.on('heartbeatTimeout', (clientId) => {
|
||||||
|
console.log(`Client ${clientId} heartbeat timeout - will handle gracefully`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client configuration
|
||||||
const client = SmartIpc.createClient({
|
const client = SmartIpc.createClient({
|
||||||
id: 'monitored-service',
|
id: 'monitored-service',
|
||||||
clientId: 'heartbeat-client',
|
socketPath: '/tmp/monitored.sock',
|
||||||
heartbeat: true // Enable heartbeat responses
|
heartbeat: true,
|
||||||
|
heartbeatInterval: 3000,
|
||||||
|
heartbeatTimeout: 10000,
|
||||||
|
heartbeatInitialGracePeriodMs: 5000,
|
||||||
|
heartbeatThrowOnTimeout: false
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('heartbeatTimeout', () => {
|
||||||
|
console.log('Heartbeat timeout detected, reconnecting...');
|
||||||
|
// Handle reconnection logic
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### 📊 Real-time Metrics & Observability
|
### 🧹 Automatic Socket Cleanup
|
||||||
|
|
||||||
Track performance and connection health:
|
Never worry about stale socket files:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Server metrics
|
const server = SmartIpc.createServer({
|
||||||
|
id: 'clean-service',
|
||||||
|
socketPath: '/tmp/service.sock',
|
||||||
|
autoCleanupSocketFile: true, // Remove stale socket on start
|
||||||
|
socketMode: 0o600 // Set socket permissions (Unix only)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Socket file will be cleaned up automatically on start
|
||||||
|
await server.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 Real-time Metrics
|
||||||
|
|
||||||
|
Monitor your IPC performance:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server stats
|
||||||
const serverStats = server.getStats();
|
const serverStats = server.getStats();
|
||||||
console.log({
|
console.log({
|
||||||
isRunning: serverStats.isRunning,
|
isRunning: serverStats.isRunning,
|
||||||
connectedClients: serverStats.connectedClients,
|
connectedClients: serverStats.connectedClients,
|
||||||
totalConnections: serverStats.totalConnections,
|
totalConnections: serverStats.totalConnections,
|
||||||
uptime: serverStats.uptime,
|
|
||||||
metrics: {
|
metrics: {
|
||||||
messagesSent: serverStats.metrics.messagesSent,
|
messagesSent: serverStats.metrics.messagesSent,
|
||||||
messagesReceived: serverStats.metrics.messagesReceived,
|
messagesReceived: serverStats.metrics.messagesReceived,
|
||||||
bytesSent: serverStats.metrics.bytesSent,
|
|
||||||
bytesReceived: serverStats.metrics.bytesReceived,
|
|
||||||
errors: serverStats.metrics.errors
|
errors: serverStats.metrics.errors
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Client metrics
|
// Client stats
|
||||||
const clientStats = client.getStats();
|
const clientStats = client.getStats();
|
||||||
console.log({
|
console.log({
|
||||||
connected: clientStats.connected,
|
connected: clientStats.connected,
|
||||||
reconnectAttempts: clientStats.reconnectAttempts,
|
reconnectAttempts: clientStats.reconnectAttempts,
|
||||||
lastActivity: clientStats.lastActivity,
|
|
||||||
metrics: clientStats.metrics
|
metrics: clientStats.metrics
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track specific clients on server
|
// Get specific client info
|
||||||
const clientInfo = server.getClientInfo('client-1');
|
const clientInfo = server.getClientInfo('client-123');
|
||||||
console.log({
|
console.log({
|
||||||
clientId: clientInfo.clientId,
|
connectedAt: new Date(clientInfo.connectedAt),
|
||||||
metadata: clientInfo.metadata,
|
lastActivity: new Date(clientInfo.lastActivity),
|
||||||
connectedAt: clientInfo.connectedAt,
|
metadata: clientInfo.metadata
|
||||||
lastActivity: clientInfo.lastActivity,
|
|
||||||
subscriptions: clientInfo.subscriptions
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🛡️ Security & Limits
|
### 🎯 Broadcasting
|
||||||
|
|
||||||
Protect against malicious or misbehaving clients:
|
Send messages to multiple clients:
|
||||||
|
|
||||||
```typescript
|
|
||||||
const secureServer = SmartIpc.createServer({
|
|
||||||
id: 'secure-service',
|
|
||||||
maxMessageSize: 10 * 1024 * 1024, // 10MB max message size
|
|
||||||
maxConnections: 100, // Limit concurrent connections
|
|
||||||
connectionTimeout: 60000, // Drop idle connections after 1 minute
|
|
||||||
|
|
||||||
// Authentication (coming soon)
|
|
||||||
auth: {
|
|
||||||
required: true,
|
|
||||||
validator: async (token) => {
|
|
||||||
// Validate auth token
|
|
||||||
return validateToken(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rate limiting per client
|
|
||||||
secureServer.use(rateLimitMiddleware({
|
|
||||||
windowMs: 60000, // 1 minute window
|
|
||||||
max: 100 // 100 requests per window
|
|
||||||
}));
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🎯 Broadcast to Specific Clients
|
|
||||||
|
|
||||||
Send targeted messages:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Broadcast to all connected clients
|
// Broadcast to all connected clients
|
||||||
server.broadcast('system-alert', {
|
await server.broadcast('announcement', {
|
||||||
message: 'Maintenance in 5 minutes'
|
message: 'Server will restart in 5 minutes',
|
||||||
|
severity: 'warning'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send to specific client
|
// Send to specific clients
|
||||||
server.sendToClient('client-1', 'personal-message', {
|
await server.broadcastTo(
|
||||||
content: 'This is just for you'
|
['client-1', 'client-2'],
|
||||||
});
|
'private-message',
|
||||||
|
{ content: 'This is just for you two' }
|
||||||
|
);
|
||||||
|
|
||||||
// Send to multiple specific clients
|
// Send to one client
|
||||||
server.sendToClients(['client-1', 'client-2'], 'group-message', {
|
await server.sendToClient('client-1', 'direct', {
|
||||||
content: 'Group notification'
|
data: 'Personal message'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get all connected client IDs
|
|
||||||
const clients = server.getConnectedClients();
|
|
||||||
console.log('Connected clients:', clients);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Error Handling
|
## 🧪 Testing Utilities
|
||||||
|
|
||||||
Comprehensive error handling with typed errors:
|
SmartIPC includes powerful helpers for testing:
|
||||||
|
|
||||||
|
### Wait for Server
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { IpcError, ConnectionError, TimeoutError } from '@push.rocks/smartipc';
|
import { SmartIpc } from '@push.rocks/smartipc';
|
||||||
|
|
||||||
// Client error handling
|
// Start your server in another process
|
||||||
|
const serverProcess = spawn('node', ['server.js']);
|
||||||
|
|
||||||
|
// Wait for it to be ready
|
||||||
|
await SmartIpc.waitForServer({
|
||||||
|
socketPath: '/tmp/test.sock',
|
||||||
|
timeoutMs: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now safe to connect clients
|
||||||
|
const client = SmartIpc.createClient({
|
||||||
|
id: 'test-client',
|
||||||
|
socketPath: '/tmp/test.sock'
|
||||||
|
});
|
||||||
|
await client.connect();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spawn and Connect
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Helper that spawns a server and connects a client
|
||||||
|
const { client, serverProcess } = await SmartIpc.spawnAndConnect({
|
||||||
|
serverScript: './server.js',
|
||||||
|
socketPath: '/tmp/test.sock',
|
||||||
|
clientId: 'test-client',
|
||||||
|
connectRetry: {
|
||||||
|
enabled: true,
|
||||||
|
maxAttempts: 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use the client
|
||||||
|
const response = await client.request('ping', {});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await client.disconnect();
|
||||||
|
serverProcess.kill();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎭 Event Handling
|
||||||
|
|
||||||
|
SmartIPC provides comprehensive event emitters:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server events
|
||||||
|
server.on('start', () => console.log('Server started'));
|
||||||
|
server.on('ready', () => console.log('Server ready for connections'));
|
||||||
|
server.on('clientConnect', (clientId, metadata) => {
|
||||||
|
console.log(`Client ${clientId} connected with metadata:`, metadata);
|
||||||
|
});
|
||||||
|
server.on('clientDisconnect', (clientId) => {
|
||||||
|
console.log(`Client ${clientId} disconnected`);
|
||||||
|
});
|
||||||
|
server.on('error', (error, clientId) => {
|
||||||
|
console.error(`Error from ${clientId}:`, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client events
|
||||||
|
client.on('connect', () => console.log('Connected to server'));
|
||||||
|
client.on('disconnect', () => console.log('Disconnected from server'));
|
||||||
|
client.on('reconnecting', (attempt) => {
|
||||||
|
console.log(`Reconnection attempt ${attempt}`);
|
||||||
|
});
|
||||||
client.on('error', (error) => {
|
client.on('error', (error) => {
|
||||||
if (error instanceof ConnectionError) {
|
console.error('Client error:', error);
|
||||||
console.error('Connection failed:', error.message);
|
});
|
||||||
} else if (error instanceof TimeoutError) {
|
client.on('heartbeatTimeout', (error) => {
|
||||||
console.error('Request timed out:', error.message);
|
console.warn('Heartbeat timeout:', error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛡️ Error Handling
|
||||||
|
|
||||||
|
Robust error handling with detailed error information:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Client-side error handling
|
||||||
|
try {
|
||||||
|
const response = await client.request('riskyOperation', data, {
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.includes('timeout')) {
|
||||||
|
console.error('Request timed out');
|
||||||
|
} else if (error.message.includes('Failed to register')) {
|
||||||
|
console.error('Could not register with server');
|
||||||
} else {
|
} else {
|
||||||
console.error('Unknown error:', error);
|
console.error('Unknown error:', error);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Server error handling
|
|
||||||
server.on('client-error', (clientId, error) => {
|
|
||||||
console.error(`Client ${clientId} error:`, error);
|
|
||||||
|
|
||||||
// Optionally disconnect misbehaving clients
|
|
||||||
if (error.code === 'INVALID_MESSAGE') {
|
|
||||||
server.disconnectClient(clientId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Request with error handling
|
|
||||||
try {
|
|
||||||
const response = await client.request('risky-operation', data, {
|
|
||||||
timeout: 5000,
|
|
||||||
retries: 3
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof TimeoutError) {
|
|
||||||
// Handle timeout
|
|
||||||
} else {
|
|
||||||
// Handle other errors
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
// Server-side error boundaries
|
||||||
|
server.onMessage('process', async (data, clientId) => {
|
||||||
SmartIPC includes comprehensive testing utilities:
|
try {
|
||||||
|
return await riskyProcessing(data);
|
||||||
```typescript
|
} catch (error) {
|
||||||
import { createTestServer, createTestClient } from '@push.rocks/smartipc/testing';
|
console.error(`Processing failed for ${clientId}:`, error);
|
||||||
|
throw error; // Will be sent back to client as error
|
||||||
describe('My IPC integration', () => {
|
}
|
||||||
let server, client;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
server = await createTestServer();
|
|
||||||
client = await createTestClient(server);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await client.disconnect();
|
|
||||||
await server.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle messages', async () => {
|
|
||||||
server.onMessage('test', (data) => ({ echo: data }));
|
|
||||||
|
|
||||||
const response = await client.request('test', { value: 42 });
|
|
||||||
expect(response.echo.value).toBe(42);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Performance Benchmarks
|
## 🏗️ Architecture
|
||||||
|
|
||||||
SmartIPC has been optimized for high throughput and low latency:
|
SmartIPC uses a clean, layered architecture:
|
||||||
|
|
||||||
| Transport | Messages/sec | Avg Latency | Use Case |
|
|
||||||
|-----------|-------------|-------------|----------|
|
|
||||||
| Unix Socket | 150,000+ | < 0.1ms | Local high-performance IPC |
|
|
||||||
| TCP (localhost) | 100,000+ | < 0.2ms | Local network-capable IPC |
|
|
||||||
| TCP (network) | 50,000+ | < 1ms | Distributed systems |
|
|
||||||
| Named Pipe | 120,000+ | < 0.15ms | Windows local IPC |
|
|
||||||
|
|
||||||
*Benchmarked on modern hardware with 1KB message payloads*
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
SmartIPC uses a layered architecture for maximum flexibility:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────┐
|
||||||
│ Application Layer │
|
│ Your Application │
|
||||||
│ (Your business logic and handlers) │
|
│ (Business logic) │
|
||||||
└─────────────────────────────────────────┘
|
└─────────────────────────────────────────┘
|
||||||
↕
|
↕
|
||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────┐
|
||||||
│ IPC Client / Server │
|
│ IpcServer / IpcClient │
|
||||||
│ (High-level API, patterns, routing) │
|
│ (High-level API, Message routing) │
|
||||||
└─────────────────────────────────────────┘
|
└─────────────────────────────────────────┘
|
||||||
↕
|
↕
|
||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────┐
|
||||||
│ IPC Channel │
|
│ IpcChannel │
|
||||||
│ (Connection management, reconnection, │
|
│ (Connection management, Heartbeat, │
|
||||||
│ heartbeat, request/response) │
|
│ Reconnection, Request/Response) │
|
||||||
└─────────────────────────────────────────┘
|
└─────────────────────────────────────────┘
|
||||||
↕
|
↕
|
||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────┐
|
||||||
@@ -435,25 +488,94 @@ SmartIPC uses a layered architecture for maximum flexibility:
|
|||||||
└─────────────────────────────────────────┘
|
└─────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
## Comparison with Alternatives
|
## 🎯 Common Use Cases
|
||||||
|
|
||||||
| Feature | SmartIPC | node-ipc | zeromq | |
|
### Microservices Communication
|
||||||
|---------|----------|----------|---------|--|
|
```typescript
|
||||||
| Zero Dependencies | ✅ | ❌ | ❌ | |
|
// API Gateway
|
||||||
| TypeScript Native | ✅ | ❌ | ❌ | |
|
const gateway = SmartIpc.createServer({
|
||||||
| Auto-Reconnect | ✅ | ⚠️ | ✅ | |
|
id: 'api-gateway',
|
||||||
| Request/Response | ✅ | ⚠️ | ✅ | |
|
socketPath: '/tmp/gateway.sock'
|
||||||
| Pub/Sub | ✅ | ❌ | ✅ | |
|
});
|
||||||
| Built-in Metrics | ✅ | ❌ | ❌ | |
|
|
||||||
| Heartbeat | ✅ | ❌ | ✅ | |
|
|
||||||
| Message Size Limits | ✅ | ❌ | ✅ | |
|
|
||||||
| Type Safety | ✅ | ❌ | ❌ | |
|
|
||||||
|
|
||||||
## Support
|
// User Service
|
||||||
|
const userService = SmartIpc.createClient({
|
||||||
|
id: 'api-gateway',
|
||||||
|
socketPath: '/tmp/gateway.sock',
|
||||||
|
clientId: 'user-service'
|
||||||
|
});
|
||||||
|
|
||||||
- 📖 [Documentation](https://code.foss.global/push.rocks/smartipc)
|
// Order Service
|
||||||
- 🐛 [Issue Tracker](https://code.foss.global/push.rocks/smartipc/issues)
|
const orderService = SmartIpc.createClient({
|
||||||
- 💬 [Discussions](https://code.foss.global/push.rocks/smartipc/discussions)
|
id: 'api-gateway',
|
||||||
|
socketPath: '/tmp/gateway.sock',
|
||||||
|
clientId: 'order-service'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Worker Process Management
|
||||||
|
```typescript
|
||||||
|
// Main process
|
||||||
|
const server = SmartIpc.createServer({
|
||||||
|
id: 'main',
|
||||||
|
socketPath: '/tmp/workers.sock'
|
||||||
|
});
|
||||||
|
|
||||||
|
server.onMessage('job-complete', (result, workerId) => {
|
||||||
|
console.log(`Worker ${workerId} completed job:`, result);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Worker process
|
||||||
|
const worker = SmartIpc.createClient({
|
||||||
|
id: 'main',
|
||||||
|
socketPath: '/tmp/workers.sock',
|
||||||
|
clientId: `worker-${process.pid}`
|
||||||
|
});
|
||||||
|
|
||||||
|
await worker.sendMessage('job-complete', {
|
||||||
|
jobId: '123',
|
||||||
|
result: processedData
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Real-time Event Distribution
|
||||||
|
```typescript
|
||||||
|
// Event bus
|
||||||
|
const eventBus = SmartIpc.createServer({
|
||||||
|
id: 'event-bus',
|
||||||
|
socketPath: '/tmp/events.sock'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Services subscribe to events
|
||||||
|
const analyticsService = SmartIpc.createClient({
|
||||||
|
id: 'event-bus',
|
||||||
|
socketPath: '/tmp/events.sock'
|
||||||
|
});
|
||||||
|
|
||||||
|
await analyticsService.subscribe('user.*', (event) => {
|
||||||
|
trackEvent(event);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 Performance
|
||||||
|
|
||||||
|
SmartIPC is optimized for high throughput and low latency:
|
||||||
|
|
||||||
|
| Transport | Messages/sec | Avg Latency | Use Case |
|
||||||
|
|-----------|-------------|-------------|----------|
|
||||||
|
| Unix Socket | 150,000+ | < 0.1ms | Local high-performance IPC (Linux/macOS) |
|
||||||
|
| Named Pipe | 120,000+ | < 0.15ms | Windows local IPC |
|
||||||
|
| TCP (localhost) | 100,000+ | < 0.2ms | Local network-capable IPC |
|
||||||
|
| TCP (network) | 50,000+ | < 1ms | Distributed systems |
|
||||||
|
|
||||||
|
- **Memory efficient**: Streaming support for large payloads
|
||||||
|
- **CPU efficient**: Event-driven, non-blocking I/O
|
||||||
|
|
||||||
|
## 🔧 Requirements
|
||||||
|
|
||||||
|
- Node.js >= 14.x
|
||||||
|
- TypeScript >= 4.x (for development)
|
||||||
|
- Unix-like OS (Linux, macOS) or Windows
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
@@ -473,7 +595,3 @@ Registered at District court Bremen HRB 35230 HB, Germany
|
|||||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Built with ❤️ by Task Venture Capital GmbH**
|
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartipc',
|
name: '@push.rocks/smartipc',
|
||||||
version: '2.1.0',
|
version: '2.1.3',
|
||||||
description: 'A library for node inter process communication, providing an easy-to-use API for IPC.'
|
description: 'A library for node inter process communication, providing an easy-to-use API for IPC.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export class IpcChannel<TRequest = any, TResponse = any> extends plugins.EventEm
|
|||||||
private reconnectTimer?: NodeJS.Timeout;
|
private reconnectTimer?: NodeJS.Timeout;
|
||||||
private heartbeatTimer?: NodeJS.Timeout;
|
private heartbeatTimer?: NodeJS.Timeout;
|
||||||
private heartbeatCheckTimer?: NodeJS.Timeout;
|
private heartbeatCheckTimer?: NodeJS.Timeout;
|
||||||
|
private heartbeatGraceTimer?: NodeJS.Timeout;
|
||||||
private lastHeartbeat: number = Date.now();
|
private lastHeartbeat: number = Date.now();
|
||||||
private connectionStartTime: number = Date.now();
|
private connectionStartTime: number = Date.now();
|
||||||
private isReconnecting = false;
|
private isReconnecting = false;
|
||||||
@@ -81,6 +82,18 @@ export class IpcChannel<TRequest = any, TResponse = any> extends plugins.EventEm
|
|||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Normalize heartbeatThrowOnTimeout to boolean (defensive for JS consumers)
|
||||||
|
const throwOnTimeout = (this.options as any).heartbeatThrowOnTimeout;
|
||||||
|
if (throwOnTimeout !== undefined) {
|
||||||
|
if (throwOnTimeout === 'false') {
|
||||||
|
this.options.heartbeatThrowOnTimeout = false;
|
||||||
|
} else if (throwOnTimeout === 'true') {
|
||||||
|
this.options.heartbeatThrowOnTimeout = true;
|
||||||
|
} else if (typeof throwOnTimeout !== 'boolean') {
|
||||||
|
this.options.heartbeatThrowOnTimeout = Boolean(throwOnTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.transport = createTransport(this.options);
|
this.transport = createTransport(this.options);
|
||||||
this.setupTransportHandlers();
|
this.setupTransportHandlers();
|
||||||
}
|
}
|
||||||
@@ -217,16 +230,27 @@ export class IpcChannel<TRequest = any, TResponse = any> extends plugins.EventEm
|
|||||||
});
|
});
|
||||||
}, this.options.heartbeatInterval!);
|
}, this.options.heartbeatInterval!);
|
||||||
|
|
||||||
|
// Delay starting the check until after the grace period
|
||||||
|
const gracePeriod = this.options.heartbeatInitialGracePeriodMs || 0;
|
||||||
|
|
||||||
|
if (gracePeriod > 0) {
|
||||||
|
// Use a timer to delay the first check
|
||||||
|
this.heartbeatGraceTimer = setTimeout(() => {
|
||||||
|
this.startHeartbeatCheck();
|
||||||
|
}, gracePeriod);
|
||||||
|
} else {
|
||||||
|
// No grace period, start checking immediately
|
||||||
|
this.startHeartbeatCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start heartbeat timeout checking (separated for grace period handling)
|
||||||
|
*/
|
||||||
|
private startHeartbeatCheck(): void {
|
||||||
// Check for heartbeat timeout
|
// Check for heartbeat timeout
|
||||||
this.heartbeatCheckTimer = setInterval(() => {
|
this.heartbeatCheckTimer = setInterval(() => {
|
||||||
const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeat;
|
const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeat;
|
||||||
const timeSinceConnection = Date.now() - this.connectionStartTime;
|
|
||||||
const gracePeriod = this.options.heartbeatInitialGracePeriodMs || 0;
|
|
||||||
|
|
||||||
// Skip timeout check during initial grace period
|
|
||||||
if (timeSinceConnection < gracePeriod) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeSinceLastHeartbeat > this.options.heartbeatTimeout!) {
|
if (timeSinceLastHeartbeat > this.options.heartbeatTimeout!) {
|
||||||
const error = new Error('Heartbeat timeout');
|
const error = new Error('Heartbeat timeout');
|
||||||
@@ -238,9 +262,11 @@ export class IpcChannel<TRequest = any, TResponse = any> extends plugins.EventEm
|
|||||||
} else {
|
} else {
|
||||||
// Emit heartbeatTimeout event instead of error
|
// Emit heartbeatTimeout event instead of error
|
||||||
this.emit('heartbeatTimeout', error);
|
this.emit('heartbeatTimeout', error);
|
||||||
|
// Clear timers to avoid repeated events
|
||||||
|
this.stopHeartbeat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, this.options.heartbeatTimeout! / 2);
|
}, Math.max(1000, Math.floor(this.options.heartbeatTimeout! / 2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -256,6 +282,11 @@ export class IpcChannel<TRequest = any, TResponse = any> extends plugins.EventEm
|
|||||||
clearInterval(this.heartbeatCheckTimer);
|
clearInterval(this.heartbeatCheckTimer);
|
||||||
this.heartbeatCheckTimer = undefined;
|
this.heartbeatCheckTimer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.heartbeatGraceTimer) {
|
||||||
|
clearTimeout(this.heartbeatGraceTimer);
|
||||||
|
this.heartbeatGraceTimer = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -430,7 +461,7 @@ export class IpcChannel<TRequest = any, TResponse = any> extends plugins.EventEm
|
|||||||
* Register a message handler
|
* Register a message handler
|
||||||
*/
|
*/
|
||||||
public on(event: string, handler: (payload: any) => any | Promise<any>): this {
|
public on(event: string, handler: (payload: any) => any | Promise<any>): this {
|
||||||
if (event === 'message' || event === 'connect' || event === 'disconnect' || event === 'error' || event === 'reconnecting' || event === 'drain') {
|
if (event === 'message' || event === 'connect' || event === 'disconnect' || event === 'error' || event === 'reconnecting' || event === 'drain' || event === 'heartbeatTimeout') {
|
||||||
// Special handling for channel events
|
// Special handling for channel events
|
||||||
super.on(event, handler);
|
super.on(event, handler);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ export class IpcClient extends plugins.EventEmitter {
|
|||||||
clientId: this.clientId,
|
clientId: this.clientId,
|
||||||
metadata: this.options.metadata
|
metadata: this.options.metadata
|
||||||
},
|
},
|
||||||
{ timeout: registerTimeoutMs }
|
{
|
||||||
|
timeout: registerTimeoutMs,
|
||||||
|
headers: { clientId: this.clientId } // Include clientId in headers for proper routing
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
@@ -194,10 +197,20 @@ export class IpcClient extends plugins.EventEmitter {
|
|||||||
this.emit('disconnect', reason);
|
this.emit('disconnect', reason);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.channel.on('error', (error) => {
|
this.channel.on('error', (error: any) => {
|
||||||
|
// If heartbeat timeout and configured not to throw, convert to heartbeatTimeout event
|
||||||
|
if (error && error.message === 'Heartbeat timeout' && this.options.heartbeatThrowOnTimeout === false) {
|
||||||
|
this.emit('heartbeatTimeout', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.emit('error', error);
|
this.emit('error', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.channel.on('heartbeatTimeout', (error) => {
|
||||||
|
// Forward heartbeatTimeout event (when heartbeatThrowOnTimeout is false)
|
||||||
|
this.emit('heartbeatTimeout', error);
|
||||||
|
});
|
||||||
|
|
||||||
this.channel.on('reconnecting', (info) => {
|
this.channel.on('reconnecting', (info) => {
|
||||||
this.emit('reconnecting', info);
|
this.emit('reconnecting', info);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -200,6 +200,11 @@ export class IpcServer extends plugins.EventEmitter {
|
|||||||
this.emit('error', error, 'server');
|
this.emit('error', error, 'server');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.primaryChannel.on('heartbeatTimeout', (error) => {
|
||||||
|
// Forward heartbeatTimeout event (when heartbeatThrowOnTimeout is false)
|
||||||
|
this.emit('heartbeatTimeout', error, 'server');
|
||||||
|
});
|
||||||
|
|
||||||
// Connect the primary channel (will start as server)
|
// Connect the primary channel (will start as server)
|
||||||
await this.primaryChannel.connect();
|
await this.primaryChannel.connect();
|
||||||
|
|
||||||
@@ -339,6 +344,19 @@ export class IpcServer extends plugins.EventEmitter {
|
|||||||
}
|
}
|
||||||
this.emit('error', error, actualClientId);
|
this.emit('error', error, actualClientId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
channel.on('heartbeatTimeout', (error) => {
|
||||||
|
// Find the actual client ID for this channel
|
||||||
|
let actualClientId = clientId;
|
||||||
|
for (const [id, client] of this.clients) {
|
||||||
|
if (client.channel === channel) {
|
||||||
|
actualClientId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Forward heartbeatTimeout event (when heartbeatThrowOnTimeout is false)
|
||||||
|
this.emit('heartbeatTimeout', error, actualClientId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ export class UnixSocketTransport extends IpcTransport {
|
|||||||
private socket: plugins.net.Socket | null = null;
|
private socket: plugins.net.Socket | null = null;
|
||||||
private server: plugins.net.Server | null = null;
|
private server: plugins.net.Server | null = null;
|
||||||
private clients: Set<plugins.net.Socket> = new Set();
|
private clients: Set<plugins.net.Socket> = new Set();
|
||||||
|
private socketToClientId = new WeakMap<plugins.net.Socket, string>();
|
||||||
|
private clientIdToSocket = new Map<string, plugins.net.Socket>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect as client or start as server
|
* Connect as client or start as server
|
||||||
@@ -239,6 +241,12 @@ export class UnixSocketTransport extends IpcTransport {
|
|||||||
|
|
||||||
socket.on('close', () => {
|
socket.on('close', () => {
|
||||||
this.clients.delete(socket);
|
this.clients.delete(socket);
|
||||||
|
// Clean up clientId mappings
|
||||||
|
const clientId = this.socketToClientId.get(socket);
|
||||||
|
if (clientId && this.clientIdToSocket.get(clientId) === socket) {
|
||||||
|
this.clientIdToSocket.delete(clientId);
|
||||||
|
}
|
||||||
|
this.socketToClientId.delete(socket);
|
||||||
this.emit('clientDisconnected', socket);
|
this.emit('clientDisconnected', socket);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -307,7 +315,18 @@ export class UnixSocketTransport extends IpcTransport {
|
|||||||
// Parse and emit the message with socket reference
|
// Parse and emit the message with socket reference
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(messageData.toString('utf8')) as IIpcMessageEnvelope;
|
const message = JSON.parse(messageData.toString('utf8')) as IIpcMessageEnvelope;
|
||||||
|
|
||||||
|
// Update clientId mapping
|
||||||
|
const clientId = message.headers?.clientId ??
|
||||||
|
(message.type === '__register__' ? (message.payload as any)?.clientId : undefined);
|
||||||
|
if (clientId) {
|
||||||
|
this.socketToClientId.set(socket, clientId);
|
||||||
|
this.clientIdToSocket.set(clientId, socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit both events so IpcChannel can process it
|
||||||
this.emit('clientMessage', message, socket);
|
this.emit('clientMessage', message, socket);
|
||||||
|
this.emit('message', message);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.emit('error', new Error(`Failed to parse message: ${error.message}`));
|
this.emit('error', new Error(`Failed to parse message: ${error.message}`));
|
||||||
}
|
}
|
||||||
@@ -415,7 +434,33 @@ export class UnixSocketTransport extends IpcTransport {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (this.server && this.clients.size > 0) {
|
} else if (this.server && this.clients.size > 0) {
|
||||||
// Server mode - broadcast to all clients
|
// Server mode - route by clientId if present, otherwise broadcast
|
||||||
|
const targetClientId = message.headers?.clientId;
|
||||||
|
|
||||||
|
if (targetClientId && this.clientIdToSocket.has(targetClientId)) {
|
||||||
|
// Send to specific client
|
||||||
|
const targetSocket = this.clientIdToSocket.get(targetClientId)!;
|
||||||
|
if (targetSocket && !targetSocket.destroyed) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const success = targetSocket.write(frame, (error) => {
|
||||||
|
if (error) {
|
||||||
|
resolve(false);
|
||||||
|
} else {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
targetSocket.once('drain', () => resolve(true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Socket is destroyed, remove from mappings
|
||||||
|
this.clientIdToSocket.delete(targetClientId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Broadcast to all clients (fallback for messages without specific target)
|
||||||
const promises: Promise<boolean>[] = [];
|
const promises: Promise<boolean>[] = [];
|
||||||
|
|
||||||
for (const client of this.clients) {
|
for (const client of this.clients) {
|
||||||
@@ -437,6 +482,7 @@ export class UnixSocketTransport extends IpcTransport {
|
|||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
return results.every(r => r);
|
return results.every(r => r);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
21
ts/index.ts
21
ts/index.ts
@@ -29,20 +29,27 @@ export class SmartIpc {
|
|||||||
|
|
||||||
while (Date.now() - startTime < timeout) {
|
while (Date.now() - startTime < timeout) {
|
||||||
try {
|
try {
|
||||||
// Try to connect as a temporary client
|
// Create a temporary client with proper options
|
||||||
const testClient = new IpcClient({
|
const testClient = SmartIpc.createClient({
|
||||||
id: `test-probe-${Date.now()}`,
|
id: 'test-probe',
|
||||||
socketPath: options.socketPath,
|
socketPath: options.socketPath,
|
||||||
autoReconnect: false,
|
clientId: `probe-${process.pid}-${Date.now()}`,
|
||||||
heartbeat: false
|
heartbeat: false,
|
||||||
|
connectRetry: {
|
||||||
|
enabled: false // Don't retry, we're handling retries here
|
||||||
|
},
|
||||||
|
registerTimeoutMs: 2000 // Short timeout for quick probing
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Try to connect and register with the server
|
||||||
await testClient.connect();
|
await testClient.connect();
|
||||||
|
|
||||||
|
// Success! Clean up and return
|
||||||
await testClient.disconnect();
|
await testClient.disconnect();
|
||||||
return; // Server is ready
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Server not ready yet, wait and retry
|
// Server not ready yet, wait and retry
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user