Skip to content
This repository has been archived by the owner on Nov 9, 2023. It is now read-only.

Handle JSON-RPC notifications #104

Merged
merged 9 commits into from
Aug 15, 2022
Merged
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ A tool for processing JSON-RPC requests and responses.
```js
const { JsonRpcEngine } = require('json-rpc-engine');

let engine = new JsonRpcEngine();
const engine = new JsonRpcEngine();
```

Build a stack of JSON-RPC processors by pushing middleware to the engine.
Expand All @@ -22,7 +22,7 @@ engine.push(function (req, res, next, end) {
Requests are handled asynchronously, stepping down the stack until complete.

```js
let request = { id: 1, jsonrpc: '2.0', method: 'hello' };
const request = { id: 1, jsonrpc: '2.0', method: 'hello' };

engine.handle(request, function (err, response) {
// Do something with response.result, or handle response.error
Expand Down Expand Up @@ -53,6 +53,18 @@ engine.push(function (req, res, next, end) {
});
```

If you specify a `notificationHandler` when constructing the engine, JSON-RPC notifications passed to `handle()` will be handed off directly to this function without touching the middleware stack:

```js
const engine = new JsonRpcEngine({ notificationHandler });

// A notification is defined as a JSON-RPC request without an `id` property.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it could cause all of kinds of runtime bugs. Glad we're using TypeScript 😬

const notification = { jsonrpc: '2.0', method: 'hello' };

const response = await engine.handle(notification);
console.log(typeof response); // 'undefined'
```

Engines can be nested by converting them to middleware using `JsonRpcEngine.asMiddleware()`:

```js
Expand Down
99 changes: 94 additions & 5 deletions src/JsonRpcEngine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ const jsonrpc = '2.0' as const;

describe('JsonRpcEngine', () => {
it('handle: throws on truthy, non-function callback', () => {
const engine: any = new JsonRpcEngine();
expect(() => engine.handle({}, true)).toThrow(
const engine = new JsonRpcEngine();
expect(() => engine.handle({} as any, 'foo' as any)).toThrow(
'"callback" must be a function if provided.',
);
});

it('handle: returns error for invalid request parameter', async () => {
it('handle: returns error for invalid request value', async () => {
const engine = new JsonRpcEngine();
let response: any = await engine.handle(null as any);
expect(response.error.code).toStrictEqual(-32600);
Expand All @@ -30,15 +30,104 @@ describe('JsonRpcEngine', () => {

it('handle: returns error for invalid request method', async () => {
const engine = new JsonRpcEngine();
let response: any = await engine.handle({ method: null } as any);
const response: any = await engine.handle({ id: 1, method: null } as any);

expect(response.error.code).toStrictEqual(-32600);
expect(response.result).toBeUndefined();
});

it('handle: returns error for invalid request method with nullish id', async () => {
const engine = new JsonRpcEngine();
const response: any = await engine.handle({
id: undefined,
method: null,
} as any);

response = await engine.handle({ method: true } as any);
expect(response.error.code).toStrictEqual(-32600);
expect(response.result).toBeUndefined();
});

it('handle: returns undefined for malformed notifications', async () => {
const middleware = jest.fn();
const notificationHandler = jest.fn();
const engine = new JsonRpcEngine({ notificationHandler });
engine.push(middleware);

expect(
await engine.handle({ jsonrpc, method: true } as any),
).toBeUndefined();
expect(notificationHandler).not.toHaveBeenCalled();
expect(middleware).not.toHaveBeenCalled();
});

it('handle: treats notifications as requests when no notification handler is specified', async () => {
const middleware = jest.fn().mockImplementation((_req, res, _next, end) => {
res.result = 'bar';
end();
});
const engine = new JsonRpcEngine();
engine.push(middleware);

expect(await engine.handle({ jsonrpc, method: 'foo' })).toStrictEqual({
jsonrpc,
result: 'bar',
id: undefined,
});
expect(middleware).toHaveBeenCalledTimes(1);
});

it('handle: forwards notifications to handlers', async () => {
const middleware = jest.fn();
const notificationHandler = jest.fn();
const engine = new JsonRpcEngine({ notificationHandler });
engine.push(middleware);

expect(await engine.handle({ jsonrpc, method: 'foo' })).toBeUndefined();
expect(notificationHandler).toHaveBeenCalledTimes(1);
expect(notificationHandler).toHaveBeenCalledWith({
jsonrpc,
method: 'foo',
});
expect(middleware).not.toHaveBeenCalled();
});

it('handle: re-throws errors from notification handlers (async)', async () => {
const notificationHandler = jest.fn().mockImplementation(() => {
throw new Error('baz');
});
const engine = new JsonRpcEngine({ notificationHandler });

await expect(engine.handle({ jsonrpc, method: 'foo' })).rejects.toThrow(
new Error('baz'),
);
expect(notificationHandler).toHaveBeenCalledTimes(1);
expect(notificationHandler).toHaveBeenCalledWith({
jsonrpc,
method: 'foo',
});
});

it('handle: re-throws errors from notification handlers (callback)', async () => {
const notificationHandler = jest.fn().mockImplementation(() => {
throw new Error('baz');
});
const engine = new JsonRpcEngine({ notificationHandler });

await new Promise<void>((resolve) => {
engine.handle({ jsonrpc, method: 'foo' }, (error, response) => {
expect(error).toStrictEqual(new Error('baz'));
expect(response).toBeUndefined();

expect(notificationHandler).toHaveBeenCalledTimes(1);
expect(notificationHandler).toHaveBeenCalledWith({
jsonrpc,
method: 'foo',
});
resolve();
});
});
});

it('handle: basic middleware test 1', async () => {
const engine = new JsonRpcEngine();

Expand Down
Loading