/** * A light implementation of {@link https://www.jsonrpc.org/specification | JSON RPC 2.0} * @packageDocumentation * @remarks * See the introduction at {@link https://github.com/Jack-Works/async-call | Github} */ /** * AbortSignal * @public * @see {@link https://mdn.io/AbortSignal} * @remarks * This is a subset of the AbortSignal interface defined in the [WinterCG](https://wintercg.org/). */ export declare interface AbortSignalLike { readonly aborted: boolean; addEventListener(type: 'abort', listener: () => void, options: { once: boolean; }): void; removeEventListener(type: 'abort', listener: () => void): void; throwIfAborted(): void; reason: any; } /** * Create a RPC server & client. * * @remarks * See {@link AsyncCallOptions} * * thisSideImplementation can be a Promise so you can write: * * ```ts * export const service = AsyncCall(typeof window === 'object' ? {} : import('./backend/service.js'), {}) * ``` * * @param thisSideImplementation - The implementation when this AsyncCall acts as a JSON RPC server. Can be a Promise. * @param options - {@link AsyncCallOptions} * @typeParam OtherSideImplementedFunctions - The type of the API that server expose. For any function on this interface, it will be converted to the async version. * @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. * @public */ export declare function AsyncCall(thisSideImplementation: null | undefined | object | Promise, options: AsyncCallOptions): AsyncVersionOf; /** * Log options * @remarks * This option controls how AsyncCall log requests to the console. * @public * @privateRemarks * TODO: rename to AsyncCallLogOptions * TODO: split to server log and client log */ export declare interface AsyncCallLogLevel { /** * Log all incoming requests * @defaultValue true * @privateRemarks * TODO: rename to called */ beCalled?: boolean; /** * Log all errors when responding requests * @defaultValue true */ localError?: boolean; /** * Log errors from the remote * @defaultValue true */ remoteError?: boolean; /** * Send the stack to the remote when making requests * @defaultValue false * @privateRemarks * TODO: rename this field to sendRequestStack and move it to AsyncCallOptions. */ sendLocalStack?: boolean; /** * Style of the log * @remarks * If this option is set to "pretty", it will log with some CSS to make the log easier to read in the browser devtools. * Check out this article to know more about it: {@link https://dev.to/annlin/consolelog-with-css-style-1mmp | Console.log with CSS Style} * @defaultValue 'pretty' */ type?: 'basic' | 'pretty'; /** * If log a function that can replay the request * @remarks * Do not use this options in the production environment because it will log a closure that captures all arguments of requests. This may cause memory leak. * @defaultValue false */ requestReplay?: boolean; } /** * Options for {@link AsyncCall} * @public */ export declare interface AsyncCallOptions { /** * Name used when pretty log is enabled. * @defaultValue `rpc` * @deprecated Renamed to "name". */ key?: string; /** * Name used when pretty log is enabled. * @defaultValue `rpc` */ name?: string; /** * Serializer of the requests and responses. * @deprecated Use "encoding" option instead. This option will be removed in the next major version. * @see {@link Serialization}. */ serializer?: Serialization; /** * Encoder of requests and responses. * @see {@link IsomorphicEncoder} or {@link IsomorphicEncoderFull}. * @remarks * There are some built-in encoders: * * - JSONEncoder: is using JSON.parser and JSON.stringify under the hood. * @defaultValue undefined */ encoder?: IsomorphicEncoder | IsomorphicEncoderFull; /** * Provide the logger * @see {@link ConsoleInterface} * @defaultValue globalThis.console * @privateRemarks * TODO: allow post-create tweak? */ logger?: ConsoleInterface; /** * The message channel to exchange messages between server and client * @example * {@link https://github.com/Jack-Works/async-call-rpc/blob/main/utils-src/web/websocket.client.ts | Example for CallbackBasedChannel} or {@link https://github.com/Jack-Works/async-call-rpc/blob/main/utils-src/node/websocket.server.ts | Example for EventBasedChannel} * @privateRemarks * TODO: split to ClientChannel (onResponse, send) and IsomorphicChannel */ channel: CallbackBasedChannel | EventBasedChannel | Promise | EventBasedChannel>; /** * Choose log level. * @remarks * - `true` is a reasonable default value, which means all options are the default in the {@link AsyncCallLogLevel} * * - `false` is disable all logs * * - `"all"` is enable all logs (stronger than `true`). * @defaultValue true * @privateRemarks * TODO: allow post-create tweak? */ log?: AsyncCallLogLevel | boolean | 'all'; /** * Control the behavior that different from the JSON-RPC spec * @see {@link AsyncCallStrictJSONRPC} * @remarks * - `true` is to enable all strict options * - `false` is to disable all strict options * @defaultValue true */ strict?: AsyncCallStrictJSONRPC | boolean; /** * Choose flavor of parameter structures defined in the spec * @see {@link https://www.jsonrpc.org/specification#parameter_structures} * @remarks * When using `by-name`, only first parameter is sent to the remote and it must be an object. * * @deprecated renamed to "parameterStructure" * @defaultValue "by-position" */ parameterStructures?: 'by-position' | 'by-name'; /** * Choose flavor of parameter structures defined in the spec * @see {@link https://www.jsonrpc.org/specification#parameter_structures} * @remarks * When using `by-name`, only first parameter is sent to the remote and it must be an object. * * @privateRemarks * TODO: review the edge cases when using "by-name". * TODO: throw an error/print a warning when using "by-name" and the first parameter is not an object/more than 1 parameter is given. * @defaultValue "by-position" */ parameterStructure?: 'by-position' | 'by-name'; /** * Prefer local implementation than remote. * @remarks * 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. * @defaultValue false */ preferLocalImplementation?: boolean; /** * The ID generator of each JSON RPC request * @defaultValue () =\> Math.random().toString(36).slice(2) */ idGenerator?(): string | number; /** * Change the {@link ErrorResponseDetail}. * @privateRemarks * TODO: provide a JSONRPCError class to allow customizing ErrorResponseDetail without mapError. */ mapError?: ErrorMapFunction; /** * If the instance is "thenable". * @defaultValue undefined * @remarks * If this options is set to `true`, it will return a `then` method normally (forwards the request to the remote). * * If this options is set to `false`, it will return `undefined`, which means a method named "then" on the remote is not reachable. * * 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. * * This option is used to resolve the problem caused by Promise auto-unwrapping. * * Consider this code: * * ```ts * async function getRPC() { * return AsyncCall(...) * } * ``` * * According to the JS semantics, it will invoke the "then" method immediately on the returning instance which is unwanted in most scenarios. * * To avoid this problem, methods called "then" are omitted from the type signatures. Strongly suggest to not use "then" as your RPC method name. */ thenable?: boolean; /** * AbortSignal to stop the instance. * @see {@link https://mdn.io/AbortSignal} * @remarks * `signal` is used to stop the instance. If the `signal` is aborted, then all new requests will be rejected, except for the pending ones. */ signal?: AbortSignalLike; /** * AbortSignal to force stop the instance. * @see {@link https://mdn.io/AbortSignal} * @remarks * `signal` is used to stop the instance. If the `signal` is aborted, then all new requests will be rejected, and the pending requests will be forcibly rejected and pending results will be ignored. */ forceSignal?: AbortSignalLike; } /** * Strict options * @remarks * Control the behavior that different from the JSON-RPC specification. * @public * @deprecated renamed to {@link AsyncCallStrictOptions} */ export declare interface AsyncCallStrictJSONRPC extends AsyncCallStrictOptions { } /** * Strict options * @remarks * Control the behavior that different from the JSON-RPC specification. * @public */ export declare interface AsyncCallStrictOptions { /** * Controls if AsyncCall send an ErrorResponse when the requested method is not defined. * @remarks * If this option is set to false, AsyncCall will ignore the request and print a log if the method is not defined. * @defaultValue true */ methodNotFound?: boolean; /** * Controls if AsyncCall send an ErrorResponse when the message is not valid. * @remarks * If this option is set 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. * @defaultValue true */ unknownMessage?: boolean; } /** * The async generator version of the AsyncCall * @param thisSideImplementation - The implementation when this AsyncCall acts as a JSON RPC server. * @param options - {@link AsyncCallOptions} * @typeParam OtherSideImplementedFunctions - The type of the API that server expose. For any function on this interface, AsyncCall will convert it to the Promised type. * @remarks * Warning: Due to technical limitation, AsyncGeneratorCall will leak memory. Use it at your own risk. * * To use AsyncGeneratorCall, the server and the client MUST support the following JSON RPC internal methods which is pre ECMAScript async generator semantics: * * - `rpc.async-iterator.start` * * - `rpc.async-iterator.next` * * - `rpc.async-iterator.return` * * - `rpc.async-iterator.throw` * * @example * ```ts * const server = { * async *generator() { * let last = 0 * while (true) yield last++ * }, * } * type Server = typeof server * const serverRPC = AsyncGeneratorCall({}, { channel }) * async function main() { * for await (const x of serverRPC.generator()) { * console.log('Server yielded number', x) * } * } * ``` * @public */ export declare function AsyncGeneratorCall(thisSideImplementation: null | undefined | object | Promise, options: AsyncCallOptions): AsyncGeneratorVersionOf; /** * Make all generator in the type T becomes AsyncGenerator * * @remarks * Only generics signatures on function that returning an AsyncGenerator will be preserved due to the limitation of TypeScript. * * Method called `then` are intentionally removed because it is very likely to be a foot gun in promise auto-unwrap. * @public */ export declare type AsyncGeneratorVersionOf = T extends Record ? 'then' extends keyof T ? Omit, 'then'> : T : _AsyncGeneratorVersionOf; /** @internal */ export declare type _AsyncGeneratorVersionOf = { [key in keyof T as key extends 'then' ? never : T[key] extends _IteratorOrIterableFunction ? key : never]: T[key] extends _IteratorOrIterableFunction ? _IteratorLikeToAsyncGenerator : never; }; /** * Make all functions in T becomes an async function and filter non-Functions out. * * @remarks * Only generics signatures on function that returning an Promise will be preserved due to the limitation of TypeScript. * * Method called `then` are intentionally removed because it is very likely to be a foot gun in promise auto-unwrap. * @public */ export declare type AsyncVersionOf = T extends Record PromiseLike> ? 'then' extends keyof T ? Omit, 'then'> : T : _AsyncVersionOf; /** @internal */ export declare type _AsyncVersionOf = { readonly [key in keyof T as key extends 'then' ? never : T[key] extends Function ? key : never]: T[key] extends (...args: any) => Promise ? T[key] : T[key] extends (...args: infer Args) => infer Return ? (...args: Args) => Promise> : never; }; /** * Wrap the AsyncCall instance to use batch call. * @param asyncCallInstance - The AsyncCall instance * @example * const [batched, send, drop] = batch(AsyncCall(...)) * batched.call1() // pending * batched.call2() // pending * send() // send all pending requests * drop() // drop all pending requests * @returns It will return a tuple. * * The first item is the batched version of AsyncCall instance passed in. * * The second item is a function to send all pending requests. * * The third item is a function to drop and reject all pending requests. * @public */ export declare function batch(asyncCallInstance: T): [T, () => void, (error?: unknown) => void]; /** * This interface represents a "callback" model. * @remarks * Usually used for there are many remotes (act like a server). * Example: {@link https://github.com/Jack-Works/async-call-rpc/blob/main/utils-src/web/websocket.client.ts | Example for CallbackBasedChannel} * @public */ export declare interface CallbackBasedChannel extends Partial> { /** * Setup the CallbackBasedChannel. * @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. * @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. * @returns a function that unregister the setup. */ setup(jsonRPCHandlerCallback: (jsonRPCPayload: unknown, hint?: undefined | 'request' | 'response') => Promise, isValidJSONRPCPayload: (data: unknown, hint?: undefined | 'request' | 'response') => boolean | Promise): (() => void) | void; } /** * Encoder of the client. * @public */ export declare interface ClientEncoding { /** * Encode the request object. * @param data - The request object */ encodeRequest(data: Requests): EncodedRequest | PromiseLike; /** * Decode the response object. * @param encoded - The encoded response object */ decodeResponse(encoded: EncodedResponse): Responses | PromiseLike; } /** * The minimal Console interface that AsyncCall needs. * @public * @remarks * The method not provided will use "log" as it's fallback. */ declare interface ConsoleInterface { warn?(...args: unknown[]): void; debug?(...args: unknown[]): void; log(...args: unknown[]): void; groupCollapsed?(...args: unknown[]): void; groupEnd?(...args: unknown[]): void; error?(...args: unknown[]): void; } export { ConsoleInterface as Console } export { ConsoleInterface } /** * @public * @param error - The exception * @param request - The request object * @privateRemarks * TODO: remove T generic parameter */ export declare type ErrorMapFunction = (error: unknown, request: Readonly) => { code: number; message: string; data?: T; }; /** * JSON-RPC ErrorResponse object. * @public * @see https://www.jsonrpc.org/specification#error_object */ export declare interface ErrorResponse { readonly jsonrpc: '2.0'; readonly id?: ID; readonly error: ErrorResponseDetail; } /** * The "error" record on the JSON-RPC ErrorResponse object. * @public * @see https://www.jsonrpc.org/specification#error_object */ export declare interface ErrorResponseDetail { readonly code: number; readonly message: string; readonly data?: Error; } /** * This interface represents a "on message" - "send response" model. * @remarks * Usually used for there is only 1 remote (act like a client). * Example: {@link https://github.com/Jack-Works/async-call-rpc/blob/main/utils-src/node/websocket.server.ts | Example for EventBasedChannel} * @public */ export declare interface EventBasedChannel { /** * Register the message listener. * @param listener - The message listener. * @returns a function that unregister the listener. */ on(listener: (data: Data, hint?: 'request' | 'response' | undefined) => void): void | (() => void); /** * Send the data to the remote side. * @param data - The data should send to the remote side. */ send(data: Data): void | Promise; } /** * ID of a JSON-RPC request/response. * @public */ export declare type ID = string | number | null | undefined; /** * Make the returning type to `Promise` * @internal * @remarks * Due to the limitation of TypeScript, generic signatures cannot be preserved * if the function is the top level parameter of this utility type, * or the function is not returning `Promise`. */ export declare type _IgnoreResponse = T extends (...args: infer Args) => unknown ? (...args: Args) => Promise : { [key in keyof T as T[key] extends Function ? key : never]: T[key] extends (...args: infer Args) => infer Return ? Return extends Promise ? T[key] : (...args: Args) => Promise : never; }; /** * Encoder that work for both server and client. * @public */ export declare interface IsomorphicEncoder { /** * Encode the request or response object. * @param data - The request or response object */ encode(data: Requests | Responses): EncodedRequest | EncodedResponse | PromiseLike; /** * Decode the request or response object. * @param encoded - The encoded request or response object */ decode(encoded: EncodedRequest | EncodedResponse): Requests | Responses | PromiseLike; } /** * Encoder that work for both server and client. * @public */ export declare interface IsomorphicEncoderFull extends ClientEncoding, ServerEncoding, Partial> { } /** @internal */ export declare type _IteratorLikeToAsyncGenerator = T extends (...args: any) => AsyncGenerator ? T : T extends (...args: infer Args) => Iterator | Iterable | AsyncIterator | AsyncIterable ? (...args: Args) => AsyncGenerator : never; /** @internal */ export declare type _IteratorOrIterableFunction = (...args: any) => Iterator | Iterable | AsyncIterator | AsyncIterable; /** * Create a encoder by JSON.parse/stringify * * @public * @param options - Options for this encoder. * @remarks {@link IsomorphicEncoder} */ export declare function JSONEncoder({ keepUndefined, replacer, reviver, space, }?: JSONEncoderOptions): IsomorphicEncoder; /** @public */ export declare namespace JSONEncoder { const Default: IsomorphicEncoder; } /** * @public * Options of {@link (JSONEncoder:function)} */ export declare interface JSONEncoderOptions { /** Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. */ space?: string | number | undefined; /** * How to handle `"undefined"` in the result of {@link SuccessResponse}. * * @remarks * If you need a full support of encoding `undefined`, for example, when the result is `{ field: undefined }` and you want to keep it, * you need to find another library to do this. * * If this is not handled properly, `JSON.stringify` will emit an invalid JSON-RPC object (fields with `undefined` value will be omitted). * * Options: * - `"null"`(**default**): convert it to `null`. * - `false`: do not do anything, let it break. */ keepUndefined?: false | 'null' | undefined; /** A function that transforms the results. */ replacer?: ((this: any, key: string, value: any) => any) | undefined; /** A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is. */ reviver?: ((this: any, key: string, value: any) => any) | undefined; } /** * Create a serialization by JSON.parse/stringify * * @param replacerAndReceiver - Replacer and receiver of JSON.parse/stringify * @param space - Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. * @param undefinedKeepingBehavior - How to keep "undefined" in result of SuccessResponse? * * If it is not handled properly, JSON.stringify will emit an invalid JSON RPC object. * * Options: * - `"null"`(**default**): convert it to null. * - `"keep"`: try to keep it by additional property "undef". * - `false`: Don't keep it, let it break. * @remarks {@link Serialization} * @public */ export declare const JSONSerialization: (replacerAndReceiver?: [(((key: string, value: any) => any) | undefined)?, (((key: string, value: any) => any) | undefined)?], space?: string | number | undefined, undefinedKeepingBehavior?: 'keep' | 'null' | false) => Serialization; /** * Serialization implementation that do nothing * @remarks {@link Serialization} * @public * @deprecated Will be removed in next major version */ export declare const NoSerialization: Serialization; /** * Wrap the AsyncCall instance to send notification. * @param instanceOrFnOnInstance - The AsyncCall instance or function on the AsyncCall instance * @example * const notifyOnly = notify(AsyncCall(...)) * @public */ export declare function notify(instanceOrFnOnInstance: T): _IgnoreResponse; /** * JSON-RPC Request object. * @public * @see https://www.jsonrpc.org/specification#request_object */ export declare interface Request { readonly jsonrpc: '2.0'; readonly id?: ID; readonly method: string; readonly params: readonly unknown[] | object; /** * Non-standard field. It records the caller's stack of this Request. */ readonly remoteStack?: string | undefined; } /** * A request object or an array of request objects. * @public */ export declare type Requests = Request | readonly Request[]; /** * A JSON-RPC response object * @public * @see https://www.jsonrpc.org/specification#response_object */ export declare type Response = SuccessResponse | ErrorResponse; /** * A response object or an array of response objects. * @public */ export declare type Responses = Response | readonly Response[]; /** * Serialize and deserialize of the JSON-RPC payload * @public * @deprecated Use {@link IsomorphicEncoder} instead. */ export declare interface Serialization { /** * Serialize data * @param from - original data */ serialization(from: any): unknown | PromiseLike; /** * Deserialize data * @param serialized - Serialized data */ deserialization(serialized: unknown): unknown | PromiseLike; } /** * Encoder of the server. * @public */ export declare interface ServerEncoding { /** * Encode the response object. * @param data - The response object */ decodeRequest(encoded: EncodedRequest): Requests | PromiseLike; /** * Decode the request object. * @param encoded - The encoded request object */ encodeResponse(data: Responses): EncodedResponse | PromiseLike; } /** * JSON-RPC SuccessResponse object. * @public * @see https://www.jsonrpc.org/specification#response_object */ export declare interface SuccessResponse { readonly jsonrpc: '2.0'; readonly id?: ID; result: unknown; /** * Non-standard property * @remarks * This is a non-standard field that used to represent the result field should be `undefined` instead of `null`. * * A field with value `undefined` will be omitted in `JSON.stringify`, * and if the `"result"` field is omitted, this is no longer a valid JSON-RPC response object. * * By default, AsyncCall will convert `undefined` to `null` to keep the response valid, but it _won't_ add this field. * * Set `keepUndefined` in JSONEncoderOptions to `"keep"` will add this field. * * This field starts with a space, so TypeScript will hide it when providing completion. */ undef?: unknown; } export { }