@xtr-dev/zod-rpc
Version:
Simple, type-safe RPC library with Zod validation and automatic TypeScript inference
216 lines • 6.99 kB
JavaScript
import { Channel } from './channel.js';
import { createWebSocketTransport } from './transports/index.js';
import { implementService } from './service.js';
import WebSocket from 'ws';
/**
* RPC Server for hosting type-safe remote procedure call services.
*
* @example
* ```typescript
* const server = createRPCServer('ws://localhost:8080')
* .implement(userService, {
* get: async ({ userId }) => ({ id: userId, name: 'John' }),
* create: async ({ name, email }) => ({ id: '123', success: true })
* });
*
* await server.start();
* ```
* @group Server API
*/
export class RPCServer {
url;
config;
channel;
server;
serverId;
timeout;
services = new Map();
implementations = new Map();
constructor(url, config = {}) {
this.url = url;
this.config = config;
this.serverId = config.serverId || 'server';
this.timeout = config.timeout || 30000;
}
implement(service, implementation) {
this.services.set(service.id, service);
this.implementations.set(service.id, implementation);
return this;
}
async start() {
if (this.url.startsWith('ws://') || this.url.startsWith('wss://')) {
await this.startWebSocketServer();
}
else if (this.url.startsWith('http://') || this.url.startsWith('https://')) {
await this.startHttpServer();
}
else {
throw new Error(`Unsupported URL scheme: ${this.url}`);
}
}
async startWebSocketServer() {
const urlObj = new URL(this.url);
const port = parseInt(urlObj.port) || 8080;
const host = urlObj.hostname || 'localhost';
this.server = new WebSocket.Server({ port, host });
console.log(`🚀 RPC Server starting on ${this.url}`);
this.server.on('connection', async (ws) => {
console.log('📡 Client connected');
const transport = createWebSocketTransport(ws, false); // Don't auto-reconnect on server
const channel = new Channel(this.serverId, this.timeout);
for (const [serviceId, service] of this.services) {
const implementation = this.implementations.get(serviceId);
if (implementation) {
const methods = implementService(service, implementation, this.serverId);
methods.forEach((method) => channel.publishMethod(method));
}
}
await channel.connect(transport);
console.log(`✅ Server channel connected with services: ${Array.from(this.services.keys()).join(', ')}`);
ws.on('close', () => {
console.log('📴 Client disconnected');
channel.disconnect();
});
ws.on('error', (error) => {
console.error('❌ WebSocket error:', error);
channel.disconnect();
});
});
this.logAvailableServices();
}
async startHttpServer() {
throw new Error('HTTP transport for server not yet implemented. Use WebSocket transport.');
}
logAvailableServices() {
console.log('📋 Available services:');
for (const [serviceId, service] of this.services) {
console.log(` 📦 ${serviceId}:`);
for (const methodName of Object.keys(service.methods)) {
console.log(` - ${serviceId}.${methodName}`);
}
}
}
async stop() {
if (this.server) {
return new Promise((resolve) => {
this.server.close(() => {
console.log('🛑 Server stopped');
this.server = undefined; // Clear server reference
resolve();
});
});
}
if (this.channel) {
await this.channel.disconnect();
}
}
getServices() {
return Array.from(this.services.keys());
}
isRunning() {
return this.server !== undefined;
}
}
/**
* Create a new RPC server instance.
* This is the main entry point for creating servers with service implementations.
*
* @param url - WebSocket or HTTP URL for the server to listen on
* @param config - Optional server configuration
* @returns A new RPCServer instance ready for service implementation
*
* @example
* ```typescript
* const server = createRPCServer('ws://localhost:8080', {
* serverId: 'my-server',
* timeout: 30000
* });
*
* server.implement(userService, {
* get: async ({ userId }) => ({ name: `User ${userId}`, email: `user${userId}@example.com` }),
* create: async ({ name, email }) => ({ id: '123', success: true })
* });
*
* await server.start();
* ```
*
* @group Server API
*/
export function createRPCServer(url, config) {
return new RPCServer(url, config);
}
/**
* Fluent builder pattern for creating RPC servers with advanced configurations.
* Provides a chainable API for setting server options before building the final server.
*
* @example
* ```typescript
* const server = createServer('ws://localhost:8080')
* .withId('my-server')
* .withTimeout(30000)
* .withHost('0.0.0.0')
* .withPort(8080)
* .build();
* ```
*
* @group Server API
*/
export class RPCServerBuilder {
config = {};
url;
constructor(url) {
this.url = url;
}
withId(serverId) {
const newBuilder = new RPCServerBuilder(this.url);
newBuilder.config = { ...this.config, serverId };
return newBuilder;
}
withTimeout(timeout) {
const newBuilder = new RPCServerBuilder(this.url);
newBuilder.config = { ...this.config, timeout };
return newBuilder;
}
withPort(port) {
const newBuilder = new RPCServerBuilder(this.url);
newBuilder.config = { ...this.config, port };
return newBuilder;
}
withHost(host) {
const newBuilder = new RPCServerBuilder(this.url);
newBuilder.config = { ...this.config, host };
return newBuilder;
}
build() {
return new RPCServer(this.url, this.config);
}
}
/**
* Create a new RPC server builder for fluent configuration.
* This is the preferred way to create servers with custom settings.
*
* @param url - WebSocket or HTTP URL for the server to listen on
* @returns A new RPCServerBuilder instance for chaining configuration
*
* @example
* ```typescript
* const server = createServer('ws://localhost:8080')
* .withId('my-server')
* .withTimeout(30000)
* .withHost('0.0.0.0')
* .build();
*
* server.implement(userService, userImplementation);
* await server.start();
* ```
*
* @group Server API
*/
export function createServer(url) {
return new RPCServerBuilder(url);
}
// Backward compatibility aliases
export { RPCServerBuilder as RpcServerBuilder };
export { createRPCServer as createRpcServer };
export { RPCServer as RpcServer };
//# sourceMappingURL=server.js.map