UNPKG

14.7 kBTypeScriptView Raw
1/**
2 * ! This file MUST NOT contain any import statement.
3 * ! This file is part of public API of this package (for Deno users).
4 */
5/**
6 * This interface represents a "on message" - "send response" model.
7 * @remarks
8 * Usually used for there is only 1 remote (act like a client).
9 * Example: {@link https://github.com/Jack-Works/async-call-rpc/blob/master/utils-src/node/websocket.server.ts | Example for EventBasedChannel}
10 * @public
11 */
12interface EventBasedChannel<Data = unknown> {
13 /**
14 * Register the message listener.
15 * @param listener - The message listener.
16 * @returns a function that unregister the listener.
17 */
18 on(listener: (data: Data) => void): void | (() => void);
19 /**
20 * Send the data to the remote side.
21 * @param data - The data should send to the remote side.
22 */
23 send(data: Data): void;
24}
25/**
26 * This interface represents a "callback" model.
27 * @remarks
28 * Usually used for there are many remotes (act like a server).
29 * Example: {@link https://github.com/Jack-Works/async-call-rpc/blob/master/utils-src/web/websocket.client.ts | Example for CallbackBasedChannel}
30 * @public
31 */
32interface CallbackBasedChannel<Data = unknown> extends Partial<EventBasedChannel<Data>> {
33 /**
34 * Setup the CallbackBasedChannel.
35 * @param jsonRPCHandlerCallback - A function that will execute the JSON RPC request then give the result back. If the result is undefined, it means no response is created.
36 * @param isValidPayload - A util function that will try to validate if the message is a valid JSON RPC request. It will be synchronous if possible.
37 * @returns a function that unregister the setup.
38 */
39 setup(jsonRPCHandlerCallback: (jsonRPCPayload: unknown) => Promise<unknown | undefined>, isValidJSONRPCPayload: (data: unknown) => boolean | Promise<boolean>): (() => void) | void;
40}
41/**
42 * Log options of AsyncCall
43 * @remarks
44 * This option controls how AsyncCall should log RPC calls to the console.
45 * @public
46 */
47interface AsyncCallLogLevel {
48 /**
49 * Log all requests to this instance
50 * @defaultValue true
51 */
52 beCalled?: boolean;
53 /**
54 * Log all errors produced when responding requests
55 * @defaultValue true
56 */
57 localError?: boolean;
58 /**
59 * Log remote errors
60 * @defaultValue true
61 */
62 remoteError?: boolean;
63 /**
64 * Send the call stack to the remote when making requests
65 * @defaultValue false
66 */
67 sendLocalStack?: boolean;
68 /**
69 * Control if AsyncCall should make the log better
70 * @remarks
71 * If use "pretty", it will call the logger with some CSS to make the log easier to read.
72 * Check out this article to know more about it: {@link https://dev.to/annlin/consolelog-with-css-style-1mmp | Console.log with CSS Style}
73 * @defaultValue 'pretty'
74 */
75 type?: 'basic' | 'pretty';
76 /**
77 * Log a function that allows to execute the request with same arguments again
78 * @remarks
79 * Do not use this options in the production environment because it will log a closure that captures the arguments of requests. This may cause memory leak.
80 * @defaultValue false
81 */
82 requestReplay?: boolean;
83}
84/**
85 * Control the behavior that different from the JSON RPC spec.
86 * @public
87 */
88interface AsyncCallStrictJSONRPC {
89 /**
90 * Controls if AsyncCall send an ErrorResponse when the requested method is not defined.
91 * @remarks
92 * Set this options to false, AsyncCall will ignore the request (but print a log) if the method is not defined.
93 * @defaultValue true
94 */
95 methodNotFound?: boolean;
96 /**
97 * Controls if AsyncCall send an ErrorResponse when the message is not valid.
98 * @remarks
99 * Set this options to false, AsyncCall will ignore the request that cannot be parsed as a valid JSON RPC payload. This is useful when the message channel is also used to transfer other kinds of messages.
100 * @defaultValue true
101 */
102 unknownMessage?: boolean;
103}
104/**
105 * Options for {@link AsyncCall}
106 * @public
107 */
108interface AsyncCallOptions {
109 /**
110 * This option is used for better log print.
111 * @defaultValue `rpc`
112 */
113 key?: string;
114 /**
115 * How to serialize and deserialize the JSON RPC payload
116 *
117 * @remarks
118 * See {@link Serialization}.
119 * There is some built-in serializer:
120 *
121 * - {@link NoSerialization} (Not doing anything to the message)
122 *
123 * - {@link JSONSerialization} (Using JSON.parse/stringify in the backend)
124 *
125 * - {@link https://github.com/jack-works/async-call-rpc#web-deno-and-node-bson | BSONSerialization} (use the {@link https://npmjs.org/bson | bson} as the serializer)
126 *
127 * @defaultValue {@link NoSerialization}
128 */
129 serializer?: Serialization;
130 /**
131 * Provide the logger of AsyncCall
132 * @remarks
133 * See {@link ConsoleInterface}
134 * @defaultValue globalThis.console
135 */
136 logger?: ConsoleInterface;
137 /**
138 * The message channel to exchange messages between server and client
139 * @example
140 * {@link https://github.com/Jack-Works/async-call-rpc/blob/master/utils-src/web/websocket.client.ts | Example for CallbackBasedChannel} or {@link https://github.com/Jack-Works/async-call-rpc/blob/master/utils-src/node/websocket.server.ts | Example for EventBasedChannel}
141 */
142 channel: CallbackBasedChannel | EventBasedChannel;
143 /**
144 * Choose log level.
145 * @remarks
146 * - `true` is a reasonable default value, which means all options are the default options in the {@link AsyncCallLogLevel}
147 *
148 * - `false` is disable all logs
149 *
150 * - `"all"` is enable all logs (stronger than `true`).
151 * @defaultValue true
152 */
153 log?: AsyncCallLogLevel | boolean | 'all';
154 /**
155 * Control the behavior that different from the JSON RPC spec. See {@link AsyncCallStrictJSONRPC}
156 * @remarks
157 * - `true` is to enable all strict options
158 * - `false` is to disable all strict options
159 * @defaultValue true
160 */
161 strict?: AsyncCallStrictJSONRPC | boolean;
162 /**
163 * Choose flavor of parameter structures defined in the spec
164 * @remarks
165 *
166 * See {@link https://www.jsonrpc.org/specification#parameter_structures}
167 *
168 * When using `by-name`, only first parameter of the requests are sent to the remote and it must be an object.
169 *
170 * @privateRemarks
171 * TODO: review the edge cases when using "by-name".
172 * @defaultValue "by-position"
173 */
174 parameterStructures?: 'by-position' | 'by-name';
175 /**
176 * Prefer local implementation than remote.
177 * @remarks
178 * If you call a RPC method and it is also defined in the local, open this flag will call the local implementation directly instead of send a RPC request. No logs / serialization will be performed if a local implementation is used.
179 * @defaultValue false
180 */
181 preferLocalImplementation?: boolean;
182 /**
183 * The ID generator of each JSON RPC request
184 * @defaultValue () =\> Math.random().toString(36).slice(2)
185 */
186 idGenerator?(): string | number;
187 /**
188 * Control how to report error response according to the exception
189 */
190 mapError?: ErrorMapFunction<unknown>;
191 /**
192 * If the instance should be "thenable".
193 * @defaultValue undefined
194 * @remarks
195 * If this options is set to `true`, it will return a `then` method normally (forwards the call to the remote).
196 *
197 * If this options is set to `false`, it will return `undefined` even the remote has a method called "then".
198 *
199 * If this options is set to `undefined`, it will return `undefined` and show a warning. You must explicitly set this option to `true` or `false` to dismiss the warning.
200 *
201 * The motivation of this option is to resolve the problem caused by Promise auto-unwrapping.
202 *
203 * Consider this code:
204 *
205 * ```ts
206 * async function getRPC() {
207 * return AsyncCall(...)
208 * }
209 * ```
210 *
211 * According to the JS semantics, it will invoke the "then" method immediately on the returning instance which is unwanted in most scenarios.
212 *
213 * To avoid this problem, methods called "then" are omitted from the type signatures. Strongly suggest to not use "then" as your RPC method name.
214 */
215 thenable?: boolean;
216}
217/**
218 * JSON RPC Request object
219 * @public
220 */
221declare type JSONRPCRequest = {
222 jsonrpc: '2.0';
223 id?: string | number | null;
224 method: string;
225 params: readonly unknown[] | object;
226};
227/**
228 * @public
229 * @param error - The exception
230 * @param request - The request object
231 */
232declare type ErrorMapFunction<T = unknown> = (error: unknown, request: Readonly<JSONRPCRequest>) => {
233 code: number;
234 message: string;
235 data?: T;
236};
237/**
238 * Make all function in the type T becomes async functions and filtering non-Functions out.
239 *
240 * @remarks
241 * Only generics signatures on function that returning an Promise<T> will be preserved due to the limitation of TypeScript.
242 *
243 * Method called `then` are intentionally removed because it is very likely to be a foot gun in promise auto-unwrap.
244 * @internal
245 */
246declare type _AsyncVersionOf<T> = {
247 readonly [key in keyof T as key extends 'then' ? never : T[key] extends Function ? key : never]: T[key] extends (...args: any) => Promise<any> ? T[key] : T[key] extends (...args: infer Args) => infer Return ? (...args: Args) => Promise<Return extends PromiseLike<infer U> ? U : Return> : never;
248};
249/**
250 * The minimal Console interface that AsyncCall needs.
251 * @public
252 * @remarks
253 * The method not provided will use "log" as it's fallback.
254 */
255interface ConsoleInterface {
256 warn?(...args: unknown[]): void;
257 debug?(...args: unknown[]): void;
258 log(...args: unknown[]): void;
259 groupCollapsed?(...args: unknown[]): void;
260 groupEnd?(...args: unknown[]): void;
261 error?(...args: unknown[]): void;
262}
263
264/**
265 * Serialize and deserialize of the JSON RPC payload
266 * @public
267 */
268interface Serialization {
269 /**
270 * Serialize data
271 * @param from - original data
272 */
273 serialization(from: any): unknown | PromiseLike<unknown>;
274 /**
275 * Deserialize data
276 * @param serialized - Serialized data
277 */
278 deserialization(serialized: unknown): unknown | PromiseLike<unknown>;
279}
280
281/**
282 * Make the returning type to `Promise<void>`
283 * @internal
284 * @remarks
285 * Due to the limitation of TypeScript, generic signatures cannot be preserved
286 * if the function is the top level parameter of this utility type,
287 * or the function is not returning `Promise<void>`.
288 */
289declare type _IgnoreResponse<T> = T extends (...args: infer Args) => unknown ? (...args: Args) => Promise<void> : {
290 [key in keyof T as T[key] extends Function ? key : never]: T[key] extends (...args: infer Args) => infer Return ? Return extends Promise<void> ? T[key] : (...args: Args) => Promise<void> : never;
291};
292/**
293 * Wrap the AsyncCall instance to send notification.
294 * @param instanceOrFnOnInstance - The AsyncCall instance or function on the AsyncCall instance
295 * @example
296 * const notifyOnly = notify(AsyncCall(...))
297 * @public
298 */
299declare function notify<T extends object>(instanceOrFnOnInstance: T): _IgnoreResponse<T>;
300
301/**
302 * Serialization implementation that do nothing
303 * @remarks {@link Serialization}
304 * @public
305 */
306declare const NoSerialization: Serialization;
307/**
308 * Create a serialization by JSON.parse/stringify
309 *
310 * @param replacerAndReceiver - Replacer and receiver of JSON.parse/stringify
311 * @param space - Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
312 * @param undefinedKeepingBehavior - How to keep "undefined" in result of SuccessResponse?
313 *
314 * If it is not handled properly, JSON.stringify will emit an invalid JSON RPC object.
315 *
316 * Options:
317 * - `"null"`(**default**): convert it to null.
318 * - `"keep"`: try to keep it by additional property "undef".
319 * - `false`: Don't keep it, let it break.
320 * @remarks {@link Serialization}
321 * @public
322 */
323declare const JSONSerialization: (replacerAndReceiver?: [(((key: string, value: any) => any) | undefined)?, (((key: string, value: any) => any) | undefined)?], space?: string | number | undefined, undefinedKeepingBehavior?: 'keep' | 'null' | false) => Serialization;
324
325/**
326 * Wrap the AsyncCall instance to use batch call.
327 * @param asyncCallInstance - The AsyncCall instance
328 * @example
329 * const [batched, send, drop] = batch(AsyncCall(...))
330 * batched.call1() // pending
331 * batched.call2() // pending
332 * send() // send all pending requests
333 * drop() // drop all pending requests
334 * @returns It will return a tuple.
335 *
336 * The first item is the batched version of AsyncCall instance passed in.
337 *
338 * The second item is a function to send all pending requests.
339 *
340 * The third item is a function to drop and reject all pending requests.
341 * @public
342 */
343declare function batch<T extends object>(asyncCallInstance: T): [T, () => void, (error?: unknown) => void];
344
345/**
346 * Create a RPC server & client.
347 *
348 * @remarks
349 * See {@link AsyncCallOptions}
350 *
351 * thisSideImplementation can be a Promise so you can write:
352 *
353 * ```ts
354 * export const service = AsyncCall(typeof window === 'object' ? {} : import('./backend/service.js'), {})
355 * ```
356 *
357 * @param thisSideImplementation - The implementation when this AsyncCall acts as a JSON RPC server. Can be a Promise.
358 * @param options - {@link AsyncCallOptions}
359 * @typeParam OtherSideImplementedFunctions - The type of the API that server expose. For any function on this interface, it will be converted to the async version.
360 * @returns Same as the `OtherSideImplementedFunctions` type parameter, but every function in that interface becomes async and non-function value is removed. Method called "then" are also removed.
361 * @public
362 */
363declare function AsyncCall<OtherSideImplementedFunctions = {}>(thisSideImplementation: null | undefined | object | Promise<object>, options: AsyncCallOptions): _AsyncVersionOf<OtherSideImplementedFunctions>;
364
365export { AsyncCall, AsyncCallLogLevel, AsyncCallOptions, AsyncCallStrictJSONRPC, CallbackBasedChannel, ConsoleInterface as Console, ConsoleInterface, ErrorMapFunction, EventBasedChannel, JSONRPCRequest, JSONSerialization, NoSerialization, Serialization, _AsyncVersionOf, _IgnoreResponse, batch, notify };
366
\No newline at end of file