UNPKG

@xtr-dev/zod-rpc

Version:

Simple, type-safe RPC library with Zod validation and automatic TypeScript inference

216 lines 6.99 kB
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