Skip to content

Commit dab5cd8

Browse files
authored
Merge pull request #498 from MatrixAI/feature-agnostic_rpc
Transport Agnostic RPC implementation
2 parents f3092dd + 71833a0 commit dab5cd8

27 files changed

+4868
-3
lines changed

package-lock.json

Lines changed: 58 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
"@peculiar/webcrypto": "^1.4.0",
9797
"@peculiar/x509": "^1.8.3",
9898
"@scure/bip39": "^1.1.0",
99+
"@types/ws": "^8.5.4",
99100
"ajv": "^7.0.4",
100101
"bip39": "^3.0.3",
101102
"canonicalize": "^1.0.5",
@@ -118,13 +119,15 @@
118119
"resource-counter": "^1.2.4",
119120
"sodium-native": "^3.4.1",
120121
"threads": "^1.6.5",
121-
"utp-native": "^2.5.3",
122122
"tslib": "^2.4.0",
123-
"tsyringe": "^4.7.0"
123+
"tsyringe": "^4.7.0",
124+
"utp-native": "^2.5.3",
125+
"ws": "^8.12.0"
124126
},
125127
"devDependencies": {
126128
"@babel/preset-env": "^7.13.10",
127129
"@fast-check/jest": "^1.1.0",
130+
"@streamparser/json": "^0.0.12",
128131
"@swc/core": "^1.2.215",
129132
"@types/cross-spawn": "^6.0.2",
130133
"@types/google-protobuf": "^3.7.4",

src/RPC/RPCClient.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import type {
2+
HandlerType,
3+
JsonRpcRequestMessage,
4+
StreamPairCreateCallback,
5+
ClientManifest,
6+
} from './types';
7+
import type { JSONValue } from 'types';
8+
import type {
9+
ReadableWritablePair,
10+
WritableStream,
11+
ReadableStream,
12+
} from 'stream/web';
13+
import type {
14+
JsonRpcRequest,
15+
JsonRpcResponse,
16+
MiddlewareFactory,
17+
MapCallers,
18+
} from './types';
19+
import { CreateDestroy, ready } from '@matrixai/async-init/dist/CreateDestroy';
20+
import Logger from '@matrixai/logger';
21+
import * as middlewareUtils from './middleware';
22+
import * as rpcErrors from './errors';
23+
import * as rpcUtils from './utils';
24+
import {
25+
clientInputTransformStream,
26+
clientOutputTransformStream,
27+
} from './utils';
28+
import { never } from '../utils';
29+
30+
// eslint-disable-next-line
31+
interface RPCClient<M extends ClientManifest> extends CreateDestroy {}
32+
@CreateDestroy()
33+
class RPCClient<M extends ClientManifest> {
34+
static async createRPCClient<M extends ClientManifest>({
35+
manifest,
36+
streamPairCreateCallback,
37+
middleware = middlewareUtils.defaultClientMiddlewareWrapper(),
38+
logger = new Logger(this.name),
39+
}: {
40+
manifest: M;
41+
streamPairCreateCallback: StreamPairCreateCallback;
42+
middleware?: MiddlewareFactory<
43+
Uint8Array,
44+
JsonRpcRequest,
45+
JsonRpcResponse,
46+
Uint8Array
47+
>;
48+
logger?: Logger;
49+
}) {
50+
logger.info(`Creating ${this.name}`);
51+
const rpcClient = new this({
52+
manifest,
53+
streamPairCreateCallback,
54+
middleware,
55+
logger,
56+
});
57+
logger.info(`Created ${this.name}`);
58+
return rpcClient;
59+
}
60+
61+
protected logger: Logger;
62+
protected streamPairCreateCallback: StreamPairCreateCallback;
63+
protected middleware: MiddlewareFactory<
64+
Uint8Array,
65+
JsonRpcRequest,
66+
JsonRpcResponse,
67+
Uint8Array
68+
>;
69+
protected callerTypes: Record<string, HandlerType>;
70+
// Method proxies
71+
public readonly methodsProxy = new Proxy(
72+
{},
73+
{
74+
get: (_, method) => {
75+
if (typeof method === 'symbol') throw never();
76+
switch (this.callerTypes[method]) {
77+
case 'UNARY':
78+
return (params) => this.unaryCaller(method, params);
79+
case 'SERVER':
80+
return (params) => this.serverStreamCaller(method, params);
81+
case 'CLIENT':
82+
return () => this.clientStreamCaller(method);
83+
case 'DUPLEX':
84+
return () => this.duplexStreamCaller(method);
85+
case 'RAW':
86+
return (header) => this.rawStreamCaller(method, header);
87+
default:
88+
return;
89+
}
90+
},
91+
},
92+
);
93+
94+
public constructor({
95+
manifest,
96+
streamPairCreateCallback,
97+
middleware,
98+
logger,
99+
}: {
100+
manifest: M;
101+
streamPairCreateCallback: StreamPairCreateCallback;
102+
middleware: MiddlewareFactory<
103+
Uint8Array,
104+
JsonRpcRequest,
105+
JsonRpcResponse,
106+
Uint8Array
107+
>;
108+
logger: Logger;
109+
}) {
110+
this.callerTypes = rpcUtils.getHandlerTypes(manifest);
111+
this.streamPairCreateCallback = streamPairCreateCallback;
112+
this.middleware = middleware;
113+
this.logger = logger;
114+
}
115+
116+
public async destroy(): Promise<void> {
117+
this.logger.info(`Destroying ${this.constructor.name}`);
118+
this.logger.info(`Destroyed ${this.constructor.name}`);
119+
}
120+
121+
@ready(new rpcErrors.ErrorRpcDestroyed())
122+
public get methods(): MapCallers<M> {
123+
return this.methodsProxy as MapCallers<M>;
124+
}
125+
126+
@ready(new rpcErrors.ErrorRpcDestroyed())
127+
public async unaryCaller<I extends JSONValue, O extends JSONValue>(
128+
method: string,
129+
parameters: I,
130+
): Promise<O> {
131+
const callerInterface = await this.duplexStreamCaller<I, O>(method);
132+
const reader = callerInterface.readable.getReader();
133+
const writer = callerInterface.writable.getWriter();
134+
await writer.write(parameters);
135+
const output = await reader.read();
136+
if (output.done) {
137+
throw new rpcErrors.ErrorRpcRemoteError('Stream ended before response');
138+
}
139+
await reader.cancel();
140+
await writer.close();
141+
return output.value;
142+
}
143+
144+
@ready(new rpcErrors.ErrorRpcDestroyed())
145+
public async serverStreamCaller<I extends JSONValue, O extends JSONValue>(
146+
method: string,
147+
parameters: I,
148+
): Promise<ReadableStream<O>> {
149+
const callerInterface = await this.duplexStreamCaller<I, O>(method);
150+
const writer = callerInterface.writable.getWriter();
151+
await writer.write(parameters);
152+
await writer.close();
153+
154+
return callerInterface.readable;
155+
}
156+
157+
@ready(new rpcErrors.ErrorRpcDestroyed())
158+
public async clientStreamCaller<I extends JSONValue, O extends JSONValue>(
159+
method: string,
160+
): Promise<{
161+
output: Promise<O>;
162+
writable: WritableStream<I>;
163+
}> {
164+
const callerInterface = await this.duplexStreamCaller<I, O>(method);
165+
const reader = callerInterface.readable.getReader();
166+
const output = reader.read().then(({ value, done }) => {
167+
if (done) {
168+
throw new rpcErrors.ErrorRpcRemoteError('Stream ended before response');
169+
}
170+
return value;
171+
});
172+
return {
173+
output,
174+
writable: callerInterface.writable,
175+
};
176+
}
177+
178+
@ready(new rpcErrors.ErrorRpcDestroyed())
179+
public async duplexStreamCaller<I extends JSONValue, O extends JSONValue>(
180+
method: string,
181+
): Promise<ReadableWritablePair<O, I>> {
182+
const outputMessageTransformStream = clientOutputTransformStream<O>();
183+
const inputMessageTransformStream = clientInputTransformStream<I>(method);
184+
const middleware = this.middleware();
185+
// Hooking up agnostic stream side
186+
const streamPair = await this.streamPairCreateCallback();
187+
void streamPair.readable
188+
.pipeThrough(middleware.reverse)
189+
.pipeTo(outputMessageTransformStream.writable)
190+
.catch(() => {});
191+
void inputMessageTransformStream.readable
192+
.pipeThrough(middleware.forward)
193+
.pipeTo(streamPair.writable)
194+
.catch(() => {});
195+
196+
// Returning interface
197+
return {
198+
readable: outputMessageTransformStream.readable,
199+
writable: inputMessageTransformStream.writable,
200+
};
201+
}
202+
203+
@ready(new rpcErrors.ErrorRpcDestroyed())
204+
public async rawStreamCaller(
205+
method: string,
206+
headerParams: JSONValue,
207+
): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> {
208+
const streamPair = await this.streamPairCreateCallback();
209+
const tempWriter = streamPair.writable.getWriter();
210+
const header: JsonRpcRequestMessage = {
211+
jsonrpc: '2.0',
212+
method,
213+
params: headerParams,
214+
id: null,
215+
};
216+
await tempWriter.write(Buffer.from(JSON.stringify(header)));
217+
tempWriter.releaseLock();
218+
return streamPair;
219+
}
220+
}
221+
222+
export default RPCClient;

0 commit comments

Comments
 (0)