256 lines
5.9 KiB
TypeScript
256 lines
5.9 KiB
TypeScript
import generateUniqueId from './generateUniqueId';
|
|
import { throttleWithTickEnd } from './schedulers';
|
|
|
|
export interface CancellableCallback {
|
|
(
|
|
...args: any[]
|
|
): void;
|
|
|
|
isCanceled?: boolean;
|
|
}
|
|
|
|
type InitPayload = {
|
|
type: 'init';
|
|
messageId?: string;
|
|
args: any;
|
|
};
|
|
|
|
type CallMethodPayload = {
|
|
type: 'callMethod';
|
|
messageId?: string;
|
|
name: string;
|
|
args: any;
|
|
withCallback?: boolean;
|
|
};
|
|
|
|
type CancelProgressPayload = {
|
|
type: 'cancelProgress';
|
|
messageId: string;
|
|
};
|
|
|
|
export type OriginPayload =
|
|
InitPayload
|
|
| CallMethodPayload
|
|
| CancelProgressPayload;
|
|
|
|
export type OriginMessageData = {
|
|
channel?: string;
|
|
payloads: OriginPayload[];
|
|
};
|
|
|
|
export interface OriginMessageEvent {
|
|
data: OriginMessageData;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
|
export type ApiUpdate = { type: string } & any;
|
|
|
|
export type WorkerPayload =
|
|
{
|
|
channel?: string;
|
|
type: 'update';
|
|
update: ApiUpdate;
|
|
}
|
|
|
|
|
{
|
|
channel?: string;
|
|
type: 'methodResponse';
|
|
messageId: string;
|
|
response?: any;
|
|
error?: { message: string };
|
|
}
|
|
|
|
|
{
|
|
channel?: string;
|
|
type: 'methodCallback';
|
|
messageId: string;
|
|
callbackArgs: any[];
|
|
}
|
|
|
|
|
{
|
|
channel?: string;
|
|
type: 'unhandledError';
|
|
error?: { message: string };
|
|
};
|
|
|
|
export type WorkerMessageData = {
|
|
channel?: string;
|
|
payloads: WorkerPayload[];
|
|
};
|
|
|
|
export interface WorkerMessageEvent {
|
|
data: WorkerMessageData;
|
|
}
|
|
|
|
interface RequestState {
|
|
messageId: string;
|
|
resolve: AnyToVoidFunction;
|
|
reject: AnyToVoidFunction;
|
|
callback: AnyToVoidFunction;
|
|
}
|
|
|
|
type InputRequestTypes = Record<string, AnyFunction>;
|
|
|
|
type Values<T> = T[keyof T];
|
|
export type RequestTypes<T extends InputRequestTypes> = Values<{
|
|
[Name in keyof (T)]: {
|
|
name: Name & string;
|
|
args: Parameters<T[Name]>;
|
|
transferables?: Transferable[];
|
|
}
|
|
}>;
|
|
|
|
class ConnectorClass<T extends InputRequestTypes> {
|
|
private requestStates = new Map<string, RequestState>();
|
|
|
|
private requestStatesByCallback = new Map<AnyToVoidFunction, RequestState>();
|
|
|
|
private pendingPayloads: OriginPayload[] = [];
|
|
|
|
private pendingTransferables: Transferable[] = [];
|
|
|
|
constructor(
|
|
public target: Worker,
|
|
private onUpdate?: (update: ApiUpdate) => void,
|
|
private channel?: string,
|
|
) {
|
|
}
|
|
|
|
public destroy() {
|
|
}
|
|
|
|
init(...args: any[]) {
|
|
this.postMessageOnTickEnd({
|
|
type: 'init',
|
|
args,
|
|
});
|
|
}
|
|
|
|
request(messageData: RequestTypes<T>) {
|
|
const { requestStates, requestStatesByCallback } = this;
|
|
const { transferables, ...restMessageData } = messageData;
|
|
|
|
const messageId = generateUniqueId();
|
|
const payload: CallMethodPayload = {
|
|
type: 'callMethod',
|
|
messageId,
|
|
...restMessageData,
|
|
};
|
|
|
|
const requestState = { messageId } as RequestState;
|
|
|
|
// Re-wrap type because of `postMessage`
|
|
const promise = new Promise<any>((resolve, reject) => {
|
|
Object.assign(requestState, { resolve, reject });
|
|
});
|
|
|
|
if (typeof payload.args[payload.args.length - 1] === 'function') {
|
|
payload.withCallback = true;
|
|
|
|
const callback = payload.args.pop() as AnyToVoidFunction;
|
|
requestState.callback = callback;
|
|
requestStatesByCallback.set(callback, requestState);
|
|
}
|
|
|
|
requestStates.set(messageId, requestState);
|
|
promise
|
|
.catch(() => undefined)
|
|
.finally(() => {
|
|
requestStates.delete(messageId);
|
|
|
|
if (requestState.callback) {
|
|
requestStatesByCallback.delete(requestState.callback);
|
|
}
|
|
});
|
|
|
|
this.postMessageOnTickEnd(payload, transferables);
|
|
|
|
return promise;
|
|
}
|
|
|
|
cancelCallback(progressCallback: CancellableCallback) {
|
|
progressCallback.isCanceled = true;
|
|
|
|
const { messageId } = this.requestStatesByCallback.get(progressCallback) || {};
|
|
if (!messageId) {
|
|
return;
|
|
}
|
|
|
|
this.postMessageOnTickEnd({
|
|
type: 'cancelProgress',
|
|
messageId,
|
|
});
|
|
}
|
|
|
|
onMessage(data: WorkerMessageData) {
|
|
const { requestStates, channel } = this;
|
|
if (data.channel !== channel) {
|
|
return;
|
|
}
|
|
|
|
data.payloads.forEach((payload) => {
|
|
if (payload.type === 'update' && this.onUpdate) {
|
|
this.onUpdate(payload.update);
|
|
}
|
|
if (payload.type === 'methodResponse') {
|
|
const requestState = requestStates.get(payload.messageId);
|
|
if (requestState) {
|
|
if (payload.error) {
|
|
requestState.reject(payload.error);
|
|
} else {
|
|
requestState.resolve(payload.response);
|
|
}
|
|
}
|
|
} else if (payload.type === 'methodCallback') {
|
|
const requestState = requestStates.get(payload.messageId);
|
|
requestState?.callback?.(...payload.callbackArgs);
|
|
} else if (payload.type === 'unhandledError') {
|
|
throw new Error(payload.error?.message);
|
|
}
|
|
});
|
|
}
|
|
|
|
private postMessageOnTickEnd(payload: OriginPayload, transferables?: Transferable[]) {
|
|
this.pendingPayloads.push(payload);
|
|
|
|
if (transferables) {
|
|
this.pendingTransferables.push(...transferables);
|
|
}
|
|
|
|
this.postMessagesOnTickEnd();
|
|
}
|
|
|
|
private postMessagesOnTickEnd = throttleWithTickEnd(() => {
|
|
const { channel } = this;
|
|
const payloads = this.pendingPayloads;
|
|
const transferables = this.pendingTransferables;
|
|
|
|
this.pendingPayloads = [];
|
|
this.pendingTransferables = [];
|
|
|
|
this.target.postMessage({ channel, payloads }, transferables);
|
|
});
|
|
}
|
|
|
|
export function createConnector<T extends InputRequestTypes>(
|
|
worker: Worker,
|
|
onUpdate?: (update: ApiUpdate) => void,
|
|
channel?: string,
|
|
) {
|
|
const connector = new ConnectorClass<T>(worker, onUpdate, channel);
|
|
|
|
function handleMessage({ data }: WorkerMessageEvent) {
|
|
connector.onMessage(data);
|
|
}
|
|
|
|
worker.addEventListener('message', handleMessage);
|
|
|
|
connector.destroy = () => {
|
|
worker.removeEventListener('message', handleMessage);
|
|
};
|
|
|
|
return connector;
|
|
}
|
|
|
|
export type Connector<T extends InputRequestTypes = InputRequestTypes> = ReturnType<typeof createConnector<T>>;
|