Skip to content

Commit c6eeec8

Browse files
Copilothotlong
andcommitted
Add standalone HTTP server support to JSON-RPC plugin
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent e65e059 commit c6eeec8

1 file changed

Lines changed: 129 additions & 2 deletions

File tree

packages/protocols/json-rpc/src/index.ts

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import type { RuntimePlugin, RuntimeContext } from '@objectql/types';
10+
import { IncomingMessage, ServerResponse, createServer, Server } from 'http';
1011
import {
1112
validateRequest,
1213
validateBatchRequest,
@@ -149,6 +150,7 @@ export class JSONRPCPlugin implements RuntimePlugin {
149150
private methodSignatures: Map<string, MethodSignature>;
150151
private sessions: Map<string, Session> = new Map();
151152
private progressClients: Map<string, Set<(data: string) => void>> = new Map();
153+
private server?: Server;
152154

153155
constructor(config: JSONRPCPluginConfig = {}) {
154156
this.config = {
@@ -188,7 +190,23 @@ export class JSONRPCPlugin implements RuntimePlugin {
188190
if (!this.engine) {
189191
throw new Error('Protocol not initialized. Install hook must be called first.');
190192
}
191-
console.log(`[${this.name}] JSON-RPC protocol ready. Mount at ${this.config.basePath}`);
193+
194+
// Start standalone HTTP server for testing/development
195+
console.log(`[${this.name}] Starting JSON-RPC server (standalone)...`);
196+
197+
// Create HTTP server
198+
this.server = createServer((req, res) => this.handleRequest(req, res));
199+
200+
// Start listening
201+
await new Promise<void>((resolve, reject) => {
202+
this.server!.listen(this.config.port, () => {
203+
console.log(`[${this.name}] 🚀 JSON-RPC server listening on http://localhost:${this.config.port}${this.config.basePath}`);
204+
resolve();
205+
});
206+
this.server!.on('error', reject);
207+
});
208+
209+
console.log(`[${this.name}] JSON-RPC protocol ready`);
192210
}
193211

194212
// --- Adapter for @objectstack/core compatibility ---
@@ -204,7 +222,116 @@ export class JSONRPCPlugin implements RuntimePlugin {
204222
* Stop hook - called when kernel stops
205223
*/
206224
async onStop(ctx: RuntimeContext): Promise<void> {
207-
// Cleanup logic if needed
225+
// Stop the HTTP server
226+
if (this.server) {
227+
console.log(`[${this.name}] Stopping JSON-RPC server...`);
228+
await new Promise<void>((resolve) => {
229+
this.server!.close(() => {
230+
resolve();
231+
});
232+
});
233+
}
234+
// Cleanup sessions
235+
for (const session of this.sessions.values()) {
236+
if (session.timeout) {
237+
clearTimeout(session.timeout);
238+
}
239+
}
240+
this.sessions.clear();
241+
}
242+
243+
/**
244+
* Handle HTTP request for standalone server
245+
*/
246+
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
247+
// Enable CORS if configured
248+
if (this.config.enableCORS) {
249+
res.setHeader('Access-Control-Allow-Origin', '*');
250+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
251+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
252+
253+
if (req.method === 'OPTIONS') {
254+
res.writeHead(204);
255+
res.end();
256+
return;
257+
}
258+
}
259+
260+
const url = req.url || '/';
261+
const basePath = this.config.basePath;
262+
263+
// Check if request is for RPC endpoint
264+
if (!url.startsWith(basePath)) {
265+
res.writeHead(404, { 'Content-Type': 'application/json' });
266+
res.end(JSON.stringify({ error: 'Not Found' }));
267+
return;
268+
}
269+
270+
// Only accept POST requests
271+
if (req.method !== 'POST') {
272+
res.writeHead(405, { 'Content-Type': 'application/json' });
273+
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
274+
return;
275+
}
276+
277+
try {
278+
// Read request body
279+
let body = '';
280+
for await (const chunk of req) {
281+
body += chunk;
282+
}
283+
284+
const jsonBody = JSON.parse(body);
285+
286+
// Handle batch or single request
287+
let response;
288+
if (Array.isArray(jsonBody)) {
289+
// Validate batch request
290+
try {
291+
validateBatchRequest(jsonBody);
292+
} catch (error: any) {
293+
if (error instanceof JSONRPCValidationError) {
294+
response = createErrorResponse(null, error.code, error.message, error.data);
295+
} else {
296+
response = createErrorResponse(null, JSONRPCErrorCode.INVALID_REQUEST, 'Invalid batch request');
297+
}
298+
res.writeHead(200, { 'Content-Type': 'application/json' });
299+
res.end(JSON.stringify(response));
300+
return;
301+
}
302+
303+
if (this.config.enableChaining) {
304+
const responses = await this.processBatchWithChaining(jsonBody);
305+
response = validateBatchResponse(responses);
306+
} else {
307+
const responses = await Promise.all(
308+
jsonBody.map((request: any) => this.processRequest(request))
309+
);
310+
const filteredResponses = responses.filter(r => r !== null);
311+
response = validateBatchResponse(filteredResponses);
312+
}
313+
} else {
314+
response = await this.processRequest(jsonBody);
315+
}
316+
317+
// Send response (don't send for notifications)
318+
if (response) {
319+
res.writeHead(200, { 'Content-Type': 'application/json' });
320+
res.end(JSON.stringify(response));
321+
} else {
322+
res.writeHead(204);
323+
res.end();
324+
}
325+
} catch (error) {
326+
console.error(`[${this.name}] Request error:`, error);
327+
const errorResponse = createErrorResponse(
328+
null,
329+
JSONRPCErrorCode.PARSE_ERROR,
330+
error instanceof Error ? error.message : 'Parse error'
331+
);
332+
res.writeHead(200, { 'Content-Type': 'application/json' });
333+
res.end(JSON.stringify(errorResponse));
334+
}
208335
}
209336

210337
/**

0 commit comments

Comments
 (0)