2025-08-23 11:29:22 +00:00
|
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
2025-08-24 16:39:09 +00:00
|
|
|
import * as smartipc from '../ts/index.js';
|
|
|
|
import * as smartdelay from '@push.rocks/smartdelay';
|
2025-08-23 11:29:22 +00:00
|
|
|
import * as smartpromise from '@push.rocks/smartpromise';
|
2025-08-29 08:48:38 +00:00
|
|
|
import * as path from 'path';
|
|
|
|
import * as os from 'os';
|
|
|
|
|
|
|
|
const testSocketPath = path.join(os.tmpdir(), `test-smartipc-${Date.now()}.sock`);
|
2019-04-08 19:42:23 +02:00
|
|
|
|
2025-08-24 16:39:09 +00:00
|
|
|
let server: smartipc.IpcServer;
|
|
|
|
let client1: smartipc.IpcClient;
|
|
|
|
let client2: smartipc.IpcClient;
|
|
|
|
|
|
|
|
// Test basic server creation and startup
|
|
|
|
tap.test('should create and start an IPC server', async () => {
|
|
|
|
server = smartipc.SmartIpc.createServer({
|
|
|
|
id: 'test-server',
|
2025-08-29 08:48:38 +00:00
|
|
|
socketPath: testSocketPath,
|
|
|
|
autoCleanupSocketFile: true,
|
2025-08-24 16:39:09 +00:00
|
|
|
heartbeat: true,
|
|
|
|
heartbeatInterval: 2000
|
|
|
|
});
|
|
|
|
|
2025-08-29 08:48:38 +00:00
|
|
|
await server.start({ readyWhen: 'accepting' });
|
2025-08-24 16:39:09 +00:00
|
|
|
expect(server.getStats().isRunning).toBeTrue();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Test client connection
|
|
|
|
tap.test('should create and connect a client', async () => {
|
|
|
|
client1 = smartipc.SmartIpc.createClient({
|
|
|
|
id: 'test-server',
|
2025-08-29 08:48:38 +00:00
|
|
|
socketPath: testSocketPath,
|
2025-08-24 16:39:09 +00:00
|
|
|
clientId: 'client-1',
|
|
|
|
metadata: { name: 'Test Client 1' },
|
|
|
|
autoReconnect: true,
|
2025-08-29 08:48:38 +00:00
|
|
|
heartbeat: true,
|
|
|
|
clientOnly: true
|
2025-08-24 16:39:09 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
await client1.connect();
|
|
|
|
expect(client1.getIsConnected()).toBeTrue();
|
|
|
|
expect(client1.getClientId()).toEqual('client-1');
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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',
|
2025-08-29 08:48:38 +00:00
|
|
|
socketPath: testSocketPath,
|
2025-08-24 16:39:09 +00:00
|
|
|
clientId: 'client-2',
|
2025-08-29 08:48:38 +00:00
|
|
|
metadata: { name: 'Test Client 2' },
|
|
|
|
clientOnly: true
|
2025-08-24 16:39:09 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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();
|
2019-04-08 19:42:23 +02:00
|
|
|
|
2025-08-24 16:39:09 +00:00
|
|
|
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();
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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();
|
2019-04-08 19:42:23 +02:00
|
|
|
});
|
2025-08-24 16:39:09 +00:00
|
|
|
|
|
|
|
// 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();
|
|
|
|
}
|
2019-04-09 12:30:12 +02:00
|
|
|
});
|
2025-08-24 16:39:09 +00:00
|
|
|
|
|
|
|
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');
|
2019-04-08 19:42:23 +02:00
|
|
|
});
|
|
|
|
|
2025-08-24 16:39:09 +00:00
|
|
|
// 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();
|
2019-04-08 19:56:21 +02:00
|
|
|
});
|
2025-08-24 16:39:09 +00:00
|
|
|
|
|
|
|
// 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();
|
2019-04-08 19:42:23 +02:00
|
|
|
});
|
|
|
|
|
2025-08-24 16:39:09 +00:00
|
|
|
// 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'
|
2019-04-09 12:35:27 +02:00
|
|
|
});
|
2025-08-24 16:39:09 +00:00
|
|
|
|
|
|
|
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();
|
2019-04-09 12:30:12 +02:00
|
|
|
});
|
2019-04-08 19:56:21 +02:00
|
|
|
|
2025-08-29 08:48:38 +00:00
|
|
|
export default tap.start();
|