BREAKING CHANGE(core): Refactor core IPC: replace node-ipc with native transports and add IpcChannel / IpcServer / IpcClient with heartbeat, reconnection, request/response and pub/sub. Update tests and documentation.
This commit is contained in:
119
test/test.simple.ts
Normal file
119
test/test.simple.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartipc from '../ts/index.js';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
|
||||
let server: smartipc.IpcServer;
|
||||
let client: smartipc.IpcClient;
|
||||
|
||||
// Test TCP transport which is simpler
|
||||
tap.test('should create and start a TCP IPC server', async () => {
|
||||
server = smartipc.SmartIpc.createServer({
|
||||
id: 'tcp-test-server',
|
||||
host: 'localhost',
|
||||
port: 18765,
|
||||
heartbeat: false // Disable heartbeat for simpler testing
|
||||
});
|
||||
|
||||
await server.start();
|
||||
expect(server.getStats().isRunning).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should create and connect a TCP client', async () => {
|
||||
client = smartipc.SmartIpc.createClient({
|
||||
id: 'tcp-test-server',
|
||||
host: 'localhost',
|
||||
port: 18765,
|
||||
clientId: 'test-client-1',
|
||||
metadata: { name: 'Test Client' },
|
||||
heartbeat: false
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
expect(client.getIsConnected()).toBeTrue();
|
||||
expect(client.getClientId()).toEqual('test-client-1');
|
||||
});
|
||||
|
||||
tap.test('should send messages between server and client', async () => {
|
||||
const messageReceived = smartpromise.defer();
|
||||
|
||||
// Server listens for messages
|
||||
server.onMessage('test-message', (payload, clientId) => {
|
||||
expect(payload).toEqual({ data: 'Hello Server' });
|
||||
expect(clientId).toEqual('test-client-1');
|
||||
messageReceived.resolve();
|
||||
});
|
||||
|
||||
// Client sends message
|
||||
await client.sendMessage('test-message', { data: 'Hello Server' });
|
||||
|
||||
await messageReceived.promise;
|
||||
});
|
||||
|
||||
tap.test('should handle request/response pattern', async () => {
|
||||
// Server handles requests
|
||||
server.onMessage('add', async (payload: {a: number, b: number}, clientId) => {
|
||||
return { result: payload.a + payload.b };
|
||||
});
|
||||
|
||||
// Client makes request
|
||||
const response = await client.request<{a: number, b: number}, {result: number}>(
|
||||
'add',
|
||||
{ a: 5, b: 3 },
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
expect(response.result).toEqual(8);
|
||||
});
|
||||
|
||||
tap.test('should handle pub/sub pattern', async () => {
|
||||
// Create a second client
|
||||
const client2 = smartipc.SmartIpc.createClient({
|
||||
id: 'tcp-test-server',
|
||||
host: 'localhost',
|
||||
port: 18765,
|
||||
clientId: 'test-client-2',
|
||||
metadata: { name: 'Test Client 2' },
|
||||
heartbeat: false
|
||||
});
|
||||
|
||||
await client2.connect();
|
||||
|
||||
const messageReceived = smartpromise.defer();
|
||||
|
||||
// Client 1 subscribes to a topic
|
||||
await client.subscribe('news', (payload) => {
|
||||
expect(payload).toEqual({ headline: 'Breaking news!' });
|
||||
messageReceived.resolve();
|
||||
});
|
||||
|
||||
// Give server time to process subscription
|
||||
await smartdelay.delayFor(100);
|
||||
|
||||
// Client 2 publishes to the topic
|
||||
await client2.publish('news', { headline: 'Breaking news!' });
|
||||
|
||||
await messageReceived.promise;
|
||||
|
||||
await client2.disconnect();
|
||||
});
|
||||
|
||||
tap.test('should track metrics correctly', async () => {
|
||||
const stats = client.getStats();
|
||||
|
||||
expect(stats.connected).toBeTrue();
|
||||
expect(stats.metrics.messagesSent).toBeGreaterThan(0);
|
||||
expect(stats.metrics.messagesReceived).toBeGreaterThan(0);
|
||||
expect(stats.metrics.bytesSent).toBeGreaterThan(0);
|
||||
expect(stats.metrics.bytesReceived).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('should cleanup and close connections', async () => {
|
||||
await client.disconnect();
|
||||
await server.stop();
|
||||
|
||||
expect(server.getStats().isRunning).toBeFalse();
|
||||
expect(client.getIsConnected()).toBeFalse();
|
||||
});
|
||||
|
||||
export default tap.start();
|
314
test/test.ts
314
test/test.ts
@@ -1,41 +1,299 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartipc from '../ts/index';
|
||||
|
||||
import * as smartspawn from '@push.rocks/smartspawn';
|
||||
import * as smartipc from '../ts/index.js';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
|
||||
let serverIpc: smartipc.SmartIpc;
|
||||
let clientIpc: smartipc.SmartIpc;
|
||||
let server: smartipc.IpcServer;
|
||||
let client1: smartipc.IpcClient;
|
||||
let client2: smartipc.IpcClient;
|
||||
|
||||
tap.test('should instantiate a valid instance', async () => {
|
||||
serverIpc = new smartipc.SmartIpc({
|
||||
ipcSpace: 'testSmartIpc',
|
||||
type: 'server',
|
||||
// Test basic server creation and startup
|
||||
tap.test('should create and start an IPC server', async () => {
|
||||
server = smartipc.SmartIpc.createServer({
|
||||
id: 'test-server',
|
||||
socketPath: '/tmp/test-smartipc.sock',
|
||||
heartbeat: true,
|
||||
heartbeatInterval: 2000
|
||||
});
|
||||
serverIpc.registerHandler({
|
||||
keyword: 'hi',
|
||||
handlerFunc: (data) => {
|
||||
console.log(data);
|
||||
},
|
||||
});
|
||||
await serverIpc.start();
|
||||
|
||||
await server.start();
|
||||
expect(server.getStats().isRunning).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should create a client', async (tools) => {
|
||||
clientIpc = new smartipc.SmartIpc({
|
||||
ipcSpace: 'testSmartIpc',
|
||||
type: 'client',
|
||||
// Test client connection
|
||||
tap.test('should create and connect a client', async () => {
|
||||
client1 = smartipc.SmartIpc.createClient({
|
||||
id: 'test-server',
|
||||
socketPath: '/tmp/test-smartipc.sock',
|
||||
clientId: 'client-1',
|
||||
metadata: { name: 'Test Client 1' },
|
||||
autoReconnect: true,
|
||||
heartbeat: true
|
||||
});
|
||||
await clientIpc.start();
|
||||
clientIpc.sendMessage('hi', { awesome: 'yes' });
|
||||
|
||||
await client1.connect();
|
||||
expect(client1.getIsConnected()).toBeTrue();
|
||||
expect(client1.getClientId()).toEqual('client-1');
|
||||
});
|
||||
|
||||
tap.test('should terminate the smartipc process', async (tools) => {
|
||||
await clientIpc.stop();
|
||||
await serverIpc.stop();
|
||||
tools.delayFor(2000).then(() => {
|
||||
process.exit(0);
|
||||
// Test message sending
|
||||
tap.test('should send messages between server and client', async () => {
|
||||
const messageReceived = smartpromise.defer();
|
||||
|
||||
// Server listens for messages
|
||||
server.onMessage('test-message', (payload, clientId) => {
|
||||
expect(payload).toEqual({ data: 'Hello Server' });
|
||||
expect(clientId).toEqual('client-1');
|
||||
messageReceived.resolve();
|
||||
});
|
||||
|
||||
// Client sends message
|
||||
await client1.sendMessage('test-message', { data: 'Hello Server' });
|
||||
|
||||
await messageReceived.promise;
|
||||
});
|
||||
|
||||
tap.start();
|
||||
// Test request/response pattern
|
||||
tap.test('should handle request/response pattern', async () => {
|
||||
// Server handles requests
|
||||
server.onMessage('calculate', async (payload, clientId) => {
|
||||
expect(payload).toHaveProperty('a');
|
||||
expect(payload).toHaveProperty('b');
|
||||
return { result: payload.a + payload.b };
|
||||
});
|
||||
|
||||
// Client makes request
|
||||
const response = await client1.request<{a: number, b: number}, {result: number}>(
|
||||
'calculate',
|
||||
{ a: 5, b: 3 },
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
expect(response.result).toEqual(8);
|
||||
});
|
||||
|
||||
// Test multiple clients
|
||||
tap.test('should handle multiple clients', async () => {
|
||||
client2 = smartipc.SmartIpc.createClient({
|
||||
id: 'test-server',
|
||||
socketPath: '/tmp/test-smartipc.sock',
|
||||
clientId: 'client-2',
|
||||
metadata: { name: 'Test Client 2' }
|
||||
});
|
||||
|
||||
await client2.connect();
|
||||
expect(client2.getIsConnected()).toBeTrue();
|
||||
|
||||
const clientIds = server.getClientIds();
|
||||
expect(clientIds).toContain('client-1');
|
||||
expect(clientIds).toContain('client-2');
|
||||
expect(clientIds.length).toEqual(2);
|
||||
});
|
||||
|
||||
// Test broadcasting
|
||||
tap.test('should broadcast messages to all clients', async () => {
|
||||
const client1Received = smartpromise.defer();
|
||||
const client2Received = smartpromise.defer();
|
||||
|
||||
client1.onMessage('broadcast-test', (payload) => {
|
||||
expect(payload).toEqual({ announcement: 'Hello everyone!' });
|
||||
client1Received.resolve();
|
||||
});
|
||||
|
||||
client2.onMessage('broadcast-test', (payload) => {
|
||||
expect(payload).toEqual({ announcement: 'Hello everyone!' });
|
||||
client2Received.resolve();
|
||||
});
|
||||
|
||||
await server.broadcast('broadcast-test', { announcement: 'Hello everyone!' });
|
||||
|
||||
await Promise.all([client1Received.promise, client2Received.promise]);
|
||||
});
|
||||
|
||||
// Test selective broadcasting
|
||||
tap.test('should broadcast to specific clients based on filter', async () => {
|
||||
const client1Received = smartpromise.defer<boolean>();
|
||||
const client2Received = smartpromise.defer<boolean>();
|
||||
|
||||
client1.onMessage('selective-broadcast', () => {
|
||||
client1Received.resolve(true);
|
||||
});
|
||||
|
||||
client2.onMessage('selective-broadcast', () => {
|
||||
client2Received.resolve(true);
|
||||
});
|
||||
|
||||
// Only broadcast to client-1
|
||||
await server.broadcastTo(
|
||||
(clientId) => clientId === 'client-1',
|
||||
'selective-broadcast',
|
||||
{ data: 'Only for client-1' }
|
||||
);
|
||||
|
||||
// Wait a bit to ensure client2 doesn't receive it
|
||||
await smartdelay.delayFor(500);
|
||||
|
||||
expect(await Promise.race([
|
||||
client1Received.promise,
|
||||
smartdelay.delayFor(100).then(() => false)
|
||||
])).toBeTrue();
|
||||
|
||||
expect(await Promise.race([
|
||||
client2Received.promise,
|
||||
smartdelay.delayFor(100).then(() => false)
|
||||
])).toBeFalse();
|
||||
});
|
||||
|
||||
// Test pub/sub pattern
|
||||
tap.test('should handle pub/sub pattern', async () => {
|
||||
const messageReceived = smartpromise.defer();
|
||||
|
||||
// Client 1 subscribes to a topic
|
||||
await client1.subscribe('news', (payload) => {
|
||||
expect(payload).toEqual({ headline: 'Breaking news!' });
|
||||
messageReceived.resolve();
|
||||
});
|
||||
|
||||
// Server handles the subscription
|
||||
server.onMessage('__subscribe__', async (payload, clientId) => {
|
||||
expect(payload.topic).toEqual('news');
|
||||
});
|
||||
|
||||
// Server handles publishing
|
||||
server.onMessage('__publish__', async (payload, clientId) => {
|
||||
// Broadcast to all subscribers of the topic
|
||||
await server.broadcast(`topic:${payload.topic}`, payload.payload);
|
||||
});
|
||||
|
||||
// Client 2 publishes to the topic
|
||||
await client2.publish('news', { headline: 'Breaking news!' });
|
||||
|
||||
await messageReceived.promise;
|
||||
});
|
||||
|
||||
// Test error handling
|
||||
tap.test('should handle errors gracefully', async () => {
|
||||
const errorReceived = smartpromise.defer();
|
||||
|
||||
server.on('error', (error, clientId) => {
|
||||
errorReceived.resolve();
|
||||
});
|
||||
|
||||
// Try to send to non-existent client
|
||||
try {
|
||||
await server.sendToClient('non-existent', 'test', {});
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('not found');
|
||||
}
|
||||
});
|
||||
|
||||
// Test client disconnection
|
||||
tap.test('should handle client disconnection', async () => {
|
||||
const disconnectReceived = smartpromise.defer();
|
||||
|
||||
server.on('clientDisconnect', (clientId) => {
|
||||
if (clientId === 'client-2') {
|
||||
disconnectReceived.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
await client2.disconnect();
|
||||
expect(client2.getIsConnected()).toBeFalse();
|
||||
|
||||
await disconnectReceived.promise;
|
||||
|
||||
// Check that client is removed from server
|
||||
const clientIds = server.getClientIds();
|
||||
expect(clientIds).toContain('client-1');
|
||||
expect(clientIds).not.toContain('client-2');
|
||||
});
|
||||
|
||||
// Test auto-reconnection
|
||||
tap.test('should auto-reconnect on connection loss', async () => {
|
||||
// This test simulates connection loss by stopping and restarting the server
|
||||
const reconnected = smartpromise.defer();
|
||||
|
||||
client1.on('reconnecting', (info) => {
|
||||
expect(info).toHaveProperty('attempt');
|
||||
expect(info).toHaveProperty('delay');
|
||||
});
|
||||
|
||||
client1.on('connect', () => {
|
||||
reconnected.resolve();
|
||||
});
|
||||
|
||||
// Stop the server to simulate connection loss
|
||||
await server.stop();
|
||||
|
||||
// Wait a bit
|
||||
await smartdelay.delayFor(500);
|
||||
|
||||
// Restart the server
|
||||
await server.start();
|
||||
|
||||
// Wait for client to reconnect
|
||||
await reconnected.promise;
|
||||
expect(client1.getIsConnected()).toBeTrue();
|
||||
});
|
||||
|
||||
// Test TCP transport
|
||||
tap.test('should work with TCP transport', async () => {
|
||||
const tcpServer = smartipc.SmartIpc.createServer({
|
||||
id: 'tcp-test-server',
|
||||
host: 'localhost',
|
||||
port: 8765,
|
||||
heartbeat: false
|
||||
});
|
||||
|
||||
await tcpServer.start();
|
||||
|
||||
const tcpClient = smartipc.SmartIpc.createClient({
|
||||
id: 'tcp-test-server',
|
||||
host: 'localhost',
|
||||
port: 8765,
|
||||
clientId: 'tcp-client-1'
|
||||
});
|
||||
|
||||
await tcpClient.connect();
|
||||
expect(tcpClient.getIsConnected()).toBeTrue();
|
||||
|
||||
// Test message exchange
|
||||
const messageReceived = smartpromise.defer();
|
||||
|
||||
tcpServer.onMessage('tcp-test', (payload, clientId) => {
|
||||
expect(payload).toEqual({ data: 'TCP works!' });
|
||||
messageReceived.resolve();
|
||||
});
|
||||
|
||||
await tcpClient.sendMessage('tcp-test', { data: 'TCP works!' });
|
||||
await messageReceived.promise;
|
||||
|
||||
await tcpClient.disconnect();
|
||||
await tcpServer.stop();
|
||||
});
|
||||
|
||||
// Test message timeout
|
||||
tap.test('should timeout requests when no response is received', async () => {
|
||||
// Don't register a handler for this message type
|
||||
try {
|
||||
await client1.request(
|
||||
'non-existent-handler',
|
||||
{ data: 'test' },
|
||||
{ timeout: 1000 }
|
||||
);
|
||||
expect(true).toBeFalse(); // Should not reach here
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('timeout');
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
tap.test('should cleanup and close all connections', async () => {
|
||||
await client1.disconnect();
|
||||
await server.stop();
|
||||
|
||||
expect(server.getStats().isRunning).toBeFalse();
|
||||
expect(client1.getIsConnected()).toBeFalse();
|
||||
});
|
||||
|
||||
export default tap.start();
|
Reference in New Issue
Block a user