Skip to content

Commit f30da25

Browse files
committed
Works as a standalone library now. Pending review to merge to staging.
callers and handlers are now refactored * WIP - Newline now works, refers issue #1 node v20 fix feat: handlers implementations are now abstract arrow functions * Fixes #5 [ci skip] * resolves issue 5, makes RPC handlers abstract arrow function properties feat: rename to uppercase [ci skip] fix: handler export fix [ci skip] fix: tsconf from quic [ci skip] fix: dependencies (js quic), events and errors versions, changing to relative imports, jest dev dependency, js-quic tsconfig [ci skip] fix: tests imports, using @ [ci skip] chore: removed sysexits chore: fix default exports for callers and handlers Fixed index for handlers fix: remove @matrixai/id fix: remove @matrixai/id and ix chore : diagram [ci skip] chore : lintfix fix: errors now extend AbstractError [ci skip] fix: undoing fix #1 [ci skip] replacd errorCode with just code, references std error codes from rpc spec feat: events based createDestroy [ci skip] chore: img format fix [ci skip] chore: img in README.md [ci skip] feat: allows the user to pass in a generator function if the user wishes to specify a particular id [ci skip] fix: fixes #7 * Removes graceTimer and related jests chore: idGen name change. idGen parameter in creation and constructor. No longer optional. Only defaulted in one place. wip: added idgen to jests, was missing. [ci skip] wip: reimported ix, since a few tests rely on it. removed, matrixai/id wip: jests for #4 removed, matrixai/id wip: * Implements custom RPC Error codes. * Fixed jest for concurrent timeouts * All errors now have a cause * All errors now use custom error codes. wip: *Client uses ctx timer now wip: *Jests to test concurrency wip: *custom RPC based errors for RPC Client, now all errors have a cause and an error message WIP: * Refactor out sensitiveReplacer WIP: * Refactor out sensitiveReplacer WIP: * Update to latest async init and events * set default timeout to Infinity * jest to check server and client with infinite timeout * fixing jests which broke after changing default timeout to infinity WIP: f1x #4 WIP: f1x #11 f1x: parameterize toError, fromError and replacer wip: tofrom fix: parameterize toError, fromError and replacer fix: Makes concurrent jests non deterministic * Related #4 fix: parameterize replacer toError and fromError, change fromError to return JSONValue, stringify fromError usages * Related #10 fix: Converted global state for fromError to handle it internally. *Related: #10 Reviewed-by: @tegefaulkes [ci skip] chore: Jests for fromError and toError, and using a custom replacer. related: #10 [ci skip]
1 parent c162788 commit f30da25

37 files changed

+1944
-1247
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,6 @@ npm publish --access public
6666
git push
6767
git push --tags
6868
```
69+
70+
Domains Diagram:
71+
![diagram_encapuslated.svg](images%2Fdiagram_encapuslated.svg)

images/diagram_encapuslated.svg

+17
Loading

package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,15 @@
5353
"ts-node": "^10.9.1",
5454
"tsconfig-paths": "^3.9.0",
5555
"typedoc": "^0.23.21",
56-
"typescript": "^4.9.3"
56+
"typescript": "^4.9.3",
57+
"@fast-check/jest": "^1.1.0"
5758
},
5859
"dependencies": {
59-
"@fast-check/jest": "^1.7.2",
60-
"@matrixai/async-init": "^1.9.1",
60+
"@matrixai/async-init": "^1.9.4",
6161
"@matrixai/contexts": "^1.2.0",
62-
"@matrixai/id": "^3.3.6",
6362
"@matrixai/logger": "^3.1.0",
63+
"@matrixai/errors": "^1.2.0",
64+
"@matrixai/events": "^3.2.0",
6465
"@streamparser/json": "^0.0.17",
6566
"ix": "^5.0.0"
6667
}

src/RPCClient.ts

+91-23
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,45 @@ import type {
88
RPCStream,
99
JSONRPCResponseResult,
1010
} from './types';
11-
import type { JSONValue } from './types';
11+
import type { JSONValue, IdGen } from './types';
1212
import type {
1313
JSONRPCRequest,
1414
JSONRPCResponse,
1515
MiddlewareFactory,
1616
MapCallers,
1717
} from './types';
18+
import type { ErrorRPCRemote } from './errors';
1819
import { CreateDestroy, ready } from '@matrixai/async-init/dist/CreateDestroy';
1920
import Logger from '@matrixai/logger';
2021
import { Timer } from '@matrixai/timer';
22+
import { createDestroy } from '@matrixai/async-init';
2123
import * as rpcUtilsMiddleware from './utils/middleware';
2224
import * as rpcErrors from './errors';
2325
import * as rpcUtils from './utils/utils';
2426
import { promise } from './utils';
25-
import { never } from './errors';
27+
import { ErrorRPCStreamEnded, never } from './errors';
28+
import * as events from './events';
2629

2730
const timerCleanupReasonSymbol = Symbol('timerCleanUpReasonSymbol');
2831

29-
// eslint-disable-next-line
30-
interface RPCClient<M extends ClientManifest> extends CreateDestroy {}
31-
@CreateDestroy()
32+
/**
33+
* Events:
34+
* - {@link events.Event}
35+
*/
36+
interface RPCClient<M extends ClientManifest>
37+
extends createDestroy.CreateDestroy {}
38+
/**
39+
* You must provide an error handler `addEventListener('error')`.
40+
* Otherwise, errors will just be ignored.
41+
*
42+
* Events:
43+
* - {@link events.EventRPCClientDestroy}
44+
* - {@link events.EventRPCClientDestroyed}
45+
*/
46+
@createDestroy.CreateDestroy({
47+
eventDestroy: events.EventRPCClientDestroy,
48+
eventDestroyed: events.EventRPCClientDestroyed,
49+
})
3250
class RPCClient<M extends ClientManifest> {
3351
/**
3452
* @param obj
@@ -49,8 +67,9 @@ class RPCClient<M extends ClientManifest> {
4967
manifest,
5068
streamFactory,
5169
middlewareFactory = rpcUtilsMiddleware.defaultClientMiddlewareWrapper(),
52-
streamKeepAliveTimeoutTime = 60_000, // 1 minute
70+
streamKeepAliveTimeoutTime = Infinity, // 1 minute
5371
logger = new Logger(this.name),
72+
idGen = () => Promise.resolve(null),
5473
}: {
5574
manifest: M;
5675
streamFactory: StreamFactory;
@@ -62,6 +81,8 @@ class RPCClient<M extends ClientManifest> {
6281
>;
6382
streamKeepAliveTimeoutTime?: number;
6483
logger?: Logger;
84+
idGen: IdGen;
85+
toError?: (errorData, metadata?: JSONValue) => ErrorRPCRemote<unknown>;
6586
}) {
6687
logger.info(`Creating ${this.name}`);
6788
const rpcClient = new this({
@@ -70,11 +91,13 @@ class RPCClient<M extends ClientManifest> {
7091
middlewareFactory,
7192
streamKeepAliveTimeoutTime: streamKeepAliveTimeoutTime,
7293
logger,
94+
idGen,
7395
});
7496
logger.info(`Created ${this.name}`);
7597
return rpcClient;
7698
}
77-
99+
protected onTimeoutCallback?: () => void;
100+
protected idGen: IdGen;
78101
protected logger: Logger;
79102
protected streamFactory: StreamFactory;
80103
protected middlewareFactory: MiddlewareFactory<
@@ -84,6 +107,10 @@ class RPCClient<M extends ClientManifest> {
84107
Uint8Array
85108
>;
86109
protected callerTypes: Record<string, HandlerType>;
110+
toError: (errorData: any, metadata?: JSONValue) => Error;
111+
public registerOnTimeoutCallback(callback: () => void) {
112+
this.onTimeoutCallback = callback;
113+
}
87114
// Method proxies
88115
public readonly streamKeepAliveTimeoutTime: number;
89116
public readonly methodsProxy = new Proxy(
@@ -116,6 +143,8 @@ class RPCClient<M extends ClientManifest> {
116143
middlewareFactory,
117144
streamKeepAliveTimeoutTime,
118145
logger,
146+
idGen = () => Promise.resolve(null),
147+
toError,
119148
}: {
120149
manifest: M;
121150
streamFactory: StreamFactory;
@@ -127,20 +156,39 @@ class RPCClient<M extends ClientManifest> {
127156
>;
128157
streamKeepAliveTimeoutTime: number;
129158
logger: Logger;
159+
idGen: IdGen;
160+
toError?: (errorData, metadata?: JSONValue) => ErrorRPCRemote<unknown>;
130161
}) {
162+
this.idGen = idGen;
131163
this.callerTypes = rpcUtils.getHandlerTypes(manifest);
132164
this.streamFactory = streamFactory;
133165
this.middlewareFactory = middlewareFactory;
134166
this.streamKeepAliveTimeoutTime = streamKeepAliveTimeoutTime;
135167
this.logger = logger;
168+
this.toError = toError || rpcUtils.toError;
136169
}
137170

138-
public async destroy(): Promise<void> {
171+
public async destroy({
172+
errorCode = rpcErrors.JSONRPCErrorCode.RPCStopping,
173+
errorMessage = '',
174+
force = true,
175+
}: {
176+
errorCode?: number;
177+
errorMessage?: string;
178+
force?: boolean;
179+
} = {}): Promise<void> {
139180
this.logger.info(`Destroying ${this.constructor.name}`);
181+
182+
// You can dispatch an event before the actual destruction starts
183+
this.dispatchEvent(new events.EventRPCClientDestroy());
184+
185+
// Dispatch an event after the client has been destroyed
186+
this.dispatchEvent(new events.EventRPCClientDestroyed());
187+
140188
this.logger.info(`Destroyed ${this.constructor.name}`);
141189
}
142190

143-
@ready(new rpcErrors.ErrorRPCDestroyed())
191+
@ready(new rpcErrors.ErrorRPCCallerFailed())
144192
public get methods(): MapCallers<M> {
145193
return this.methodsProxy as MapCallers<M>;
146194
}
@@ -154,7 +202,7 @@ class RPCClient<M extends ClientManifest> {
154202
* the provided I type.
155203
* @param ctx - ContextTimed used for timeouts and cancellation.
156204
*/
157-
@ready(new rpcErrors.ErrorRPCDestroyed())
205+
@ready(new rpcErrors.ErrorMissingCaller())
158206
public async unaryCaller<I extends JSONValue, O extends JSONValue>(
159207
method: string,
160208
parameters: I,
@@ -167,7 +215,9 @@ class RPCClient<M extends ClientManifest> {
167215
await writer.write(parameters);
168216
const output = await reader.read();
169217
if (output.done) {
170-
throw new rpcErrors.ErrorRPCMissingResponse();
218+
throw new rpcErrors.ErrorMissingCaller('Missing response', {
219+
cause: ctx.signal?.reason,
220+
});
171221
}
172222
await reader.cancel();
173223
await writer.close();
@@ -189,7 +239,7 @@ class RPCClient<M extends ClientManifest> {
189239
* the provided I type.
190240
* @param ctx - ContextTimed used for timeouts and cancellation.
191241
*/
192-
@ready(new rpcErrors.ErrorRPCDestroyed())
242+
@ready(new rpcErrors.ErrorRPCCallerFailed())
193243
public async serverStreamCaller<I extends JSONValue, O extends JSONValue>(
194244
method: string,
195245
parameters: I,
@@ -218,7 +268,7 @@ class RPCClient<M extends ClientManifest> {
218268
* @param method - Method name of the RPC call
219269
* @param ctx - ContextTimed used for timeouts and cancellation.
220270
*/
221-
@ready(new rpcErrors.ErrorRPCDestroyed())
271+
@ready(new rpcErrors.ErrorRPCCallerFailed())
222272
public async clientStreamCaller<I extends JSONValue, O extends JSONValue>(
223273
method: string,
224274
ctx: Partial<ContextTimedInput> = {},
@@ -230,7 +280,9 @@ class RPCClient<M extends ClientManifest> {
230280
const reader = callerInterface.readable.getReader();
231281
const output = reader.read().then(({ value, done }) => {
232282
if (done) {
233-
throw new rpcErrors.ErrorRPCMissingResponse();
283+
throw new rpcErrors.ErrorMissingCaller('Missing response', {
284+
cause: ctx.signal?.reason,
285+
});
234286
}
235287
return value;
236288
});
@@ -251,7 +303,7 @@ class RPCClient<M extends ClientManifest> {
251303
* @param method - Method name of the RPC call
252304
* @param ctx - ContextTimed used for timeouts and cancellation.
253305
*/
254-
@ready(new rpcErrors.ErrorRPCDestroyed())
306+
@ready(new rpcErrors.ErrorRPCCallerFailed())
255307
public async duplexStreamCaller<I extends JSONValue, O extends JSONValue>(
256308
method: string,
257309
ctx: Partial<ContextTimedInput> = {},
@@ -294,10 +346,16 @@ class RPCClient<M extends ClientManifest> {
294346
signal.addEventListener('abort', abortRacePromHandler);
295347
};
296348
// Setting up abort events for timeout
297-
const timeoutError = new rpcErrors.ErrorRPCTimedOut();
349+
const timeoutError = new rpcErrors.ErrorRPCTimedOut(
350+
'Error RPC has timed out',
351+
{ cause: ctx.signal?.reason },
352+
);
298353
void timer.then(
299354
() => {
300355
abortController.abort(timeoutError);
356+
if (this.onTimeoutCallback) {
357+
this.onTimeoutCallback();
358+
}
301359
},
302360
() => {}, // Ignore cancellation error
303361
);
@@ -310,13 +368,17 @@ class RPCClient<M extends ClientManifest> {
310368
} catch (e) {
311369
cleanUp();
312370
void streamFactoryProm.then((stream) =>
313-
stream.cancel(Error('TMP stream timed out early')),
371+
stream.cancel(ErrorRPCStreamEnded),
314372
);
315373
throw e;
316374
}
317375
void timer.then(
318376
() => {
319-
rpcStream.cancel(new rpcErrors.ErrorRPCTimedOut());
377+
rpcStream.cancel(
378+
new rpcErrors.ErrorRPCTimedOut('RPC has timed out', {
379+
cause: ctx.signal?.reason,
380+
}),
381+
);
320382
},
321383
() => {}, // Ignore cancellation error
322384
);
@@ -379,8 +441,9 @@ class RPCClient<M extends ClientManifest> {
379441
* single RPC message that is sent to specify the method for the RPC call.
380442
* Any metadata of extra parameters is provided here.
381443
* @param ctx - ContextTimed used for timeouts and cancellation.
444+
* @param id - Id is generated only once, and used throughout the stream for the rest of the communication
382445
*/
383-
@ready(new rpcErrors.ErrorRPCDestroyed())
446+
@ready(new rpcErrors.ErrorRPCCallerFailed())
384447
public async rawStreamCaller(
385448
method: string,
386449
headerParams: JSONValue,
@@ -430,7 +493,9 @@ class RPCClient<M extends ClientManifest> {
430493
signal.addEventListener('abort', abortRacePromHandler);
431494
};
432495
// Setting up abort events for timeout
433-
const timeoutError = new rpcErrors.ErrorRPCTimedOut();
496+
const timeoutError = new rpcErrors.ErrorRPCTimedOut('RPC has timed out', {
497+
cause: ctx.signal?.reason,
498+
});
434499
void timer.then(
435500
() => {
436501
abortController.abort(timeoutError);
@@ -457,11 +522,12 @@ class RPCClient<M extends ClientManifest> {
457522
abortProm.p,
458523
]);
459524
const tempWriter = rpcStream.writable.getWriter();
525+
const id = await this.idGen();
460526
const header: JSONRPCRequestMessage = {
461527
jsonrpc: '2.0',
462528
method,
463529
params: headerParams,
464-
id: null,
530+
id,
465531
};
466532
await tempWriter.write(Buffer.from(JSON.stringify(header)));
467533
tempWriter.releaseLock();
@@ -484,11 +550,13 @@ class RPCClient<M extends ClientManifest> {
484550
...(rpcStream.meta ?? {}),
485551
command: method,
486552
};
487-
throw rpcUtils.toError(messageValue.error.data, metadata);
553+
throw this.toError(messageValue.error.data, metadata);
488554
}
489555
leadingMessage = messageValue;
490556
} catch (e) {
491-
rpcStream.cancel(Error('TMP received error in leading response'));
557+
rpcStream.cancel(
558+
new ErrorRPCStreamEnded('RPC Stream Ended', { cause: e }),
559+
);
492560
throw e;
493561
}
494562
tempReader.releaseLock();

0 commit comments

Comments
 (0)