diff --git a/src/DuplexJsonRpcEngine.ts b/src/DuplexJsonRpcEngine.ts new file mode 100644 index 0000000..cc5473a --- /dev/null +++ b/src/DuplexJsonRpcEngine.ts @@ -0,0 +1,180 @@ +import { + isJsonRpcRequest, + Json, + JsonRpcNotification, + JsonRpcParams, + JsonRpcRequest, + JsonRpcResponse, +} from '@metamask/utils'; + +import { + JsonRpcEngine, + JsonRpcMiddleware, + JsonRpcNotificationHandler, +} from './JsonRpcEngine'; + +type HandleArgument = + | JsonRpcRequest + | JsonRpcNotification + | (JsonRpcRequest | JsonRpcNotification)[]; + +type DuplexJsonRpcEngineArgs = { + receiverNotificationHandler: JsonRpcNotificationHandler; + senderNotificationHandler: JsonRpcNotificationHandler; +}; + +export class DuplexJsonRpcEngine { + readonly #receiver: JsonRpcEngine; + + readonly #sender: JsonRpcEngine; + + get receiver(): JsonRpcEngine { + return this.#receiver; + } + + get sender(): JsonRpcEngine { + return this.#sender; + } + + constructor({ + receiverNotificationHandler, + senderNotificationHandler, + }: DuplexJsonRpcEngineArgs) { + this.#receiver = new JsonRpcEngine({ + notificationHandler: receiverNotificationHandler, + }); + + this.#sender = new JsonRpcEngine({ + notificationHandler: senderNotificationHandler, + }); + } + + /** + * Add a middleware function to the receiving middleware stack. + * + * @param middleware - The middleware function to add. + */ + addReceiverMiddleware( + middleware: JsonRpcMiddleware, + ): void { + this.#receiver.addMiddleware(middleware); + } + + /** + * Add a middleware function to the sending middleware stack. + * + * @param middleware - The middleware function to add. + */ + addSenderMiddleware( + middleware: JsonRpcMiddleware, + ): void { + this.#sender.addMiddleware(middleware); + } + + /** + * Returns the receiving pipeline as a middleware function that can be added + * to other engines. + * + * @returns The receiving pipeline as a middleware function. + */ + receiverAsMiddleware(): JsonRpcMiddleware { + return this.#receiver.asMiddleware(); + } + + /** + * Returns the sending pipeline as a middleware function that can be added + * to other engines. + * + * @returns The sending pipeline as a middleware function. + */ + senderAsMiddleware(): JsonRpcMiddleware { + return this.#sender.asMiddleware(); + } + + /** + * Receive a JSON-RPC request, and return a response. + * + * @param request - The JSON-RPC request to receive. + * @returns The JSON-RPC response. + */ + receive( + request: JsonRpcRequest, + ): Promise>; + + /** + * Receive a JSON-RPC notification. + * + * @param notification - The notification to receive. + */ + receive( + notification: JsonRpcNotification, + ): Promise; + + /** + * Receive an array of JSON-RPC requests and/or notifications, and return an + * array of responses to any included requests. + * + * @param request - The JSON-RPC requests to receive. + * @returns An array of JSON-RPC responses. + */ + receive( + requests: (JsonRpcRequest | JsonRpcNotification)[], + ): Promise[]>; + + async receive(argument: HandleArgument) { + if (Array.isArray(argument)) { + return this.#receiver.handle(argument); + } else if (isJsonRpcRequest(argument)) { + return this.#receiver.handle(argument); + } + return this.#receiver.handle(argument); + } + + /** + * Send a JSON-RPC request, and receive a response. + * + * @param request - The JSON-RPC request to send. + * @returns The JSON-RPC response. + */ + send( + request: JsonRpcRequest, + ): Promise>; + + /** + * Send a JSON-RPC notification. + * + * @param notification - The notification to send. + */ + send( + notification: JsonRpcNotification, + ): Promise; + + /** + * Send an array of JSON-RPC requests and/or notifications, and receive an + * array of responses to any included requests. + * + * @param request - The JSON-RPC requests to send. + * @returns An array of JSON-RPC responses. + */ + send( + requests: (JsonRpcRequest | JsonRpcNotification)[], + ): Promise[]>; + + async send(argument: HandleArgument) { + if (Array.isArray(argument)) { + return this.#sender.handle(argument); + } else if (isJsonRpcRequest(argument)) { + return this.#sender.handle(argument); + } + return this.#sender.handle(argument); + } + + /** + * Destroys this engine and its sending and receiving middleware stacks. See + * {@link JsonRpcEngine.destroy} for details. + */ + destroy() { + this.#receiver.destroy(); + this.#sender.destroy(); + } +} diff --git a/src/JsonRpcEngine.test.ts b/src/JsonRpcEngine.test.ts index fe77684..1484f93 100644 --- a/src/JsonRpcEngine.test.ts +++ b/src/JsonRpcEngine.test.ts @@ -54,7 +54,7 @@ describe('JsonRpcEngine', () => { const middleware = jest.fn(); const notificationHandler = jest.fn(); const engine = new JsonRpcEngine({ notificationHandler }); - engine.push(middleware); + engine.addMiddleware(middleware); expect( await engine.handle({ jsonrpc, method: true } as any), @@ -72,7 +72,7 @@ describe('JsonRpcEngine', () => { }); const engine = new JsonRpcEngine(); - engine.push(middleware); + engine.addMiddleware(middleware); expect(await engine.handle({ jsonrpc, method: 'foo' })).toStrictEqual({ jsonrpc, @@ -86,7 +86,7 @@ describe('JsonRpcEngine', () => { const middleware = jest.fn(); const notificationHandler = jest.fn(); const engine = new JsonRpcEngine({ notificationHandler }); - engine.push(middleware); + engine.addMiddleware(middleware); expect(await engine.handle({ jsonrpc, method: 'foo' })).toBeUndefined(); expect(notificationHandler).toHaveBeenCalledTimes(1); @@ -137,7 +137,7 @@ describe('JsonRpcEngine', () => { it('handle: basic middleware test 1', async () => { const engine = new JsonRpcEngine(); - engine.push(function (_request, response, _next, end) { + engine.addMiddleware(function (_request, response, _next, end) { response.result = 42; end(); }); @@ -157,7 +157,7 @@ describe('JsonRpcEngine', () => { it('handle: basic middleware test 2', async () => { const engine = new JsonRpcEngine(); - engine.push(function (request, response, _next, end) { + engine.addMiddleware(function (request, response, _next, end) { request.method = 'banana'; response.result = 42; end(); @@ -179,7 +179,7 @@ describe('JsonRpcEngine', () => { it('handle (async): basic middleware test', async () => { const engine = new JsonRpcEngine(); - engine.push(function (_request, response, _next, end) { + engine.addMiddleware(function (_request, response, _next, end) { response.result = 42; end(); }); @@ -194,7 +194,7 @@ describe('JsonRpcEngine', () => { it('allow null result', async () => { const engine = new JsonRpcEngine(); - engine.push(function (_request, response, _next, end) { + engine.addMiddleware(function (_request, response, _next, end) { response.result = null; end(); }); @@ -214,12 +214,12 @@ describe('JsonRpcEngine', () => { it('interacting middleware test', async () => { const engine = new JsonRpcEngine(); - engine.push(function (request: any, _response, next, _end) { + engine.addMiddleware(function (request: any, _response, next, _end) { request.resultShouldBe = 42; next(); }); - engine.push(function (request: any, response, _next, end) { + engine.addMiddleware(function (request: any, response, _next, end) { response.result = request.resultShouldBe; end(); }); @@ -239,12 +239,12 @@ describe('JsonRpcEngine', () => { it('middleware ending request before all middlewares applied', async () => { const engine = new JsonRpcEngine(); - engine.push(function (_request, response, _next, end) { + engine.addMiddleware(function (_request, response, _next, end) { response.result = 42; end(); }); - engine.push(function (_request, _response, _next, _end) { + engine.addMiddleware(function (_request, _response, _next, _end) { throw new Error('Test should have ended already.'); }); @@ -263,7 +263,7 @@ describe('JsonRpcEngine', () => { it('erroring middleware test: end(error)', async () => { const engine = new JsonRpcEngine(); - engine.push(function (_request, _response, _next, end) { + engine.addMiddleware(function (_request, _response, _next, end) { end(new Error('no bueno')); }); @@ -283,7 +283,7 @@ describe('JsonRpcEngine', () => { it('erroring middleware test: response.error -> next()', async () => { const engine = new JsonRpcEngine(); - engine.push(function (_request, response, next, _end) { + engine.addMiddleware(function (_request, response, next, _end) { response.error = rpcErrors.internal({ message: 'foobar' }); next(); }); @@ -304,7 +304,7 @@ describe('JsonRpcEngine', () => { it('erroring middleware test: response.error -> end()', async () => { const engine = new JsonRpcEngine(); - engine.push(function (_request, response, _next, end) { + engine.addMiddleware(function (_request, response, _next, end) { response.error = rpcErrors.internal({ message: 'foobar' }); end(); }); @@ -325,7 +325,7 @@ describe('JsonRpcEngine', () => { it('erroring middleware test: non-function passsed to next()', async () => { const engine = new JsonRpcEngine(); - engine.push(function (_request, _response, next, _end) { + engine.addMiddleware(function (_request, _response, next, _end) { next(true as any); }); @@ -364,7 +364,7 @@ describe('JsonRpcEngine', () => { it('handle: batch payloads', async () => { const engine = new JsonRpcEngine(); - engine.push(function (request, response, _next, end) { + engine.addMiddleware(function (request, response, _next, end) { if (request.id === 4) { delete response.result; response.error = rpcErrors.internal({ message: 'foobar' }); @@ -399,7 +399,7 @@ describe('JsonRpcEngine', () => { it('handle: batch payloads (async signature)', async () => { const engine = new JsonRpcEngine(); - engine.push(function (request, response, _next, end) { + engine.addMiddleware(function (request, response, _next, end) { if (request.id === 4) { delete response.result; response.error = rpcErrors.internal({ message: 'foobar' }); @@ -429,7 +429,7 @@ describe('JsonRpcEngine', () => { it('handle: batch payload with bad request object', async () => { const engine = new JsonRpcEngine(); - engine.push(function (request, response, _next, end) { + engine.addMiddleware(function (request, response, _next, end) { response.result = request.id; return end(); }); @@ -462,14 +462,14 @@ describe('JsonRpcEngine', () => { it('return handlers test', async () => { const engine = new JsonRpcEngine(); - engine.push(function (_request, response: any, next, _end) { + engine.addMiddleware(function (_request, response: any, next, _end) { next(function (callback) { response.sawReturnHandler = true; callback(); }); }); - engine.push(function (_request, response, _next, end) { + engine.addMiddleware(function (_request, response, _next, end) { response.result = true; end(); }); @@ -491,7 +491,7 @@ describe('JsonRpcEngine', () => { const events: string[] = []; - engine.push(function (_request, _response, next, _end) { + engine.addMiddleware(function (_request, _response, next, _end) { events.push('1-next'); next(function (callback) { events.push('1-return'); @@ -499,7 +499,7 @@ describe('JsonRpcEngine', () => { }); }); - engine.push(function (_request, _response, next, _end) { + engine.addMiddleware(function (_request, _response, next, _end) { events.push('2-next'); next(function (callback) { events.push('2-return'); @@ -507,7 +507,7 @@ describe('JsonRpcEngine', () => { }); }); - engine.push(function (_request, response, _next, end) { + engine.addMiddleware(function (_request, response, _next, end) { events.push('3-end'); response.result = true; end(); @@ -533,14 +533,14 @@ describe('JsonRpcEngine', () => { let sawNextReturnHandlerCalled = false; - engine.push(function (_request, _response, next, _end) { + engine.addMiddleware(function (_request, _response, next, _end) { next(function (callback) { sawNextReturnHandlerCalled = true; callback(); }); }); - engine.push(function (_request, _response, _next, end) { + engine.addMiddleware(function (_request, _response, _next, end) { end(new Error('boom')); }); @@ -558,13 +558,13 @@ describe('JsonRpcEngine', () => { it('handles error in next handler', async () => { const engine = new JsonRpcEngine(); - engine.push(function (_request, _response, next, _end) { + engine.addMiddleware(function (_request, _response, next, _end) { next(function (_cb) { throw new Error('foo'); }); }); - engine.push(function (_request, response, _next, end) { + engine.addMiddleware(function (_request, response, _next, end) { response.result = 42; end(); }); @@ -583,7 +583,7 @@ describe('JsonRpcEngine', () => { it('handles failure to end request', async () => { const engine = new JsonRpcEngine(); - engine.push(function (_request, response, next, _end) { + engine.addMiddleware(function (_request, response, next, _end) { response.result = 42; next(); }); diff --git a/src/JsonRpcEngine.ts b/src/JsonRpcEngine.ts index c31421c..2048380 100644 --- a/src/JsonRpcEngine.ts +++ b/src/JsonRpcEngine.ts @@ -129,13 +129,25 @@ export class JsonRpcEngine extends SafeEventEmitter { * * @param middleware - The middleware function to add. */ - push( + addMiddleware( middleware: JsonRpcMiddleware, ): void { this.#assertIsNotDestroyed(); this.#middleware.push(middleware as JsonRpcMiddleware); } + /** + * Add a middleware function to the engine's middleware stack. + * + * @deprecated Use {@link JsonRpcEngine.addMiddleware} instead. + * @param middleware - The middleware function to add. + */ + push( + middleware: JsonRpcMiddleware, + ): void { + this.addMiddleware(middleware); + } + /** * Handle a JSON-RPC request, and return a response. * diff --git a/src/index.ts b/src/index.ts index d7644d5..301dc32 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,4 @@ export * from './getUniqueId'; export * from './idRemapMiddleware'; export * from './JsonRpcEngine'; export * from './mergeMiddleware'; +export * from './DuplexJsonRpcEngine';