77 */
88
99import type { RuntimePlugin , RuntimeContext } from '@objectql/types' ;
10+ import { IncomingMessage , ServerResponse , createServer , Server } from 'http' ;
1011import {
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