77 */
88
99import type { RuntimePlugin , RuntimeContext } from '@objectql/types' ;
10- import { IncomingMessage , ServerResponse , createServer , Server } from 'http' ;
1110
1211/**
1312 * Configuration for the JSON-RPC Plugin
1413 */
1514export interface JSONRPCPluginConfig {
16- /** Port to listen on */
15+ /** Port to listen on (deprecated in favor of shared Hono server) */
1716 port ?: number ;
1817 /** Base path for JSON-RPC endpoint */
1918 basePath ?: string ;
20- /** Enable CORS */
19+ /** Enable CORS (handled by Hono usually, kept for config compatibility) */
2120 enableCORS ?: boolean ;
2221 /** Enable introspection methods */
2322 enableIntrospection ?: boolean ;
@@ -97,11 +96,9 @@ interface MethodSignature {
9796 * - Notification support (requests without id)
9897 * - Built-in introspection methods (system.listMethods, system.describe)
9998 * - Session management for stateful operations
100- * - Progress notifications via Server-Sent Events (SSE)
99+ * - Progress notifications via Server-Sent Events (SSE) (Partial support in Hono adapter)
101100 * - Method call chaining with result references
102101 * - CRUD operations mapped to RPC methods
103- * - Named and positional parameter support
104- * - No direct database access - all operations through ObjectStackProtocolImplementation
105102 *
106103 * Available RPC Methods:
107104 * - object.find(objectName, query) - Find multiple records
@@ -113,56 +110,31 @@ interface MethodSignature {
113110 * - metadata.list() - List all objects
114111 * - metadata.get(objectName) - Get object metadata
115112 * - action.execute(actionName, params) - Execute custom action
116- * - session.create() - Create a new session (if sessions enabled)
117- * - session.get(key) - Get session value (if sessions enabled)
118- * - session.set(key, value) - Set session value (if sessions enabled)
119- * - session.destroy() - Destroy current session (if sessions enabled)
120- * - system.listMethods() - List available methods (if introspection enabled)
121- * - system.describe(method) - Describe method signature (if introspection enabled)
122113 *
123114 * @example
124115 * ```typescript
125- * import { ObjectKernel } from '@objectstack/core';
116+ * import { ObjectStackKernel } from '@objectstack/core';
126117 * import { JSONRPCPlugin } from '@objectql/protocol-json-rpc';
127118 *
128- * const kernel = new ObjectKernel ([
119+ * const kernel = new ObjectStackKernel ([
129120 * new JSONRPCPlugin({
130- * port: 9000,
131121 * basePath: '/rpc',
132- * enableSessions: true,
133- * enableProgress: true,
134- * enableChaining: true
122+ * enableSessions: true
135123 * })
136124 * ]);
137125 * await kernel.start();
138- *
139- * // Client request (positional):
140- * // POST /rpc
141- * // {"jsonrpc":"2.0","method":"object.find","params":["users",{"where":{"active":true}}],"id":1 }
142- *
143- * // Client request (named):
144- * // POST /rpc
145- * // {"jsonrpc":"2.0","method":"object.find","params":{"objectName":"users","query":{"where":{"active":true}}},"id":1 }
146- *
147- * // Batch request with chaining:
148- * // [{"jsonrpc":"2.0","method":"object.create","params":["users",{"name":"John"}],"id":1 },
149- * // {"jsonrpc":"2.0","method":"object.update","params":["users","$1.result.id",{"active":true}],"id":2 }]
150- *
151- * // Progress notifications (SSE):
152- * // GET /rpc/progress?session=<session-id>
153126 * ```
154127 */
155128export class JSONRPCPlugin implements RuntimePlugin {
156129 name = '@objectql/protocol-json-rpc' ;
157130 version = '0.2.0' ;
158131
159- private server ?: Server ;
160132 private engine ?: any ;
161133 private config : Required < JSONRPCPluginConfig > ;
162134 private methods : Map < string , Function > ;
163135 private methodSignatures : Map < string , MethodSignature > ;
164136 private sessions : Map < string , Session > = new Map ( ) ;
165- private progressClients : Map < string , ServerResponse > = new Map ( ) ;
137+ // private progressClients: Map<string, any > = new Map(); // TODO: SSE implementation for Hono
166138
167139 constructor ( config : JSONRPCPluginConfig = { } ) {
168140 this . config = {
@@ -202,19 +174,7 @@ export class JSONRPCPlugin implements RuntimePlugin {
202174 if ( ! this . engine ) {
203175 throw new Error ( 'Protocol not initialized. Install hook must be called first.' ) ;
204176 }
205-
206- console . log ( `[${ this . name } ] Starting JSON-RPC 2.0 server...` ) ;
207-
208- // Create HTTP server
209- this . server = createServer ( ( req , res ) => this . handleRequest ( req , res ) ) ;
210-
211- // Start listening
212- await new Promise < void > ( ( resolve ) => {
213- this . server ! . listen ( this . config . port , ( ) => {
214- console . log ( `[${ this . name } ] JSON-RPC 2.0 server listening on http://localhost:${ this . config . port } ${ this . config . basePath } ` ) ;
215- resolve ( ) ;
216- } ) ;
217- } ) ;
177+ console . log ( `[${ this . name } ] JSON-RPC protocol ready. Mount at ${ this . config . basePath } ` ) ;
218178 }
219179
220180 // --- Adapter for @objectstack/core compatibility ---
@@ -225,22 +185,54 @@ export class JSONRPCPlugin implements RuntimePlugin {
225185 async start ( ctx : any ) : Promise < void > {
226186 return this . onStart ( ctx ) ;
227187 }
228- // ---------------------------------------------------
229188
230189 /**
231190 * Stop hook - called when kernel stops
232191 */
233192 async onStop ( ctx : RuntimeContext ) : Promise < void > {
234- if ( this . server ) {
235- console . log ( `[${ this . name } ] Stopping JSON-RPC 2.0 server...` ) ;
236- await new Promise < void > ( ( resolve , reject ) => {
237- this . server ! . close ( ( err ) => {
238- if ( err ) reject ( err ) ;
239- else resolve ( ) ;
240- } ) ;
241- } ) ;
242- this . server = undefined ;
243- }
193+ // Cleanup logic if needed
194+ }
195+
196+ /**
197+ * Attach to Hono server
198+ */
199+ attachToHono ( app : any ) {
200+ const basePath = this . config . basePath ;
201+ console . log ( `[${ this . name } ] Attaching JSON-RPC to Hono at ${ basePath } ` ) ;
202+
203+ // Post handler for RPC requests
204+ app . post ( basePath , async ( c : any ) => {
205+ try {
206+ const body = await c . req . json ( ) ;
207+
208+ // Handle batch requests with optional chaining
209+ if ( Array . isArray ( body ) ) {
210+ if ( this . config . enableChaining ) {
211+ const responses = await this . processBatchWithChaining ( body ) ;
212+ return c . json ( responses ) ;
213+ } else {
214+ const responses = await Promise . all (
215+ body . map ( ( request : any ) => this . processRequest ( request ) )
216+ ) ;
217+ return c . json ( responses ) ;
218+ }
219+ } else {
220+ const response = await this . processRequest ( body ) ;
221+ // Don't send response for notifications
222+ if ( response ) {
223+ return c . json ( response ) ;
224+ } else {
225+ return c . body ( null , 204 ) ;
226+ }
227+ }
228+ } catch ( error ) {
229+ console . error ( `[${ this . name } ] Request error:` , error ) ;
230+ const errorResponse = this . createErrorResponse ( null , - 32700 , 'Parse error' ) ;
231+ return c . json ( errorResponse ) ;
232+ }
233+ } ) ;
234+
235+ // TODO: Implement GET /rpc/progress if needed using Hono streaming
244236 }
245237
246238 /**
@@ -648,93 +640,6 @@ export class JSONRPCPlugin implements RuntimePlugin {
648640 return signatures [ methodName ] || { description : 'No description available' } ;
649641 }
650642
651- /**
652- * Main HTTP request handler
653- */
654- private async handleRequest ( req : IncomingMessage , res : ServerResponse ) : Promise < void > {
655- // Enable CORS if configured
656- if ( this . config . enableCORS ) {
657- res . setHeader ( 'Access-Control-Allow-Origin' , '*' ) ;
658- res . setHeader ( 'Access-Control-Allow-Methods' , 'GET, POST, OPTIONS' ) ;
659- res . setHeader ( 'Access-Control-Allow-Headers' , 'Content-Type' ) ;
660-
661- if ( req . method === 'OPTIONS' ) {
662- res . writeHead ( 204 ) ;
663- res . end ( ) ;
664- return ;
665- }
666- }
667-
668- const url = req . url || '/' ;
669- const basePath = this . config . basePath ;
670-
671- // Check if request is for RPC endpoint
672- if ( ! url . startsWith ( basePath ) ) {
673- this . sendError ( res , null , - 32600 , 'Invalid Request: Wrong endpoint' ) ;
674- return ;
675- }
676-
677- // Handle progress SSE endpoint (GET /rpc/progress?session=<id>)
678- if ( this . config . enableProgress && req . method === 'GET' && url . includes ( '/progress' ) ) {
679- const sessionId = new URL ( url , 'http://localhost' ) . searchParams . get ( 'session' ) ;
680- if ( sessionId ) {
681- // Setup SSE
682- res . writeHead ( 200 , {
683- 'Content-Type' : 'text/event-stream' ,
684- 'Cache-Control' : 'no-cache' ,
685- 'Connection' : 'keep-alive'
686- } ) ;
687-
688- this . progressClients . set ( sessionId , res ) ;
689-
690- // Send initial connection message
691- res . write ( 'data: {"type":"connected"}\n\n' ) ;
692-
693- // Cleanup on close
694- req . on ( 'close' , ( ) => {
695- this . progressClients . delete ( sessionId ) ;
696- } ) ;
697-
698- return ;
699- }
700- }
701-
702- // Only accept POST requests for RPC
703- if ( req . method !== 'POST' ) {
704- this . sendError ( res , null , - 32600 , 'Invalid Request: Method must be POST' ) ;
705- return ;
706- }
707-
708- try {
709- const body = await this . readBody ( req ) ;
710-
711- // Handle batch requests with optional chaining
712- if ( Array . isArray ( body ) ) {
713- if ( this . config . enableChaining ) {
714- const responses = await this . processBatchWithChaining ( body ) ;
715- this . sendJSON ( res , 200 , responses ) ;
716- } else {
717- const responses = await Promise . all (
718- body . map ( request => this . processRequest ( request ) )
719- ) ;
720- this . sendJSON ( res , 200 , responses ) ;
721- }
722- } else {
723- const response = await this . processRequest ( body ) ;
724- // Don't send response for notifications
725- if ( response ) {
726- this . sendJSON ( res , 200 , response ) ;
727- } else {
728- res . writeHead ( 204 ) ;
729- res . end ( ) ;
730- }
731- }
732- } catch ( error ) {
733- console . error ( `[${ this . name } ] Request error:` , error ) ;
734- this . sendError ( res , null , - 32700 , 'Parse error' ) ;
735- }
736- }
737-
738643 /**
739644 * Process a single JSON-RPC request
740645 */
@@ -795,6 +700,7 @@ export class JSONRPCPlugin implements RuntimePlugin {
795700 id : request . id
796701 } ;
797702 } catch ( error ) {
703+ console . error ( error ) ;
798704 if ( isNotification ) return null ;
799705
800706 return this . createErrorResponse (
@@ -849,44 +755,6 @@ export class JSONRPCPlugin implements RuntimePlugin {
849755 } ;
850756 }
851757
852- /**
853- * Read request body as JSON
854- */
855- private readBody ( req : IncomingMessage ) : Promise < any > {
856- return new Promise ( ( resolve , reject ) => {
857- let body = '' ;
858- req . on ( 'data' , chunk => body += chunk . toString ( ) ) ;
859- req . on ( 'end' , ( ) => {
860- if ( ! body ) {
861- reject ( new Error ( 'Empty body' ) ) ;
862- return ;
863- }
864- try {
865- resolve ( JSON . parse ( body ) ) ;
866- } catch ( e ) {
867- reject ( new Error ( 'Invalid JSON' ) ) ;
868- }
869- } ) ;
870- req . on ( 'error' , reject ) ;
871- } ) ;
872- }
873-
874- /**
875- * Send JSON response
876- */
877- private sendJSON ( res : ServerResponse , statusCode : number , data : any ) : void {
878- res . setHeader ( 'Content-Type' , 'application/json' ) ;
879- res . writeHead ( statusCode ) ;
880- res . end ( JSON . stringify ( data , null , 2 ) ) ;
881- }
882-
883- /**
884- * Send error response
885- */
886- private sendError ( res : ServerResponse , id : any , code : number , message : string ) : void {
887- this . sendJSON ( res , 200 , this . createErrorResponse ( id , code , message ) ) ;
888- }
889-
890758 /**
891759 * Generate a unique session ID
892760 */
@@ -942,12 +810,9 @@ export class JSONRPCPlugin implements RuntimePlugin {
942810 /**
943811 * Send progress notification to SSE clients
944812 */
945- private sendProgress ( sessionId : string , progress : ProgressNotification ) : void {
946- const client = this . progressClients . get ( sessionId ) ;
947- if ( client ) {
948- client . write ( `data: ${ JSON . stringify ( progress ) } \n\n` ) ;
949- }
950- }
813+ // private sendProgress(sessionId: string, progress: ProgressNotification): void {
814+ // // TODO: Implement for Hono
815+ // }
951816
952817 /**
953818 * Resolve result references in batch requests (e.g., $1.result.id)
0 commit comments