swarpc
Version:
Full type-safe RPC library for service worker -- move things off of the UI thread with ease!
268 lines (248 loc) • 7.6 kB
text/typescript
/**
* @module
* @mergeModuleWith <project>
*/
import { type, type Type } from "arktype";
import { RequestBoundLogger } from "./log.js";
/**
* A procedure declaration
*/
export type Procedure<I extends Type, P extends Type, S extends Type> = {
/**
* ArkType type for the input (first argument) of the procedure, when calling it from the client.
*/
input: I;
/**
* ArkType type for the data as the first argument given to the `onProgress` callback
* when calling the procedure from the client.
*/
progress: P;
/**
* ArkType type for the output (return value) of the procedure, when calling it from the client.
*/
success: S;
/**
* When should the procedure automatically add ArrayBuffers and other transferable objects
* to the [transfer list](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/postMessage#transfer)
* when sending messages, both from the client to the server and vice versa.
*
* Transferring objects can improve performance by avoiding copies of large objects,
* but _moves_ them to the other context, meaning that they cannot be used in the original context after being sent.
*
* 'output-only' by default: only transferables sent from the server to the client will be transferred.
*/
autotransfer?: "always" | "never" | "output-only";
};
/**
* A promise that you can cancel by calling `.cancel(reason)` on it:
*
* ```js
* const { request, cancel } = client.runProcedure.cancelable(input, onProgress)
* setTimeout(() => cancel("Cancelled by user"), 1000)
* const result = await request
* ```
*/
export type CancelablePromise<T = unknown> = {
request: Promise<T>;
/**
* Abort the request.
* @param reason The reason for cancelling the request.
*/
cancel: (reason: string) => void;
};
/**
* An implementation of a procedure
*/
export type ProcedureImplementation<
I extends Type,
P extends Type,
S extends Type,
> = (
/**
* Input data for the procedure
*/
input: I["inferOut"],
/**
* Callback to call with progress updates.
*/
onProgress: (progress: P["inferIn"]) => void,
/**
* Additional tools useful when implementing the procedure.
*/
tools: {
/**
* AbortSignal that can be used to handle request cancellation -- see [Make cancellable requests](https://gwennlbh.github.io/swarpc/docs/#make-cancelable-requests)
*/
abortSignal?: AbortSignal;
/**
* Logger instance to use for logging messages related to this procedure call, using the same format as SWARPC's built-in logging.
*/
logger: RequestBoundLogger;
/**
* ID of the Node the request is being processed on.
*/
nodeId: string;
},
) => Promise<S["inferIn"]>;
/**
* Declarations of procedures by name.
*
* An example of declaring procedures:
* {@includeCode ../example/src/lib/procedures.ts}
*/
export type ProceduresMap = Record<string, Procedure<Type, Type, Type>>;
/**
* Implementations of procedures by name
*/
export type ImplementationsMap<Procedures extends ProceduresMap> = {
[F in keyof Procedures]: ProcedureImplementation<
Procedures[F]["input"],
Procedures[F]["progress"],
Procedures[F]["success"]
>;
};
/**
* Declaration of hooks to run on messages received from the server
*/
export type Hooks<Procedures extends ProceduresMap> = {
/**
* Called when a procedure call has been successful.
*/
success?: <Procedure extends keyof ProceduresMap>(
procedure: Procedure,
data: Procedures[Procedure]["success"]["inferOut"],
) => void;
/**
* Called when a procedure call has failed.
*/
error?: <Procedure extends keyof ProceduresMap>(
procedure: Procedure,
error: Error,
) => void;
/**
* Called when a procedure call sends progress updates.
*/
progress?: <Procedure extends keyof ProceduresMap>(
procedure: Procedure,
data: Procedures[Procedure]["progress"]["inferOut"],
) => void;
};
export const PayloadInitializeSchema = type({
by: '"sw&rpc"',
functionName: '"#initialize"',
isInitializeRequest: "true",
localStorageData: "Record<string, unknown>",
nodeId: "string",
});
export type PayloadInitialize = typeof PayloadInitializeSchema.infer;
/**
* @source
*/
export const PayloadHeaderSchema = type("<Name extends string>", {
by: '"sw&rpc"',
functionName: "Name",
requestId: "string >= 1",
});
export type PayloadHeader<
PM extends ProceduresMap,
Name extends keyof PM = keyof PM,
> = {
by: "sw&rpc";
functionName: Name & string;
requestId: string;
};
/**
* @source
*/
export const PayloadCoreSchema = type("<I, P, S>", {
"input?": "I",
"progress?": "P",
"result?": "S",
"abort?": { reason: "string" },
"error?": { message: "string" },
});
export type PayloadCore<
PM extends ProceduresMap,
Name extends keyof PM = keyof PM,
> =
| {
input: PM[Name]["input"]["inferOut"];
}
| {
progress: PM[Name]["progress"]["inferOut"];
}
| {
result: PM[Name]["success"]["inferOut"];
}
| {
abort: { reason: string };
}
| {
error: { message: string };
};
/**
* @source
*/
export const PayloadSchema = type
.scope({ PayloadCoreSchema, PayloadHeaderSchema, PayloadInitializeSchema })
.type("<Name extends string, I, P, S>", [
["PayloadHeaderSchema<Name>", "&", "PayloadCoreSchema<I, P, S>"],
"|",
"PayloadInitializeSchema",
]);
/**
* The effective payload as sent by the server to the client
*/
export type Payload<
PM extends ProceduresMap,
Name extends keyof PM = keyof PM,
> = (PayloadHeader<PM, Name> & PayloadCore<PM, Name>) | PayloadInitialize;
/**
* A procedure's corresponding method on the client instance -- used to call the procedure. If you want to be able to cancel the request, you can use the `cancelable` method instead of running the procedure directly.
*/
export type ClientMethod<P extends Procedure<Type, Type, Type>> = ((
input: P["input"]["inferIn"],
onProgress?: (progress: P["progress"]["inferOut"]) => void,
) => Promise<P["success"]["inferOut"]>) & {
/**
* A method that returns a `CancelablePromise`. Cancel it by calling `.cancel(reason)` on it, and wait for the request to resolve by awaiting the `request` property on the returned object.
*/
cancelable: (
input: P["input"]["inferIn"],
onProgress?: (progress: P["progress"]["inferOut"]) => void,
requestId?: string,
) => CancelablePromise<P["success"]["inferOut"]>;
/**
* Send the request to specific nodes, or all nodes.
* Returns an array of results, one for each node the request was sent to.
* Each result is a {@link PromiseSettledResult}, with also an additional property, the node ID of the request
*/
broadcast: (
input: P["input"]["inferIn"],
onProgress?: (
/** Map of node IDs to their progress updates */
progresses: Map<string, P["progress"]["inferOut"]>,
) => void,
/** Number of nodes to send the request to. Leave undefined to send to all nodes */
nodes?: number,
) => Promise<
Array<PromiseSettledResult<P["success"]["inferOut"]> & { node: string }>
>;
};
/**
* Symbol used as the key for the procedures map on the server instance
* @internal
* @source
*/
export const zImplementations = Symbol("SWARPC implementations");
/**
* Symbol used as the key for the procedures map on instances
* @internal
* @source
*/
export const zProcedures = Symbol("SWARPC procedures");
export type WorkerConstructor<
T extends Worker | SharedWorker = Worker | SharedWorker,
> = {
new (opts?: { name?: string }): T;
};