@xtr-dev/zod-rpc
Version:
Simple, type-safe RPC library with Zod validation and automatic TypeScript inference
155 lines • 5.22 kB
JavaScript
import { TransportError } from '../errors.js';
/**
* @group Transport Layer
*/
export class HTTPTransport {
config;
messageHandler;
connected = false;
constructor(config) {
this.config = config;
if (!config.fetch && typeof fetch === 'undefined') {
throw new TransportError('fetch is not available. Please provide a fetch implementation.');
}
}
async send(message) {
if (!this.isConnected()) {
throw new TransportError('HTTP transport is not connected');
}
const fetchFn = this.config.fetch || fetch;
const url = `${this.config.baseUrl.replace(/\/$/, '')}/rpc`;
try {
const response = await fetchFn(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: JSON.stringify(message),
signal: this.config.timeout ? AbortSignal.timeout(this.config.timeout) : undefined,
});
if (!response.ok) {
throw new TransportError(`HTTP request failed with status ${response.status}: ${response.statusText}`);
}
if (message.type === 'request') {
const responseData = await response.json();
const responseMessage = responseData;
this.messageHandler?.(responseMessage);
}
}
catch (error) {
if (error instanceof TransportError) {
throw error;
}
let errorMessage = 'Unknown error';
if (error instanceof Error) {
errorMessage = error.message;
}
throw new TransportError(`HTTP request failed: ${errorMessage}`);
}
}
onMessage(handler) {
this.messageHandler = handler;
}
async connect() {
try {
const fetchFn = this.config.fetch || fetch;
const healthUrl = `${this.config.baseUrl.replace(/\/$/, '')}/health`;
const response = await fetchFn(healthUrl, {
method: 'GET',
headers: this.config.headers,
signal: AbortSignal.timeout(5000),
});
if (response.ok) {
this.connected = true;
}
else {
throw new TransportError(`Server health check failed: ${response.statusText}`);
}
}
catch (error) {
this.connected = false;
let errorMessage = 'Unknown error';
if (error instanceof Error) {
errorMessage = error.message;
}
throw new TransportError(`Failed to connect to HTTP server: ${errorMessage}`);
}
}
async disconnect() {
this.connected = false;
}
isConnected() {
return this.connected;
}
}
/**
* Creates Express middleware for handling zod-rpc calls with a channel.
*
* @param channel - Channel with registered service implementations
* @returns Express middleware function
*
* @example
* ```typescript
* import express from 'express';
* import { zodRpc, Channel } from '@xtr-dev/zod-rpc';
* import { implementService } from '@xtr-dev/zod-rpc';
*
* const app = express();
* app.use(express.json());
*
* // Create channel for local method invocation (no transport needed)
* const channel = new Channel('server');
*
* // Implement and publish service (simplified API)
* channel.publishService(userService, {
* get: async ({ userId }) => ({ id: userId, name: 'John' }),
* create: async ({ name, email }) => ({ id: '123', success: true })
* });
*
* // Use middleware
* app.use('/rpc', zodRpc(channel));
* ```
* @group HTTP Middleware
*/
export function zodRpc(channel) {
return async (req, res, next) => {
if (req.method !== 'POST') {
return next();
}
try {
const message = req.body;
// Validate message structure
if (!message.methodId || !message.traceId || !message.callerId || !message.targetId) {
res.status(400).json({
error: 'Invalid RPC message',
message: 'Missing required fields: methodId, traceId, callerId, targetId',
});
return;
}
// Call method directly through channel
const result = await channel.invoke(message.targetId, message.methodId, message.payload);
res.json({
callerId: message.targetId,
targetId: message.callerId,
traceId: message.traceId,
methodId: message.methodId,
payload: result,
type: 'response',
});
}
catch (error) {
res.status(500).json({
error: 'RPC handler error',
message: error instanceof Error ? error.message : 'Unknown error',
});
}
};
}
/**
* @group Transport Layer
*/
export function createHTTPTransport(config) {
return new HTTPTransport(config);
}
//# sourceMappingURL=http.js.map