|
| 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