Skip to content

Commit aa5c384

Browse files
committed
feat: add optional reply callback to handler
1 parent 5439c78 commit aa5c384

File tree

3 files changed

+97
-16
lines changed

3 files changed

+97
-16
lines changed

README.md

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -512,26 +512,48 @@ In some circumstances, the final `code` and `reason` returned may be different f
512512

513513
#### client.handle([method,] handler)
514514

515-
* `method` {String} - The name of the method to be handled. If not provided, acts as a wildcard handler which will handle any call that doesn't have a more specific handler already registered.
516-
* `handler` {Function} - The function to be invoked when attempting to handle a call. Can return a `Promise`.
515+
* `method` {String} - The name of the method to be handled. If not provided, acts as a "wildcard" handler which will handle any call that doesn't have a more specific handler already registered.
516+
* `handler` {Function} - The function to be invoked when attempting to handle a call.
517517

518-
Register a call handler. Only one wildcard handler and one method-specific handler can be registered at a time. Attempting to register a handler with a duplicate method will override the former.
518+
Registers a call handler. Only one "wildcard" handler can be registered at once. Likewise, attempting to register a handler for a method which is already being handled will override the former handler.
519519

520520
When the `handler` function is invoked, it will be passed an object with the following properties:
521521
* `method` {String} - The name of the method being invoked (useful for wildcard handlers).
522-
* `params` {*} - The `params` value passed to the call.
522+
* `params` {*} - The parameters of the call.
523523
* `signal` {AbortSignal} - A signal which will abort if the underlying connection is dropped (therefore, the response will never be received by the caller). You may choose whether to ignore the signal or not, but it could save you some time if you use it to abort the call early.
524524
* `messageId` {String} - The OCPP Message ID used in the call.
525+
* `reply` {Function} - A callback function with which to pass a response to the call. Accepts a response value, an `Error`, or a `Promise`.
525526

526-
If the invocation of the `handler` resolves or returns, the resolved value will be returned to the caller.
527-
If the invocation of the `handler` rejects or throws, an error will be passed to the caller. By default, the error will be an instance of `RPCGenericError`, although additional error types are possible ([see createRPCError](#createrpcerrortype-message-details)).
528-
If the `handler` returns a `NOREPLY` symbol then no reply will be sent. It will be your responsibility to send the reply by some other means (such as [`sendRaw()`](#clientsendrawmessage)).
527+
Responses to handled calls are sent according to these rules:
528+
* If a value (or a `Promise` which resolves to a value) is passed to `reply()`, a **CALLRESULT** will be sent with this value as the result.
529+
* If an `Error` (or a `Promise` which rejects with an `Error`) is passed to `reply()`, a **CALLERROR** will be sent instead. (The `Error` may be coerced into an `RPCError`. You can use [createRPCError()](#createrpcerrortype-message-details) to reply with a specific RPC error code.)
530+
* If the `NOREPLY` symbol is passed to `reply()`, then no response will be sent. It will then be your responsibility to send the response by some other means (such as with [`sendRaw()`](#clientsendrawmessage)).
531+
* If the `handler` returns or throws before `reply()` is called, then the `reply()` callback will be called implicitly with the returned value (or thrown `Error`).
532+
* Calling `reply()` more than once, or returning/throwing after calling `reply()` is considered a no-op, will not result in any additional responses being sent, and has no effect.
529533

530-
##### Example of NOREPLY
534+
##### Example of handling an OCPP1.6 Heartbeat
535+
536+
```js
537+
client.handle('Heartbeat', ({reply}) => {
538+
reply({ currentTime: new Date().toISOString() });
539+
});
540+
541+
// or...
542+
client.handle('Heartbeat', () => {
543+
return { currentTime: new Date().toISOString() };
544+
});
545+
```
546+
547+
##### Example of using NOREPLY
531548

532549
```js
533550
const {NOREPLY} = require('ocpp-rpc');
534551

552+
client.handle('WontReply', ({reply}) => {
553+
reply(NOREPLY);
554+
});
555+
556+
// or...
535557
client.handle('WontReply', () => {
536558
return NOREPLY;
537559
});

lib/client.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -728,12 +728,27 @@ class RPCClient extends EventEmitter {
728728
}
729729

730730
const ac = new AbortController();
731-
const callPromise = Promise.resolve(handler({
732-
messageId: msgId,
733-
method,
734-
params,
735-
signal: ac.signal,
736-
}));
731+
const callPromise = new Promise(async (resolve, reject) => {
732+
function reply(val) {
733+
if (val instanceof Error) {
734+
reject(val);
735+
} else {
736+
resolve(val);
737+
}
738+
}
739+
740+
try {
741+
reply(await handler({
742+
messageId: msgId,
743+
method,
744+
params,
745+
signal: ac.signal,
746+
reply,
747+
}));
748+
} catch (err) {
749+
reply(err);
750+
}
751+
});
737752

738753
const pending = {abort: ac.abort.bind(ac), promise: callPromise};
739754
this._pendingResponses.set(msgId, pending);

test/client.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2061,8 +2061,8 @@ describe('RPCClient', function(){
20612061

20622062
const {endpoint, close} = await createServer({}, {
20632063
withClient: cli => {
2064-
cli.handle('NoReply', () => {
2065-
return NOREPLY;
2064+
cli.handle('NoReply', ({reply}) => {
2065+
reply(NOREPLY);
20662066
});
20672067
}
20682068
});
@@ -2122,6 +2122,50 @@ describe('RPCClient', function(){
21222122

21232123
});
21242124

2125+
2126+
it('can reply early before return/throw', async () => {
2127+
2128+
const {endpoint, close} = await createServer({}, {
2129+
withClient: cli => {
2130+
cli.handle('ResolveEarly', async ({reply}) => {
2131+
reply("early");
2132+
return "late";
2133+
});
2134+
cli.handle('ResolveBeforeThrow', async ({reply}) => {
2135+
reply("early");
2136+
throw Error("late");
2137+
});
2138+
cli.handle('RejectEarly', async ({reply}) => {
2139+
reply(Error("early"));
2140+
throw Error("late");
2141+
});
2142+
cli.handle('RejectBeforeReturn', async ({reply}) => {
2143+
reply(Error("early"));
2144+
return "late";
2145+
});
2146+
}
2147+
});
2148+
const cli = new RPCClient({
2149+
endpoint,
2150+
identity: 'X',
2151+
});
2152+
2153+
try {
2154+
await cli.connect();
2155+
2156+
assert.equal(await cli.call('ResolveEarly'), "early");
2157+
const err = await cli.call('RejectEarly').catch(e=>e);
2158+
assert.equal(err.message, "early");
2159+
await assert.rejects(cli.call('RejectBeforeReturn'));
2160+
assert.equal(await cli.call("ResolveBeforeThrow"), "early");
2161+
2162+
} finally {
2163+
await cli.close();
2164+
close();
2165+
}
2166+
2167+
});
2168+
21252169
});
21262170

21272171

0 commit comments

Comments
 (0)