首次提交

This commit is contained in:
2026-07-13 12:06:16 +08:00
commit e3c810b1a6
1690 changed files with 228324 additions and 0 deletions
@@ -0,0 +1,52 @@
import { IBrokerEvent, IErrorNotification, IErrorResponse, IReceiver, IRequest, IWorkerDefinition } from '../interfaces';
import { TMessageReceiverWithParams, TMessageReceiverWithoutParams, TWorkerImplementation } from '../types';
import { renderMethodNotFoundError, renderMissingResponseError, renderUnexpectedResultError } from './error-renderers';
export const createMessageHandler = <WorkerDefinition extends IWorkerDefinition>(
receiver: IReceiver,
workerImplementation: TWorkerImplementation<WorkerDefinition>
) => {
return async ({ data: { id, method, params } }: IBrokerEvent<WorkerDefinition>) => {
const messageHandler = workerImplementation[method];
try {
if (messageHandler === undefined) {
throw renderMethodNotFoundError(method);
}
const response =
params === undefined
? (messageHandler as TMessageReceiverWithoutParams<WorkerDefinition[typeof method]['response']>)()
: (
messageHandler as TMessageReceiverWithParams<
WorkerDefinition[typeof method]['params'],
WorkerDefinition[typeof method]['response']
>
)(params);
if (response === undefined) {
throw renderMissingResponseError(method);
}
const synchronousResponse = response instanceof Promise ? await response : response;
if (id === null) {
if (synchronousResponse.result !== undefined) {
throw renderUnexpectedResultError(method);
}
} else {
if (synchronousResponse.result === undefined) {
throw renderUnexpectedResultError(method);
}
const { result, transferables = [] } = <IRequest['response']>synchronousResponse;
receiver.postMessage({ id, result }, transferables);
}
} catch (err) {
const { message, status = -32603 } = err;
receiver.postMessage(<IErrorNotification | IErrorResponse>{ error: { code: status, message }, id });
}
};
};
@@ -0,0 +1,24 @@
const JSON_RPC_ERROR_CODES = { INTERNAL_ERROR: -32603, INVALID_PARAMS: -32602, METHOD_NOT_FOUND: -32601 };
const createErrorWithMessageAndStatus = (message: string, status: number) => Object.assign(new Error(message), { status });
export const renderMethodNotFoundError = (method: string) =>
createErrorWithMessageAndStatus(`The requested method called "${method}" is not supported.`, JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND);
export const renderMissingResponseError = (method: string) =>
createErrorWithMessageAndStatus(
`The handler of the method called "${method}" returned no required result.`,
JSON_RPC_ERROR_CODES.INTERNAL_ERROR
);
export const renderUnexpectedResultError = (method: string) =>
createErrorWithMessageAndStatus(
`The handler of the method called "${method}" returned an unexpected result.`,
JSON_RPC_ERROR_CODES.INTERNAL_ERROR
);
export const renderUnknownPortIdError = (portId: number) =>
createErrorWithMessageAndStatus(
`The specified parameter called "portId" with the given value "${portId}" does not identify a port connected to this worker.`,
JSON_RPC_ERROR_CODES.INVALID_PARAMS
);
@@ -0,0 +1,52 @@
import { generateUniqueNumber } from 'fast-unique-numbers';
import { IDefaultWorkerDefinition, IReceiver, IWorkerDefinition } from '../interfaces';
import { TDestroyWorkerFunction, TWorkerImplementation } from '../types';
import { renderUnknownPortIdError } from './error-renderers';
import { isSupportingTransferables } from './is-supporting-transferables';
const DESTROY_WORKER_FUNCTIONS: Map<number, TDestroyWorkerFunction> = new Map();
export const extendWorkerImplementation = <WorkerDefinition extends IWorkerDefinition>(
createWorker: (receiver: IReceiver, workerImplementation: TWorkerImplementation<WorkerDefinition>) => TDestroyWorkerFunction,
partialWorkerImplementation: TWorkerImplementation<WorkerDefinition>,
isSupportedFunction: () => boolean | Promise<boolean>
): TWorkerImplementation<WorkerDefinition> & TWorkerImplementation<IDefaultWorkerDefinition> => ({
...partialWorkerImplementation,
connect: ({ port }: { port: MessagePort }) => {
port.start();
const destroyWorker = createWorker(port, partialWorkerImplementation);
const portId = generateUniqueNumber(DESTROY_WORKER_FUNCTIONS);
DESTROY_WORKER_FUNCTIONS.set(portId, () => {
destroyWorker();
port.close();
DESTROY_WORKER_FUNCTIONS.delete(portId);
});
return { result: portId };
},
disconnect: ({ portId }: { portId: number }) => {
const destroyWorker = DESTROY_WORKER_FUNCTIONS.get(portId);
if (destroyWorker === undefined) {
throw renderUnknownPortIdError(portId);
}
destroyWorker();
return { result: null };
},
isSupported: async () => {
const isSelfSupported = await isSupportingTransferables();
if (isSelfSupported) {
const result = isSupportedFunction();
const synchronousResult = result instanceof Promise ? await result : result;
return { result: synchronousResult };
}
return { result: false };
}
});
@@ -0,0 +1,10 @@
// Bug #1: Safari does currently not support to use transferables.
export const isSupportingTransferables = () =>
new Promise<boolean>((resolve) => {
const arrayBuffer = new ArrayBuffer(0);
const { port1, port2 } = new MessageChannel();
port1.onmessage = ({ data }) => resolve(data !== null);
port2.postMessage(arrayBuffer, [arrayBuffer]);
});
@@ -0,0 +1,6 @@
import { IBrokerMessage } from './broker-message';
import { IWorkerDefinition } from './worker-definition';
export interface IBrokerEvent<WorkerDefinition extends IWorkerDefinition> extends Event {
data: IBrokerMessage<WorkerDefinition>;
}
@@ -0,0 +1,9 @@
import { IWorkerDefinition } from './worker-definition';
export interface IBrokerMessage<WorkerDefinition extends IWorkerDefinition> {
id: null | number;
method: Extract<keyof WorkerDefinition, string>;
params: WorkerDefinition[Extract<keyof WorkerDefinition, string>]['params'];
}
@@ -0,0 +1,29 @@
import { IWorkerDefinition } from './worker-definition';
export interface IDefaultWorkerDefinition extends IWorkerDefinition {
connect: {
params: {
port: MessagePort;
};
response: {
result: number;
};
};
disconnect: {
params: {
portId: number;
};
response: {
result: null;
};
};
isSupported: {
response: {
result: boolean;
};
};
}
@@ -0,0 +1,7 @@
import { IError } from './error';
export interface IErrorNotification {
error: IError;
id: null;
}
@@ -0,0 +1,7 @@
import { IError } from './error';
export interface IErrorResponse {
error: IError;
id: null;
}
+5
View File
@@ -0,0 +1,5 @@
export interface IError {
code: number;
message: string;
}
+14
View File
@@ -0,0 +1,14 @@
export * from './broker-event';
export * from './broker-message';
export * from './default-worker-definition';
export * from './error';
export * from './error-notification';
export * from './error-response';
export * from './notification';
export * from './receiver';
export * from './request';
export * from './value-array';
export * from './value-map';
export * from './worker-definition';
export * from './worker-error-message';
export * from './worker-result-message';
@@ -0,0 +1,11 @@
import { TValue } from '../types';
export interface INotification {
params: TValue;
response: {
result: undefined;
};
transferables?: Transferable[];
}
+7
View File
@@ -0,0 +1,7 @@
export interface IReceiver {
addEventListener: typeof addEventListener;
postMessage: typeof postMessage;
removeEventListener: typeof removeEventListener;
}
+13
View File
@@ -0,0 +1,13 @@
import { TValue } from '../types';
export interface IRequest {
params?: TValue[] | { [key: string]: TValue };
response: {
result: TValue;
transferables?: Transferable[];
};
transferables?: Transferable[];
}
@@ -0,0 +1,5 @@
import { TValue } from '../types';
export interface IValueArray extends Array<TValue> {
// tslint:disable-line:no-empty-interface
}
+5
View File
@@ -0,0 +1,5 @@
import { TValue } from '../types';
export interface IValueMap {
[key: string]: TValue;
}
@@ -0,0 +1,5 @@
import { TMessage } from '../types';
export interface IWorkerDefinition {
[method: string]: TMessage;
}
@@ -0,0 +1,7 @@
import { IError } from './error';
export interface IWorkerErrorMessage {
error: IError;
id: null | number;
}
@@ -0,0 +1,7 @@
import { TValue } from '../types';
export interface IWorkerResultMessage {
id: null | number;
result: TValue;
}
+27
View File
@@ -0,0 +1,27 @@
import { createMessageHandler } from './helpers/create-message-handler';
import { extendWorkerImplementation } from './helpers/extend-worker-implementation';
import { isSupportingTransferables } from './helpers/is-supporting-transferables';
import { IReceiver, IWorkerDefinition } from './interfaces';
import { TDestroyWorkerFunction, TWorkerImplementation } from './types';
/*
* @todo Explicitly referencing the barrel file seems to be necessary when enabling the
* isolatedModules compiler option.
*/
export * from './interfaces/index';
export * from './types/index';
export const createWorker = <WorkerDefinition extends IWorkerDefinition>(
receiver: IReceiver,
workerImplementation: TWorkerImplementation<WorkerDefinition>,
isSupportedFunction: () => boolean | Promise<boolean> = () => true
): TDestroyWorkerFunction => {
const fullWorkerImplementation = extendWorkerImplementation<WorkerDefinition>(createWorker, workerImplementation, isSupportedFunction);
const messageHandler = createMessageHandler(receiver, fullWorkerImplementation);
receiver.addEventListener('message', messageHandler);
return () => receiver.removeEventListener('message', messageHandler);
};
export { isSupportingTransferables as isSupported };
+6
View File
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"isolatedModules": true
},
"extends": "tsconfig-holy-grail/src/tsconfig-web-worker"
}
@@ -0,0 +1 @@
export type TDestroyWorkerFunction = () => void;
+11
View File
@@ -0,0 +1,11 @@
export * from './destroy-worker-function';
export * from './message';
export * from './message-receiver';
export * from './message-receiver-with-params';
export * from './message-receiver-without-params';
export * from './typed-array';
export * from './value';
export * from './value-map';
export * from './worker-definition';
export * from './worker-implementation';
export * from './worker-message';
@@ -0,0 +1,5 @@
import { TMessage } from './message';
export type TMessageReceiverWithParams<Params extends TMessage['params'], Response extends TMessage['response']> = (
params: Params
) => Response | Promise<Response>;
@@ -0,0 +1,3 @@
import { TMessage } from './message';
export type TMessageReceiverWithoutParams<Message extends TMessage['response']> = () => Message | Promise<Message>;
@@ -0,0 +1,7 @@
import { TMessage } from './message';
import { TMessageReceiverWithParams } from './message-receiver-with-params';
import { TMessageReceiverWithoutParams } from './message-receiver-without-params';
export type TMessageReceiver<Message extends TMessage> = Message['params'] extends undefined
? TMessageReceiverWithoutParams<Message['response']>
: TMessageReceiverWithParams<Message['params'], Message['response']>;
+3
View File
@@ -0,0 +1,3 @@
import { INotification, IRequest } from '../interfaces';
export type TMessage = INotification | IRequest;
+10
View File
@@ -0,0 +1,10 @@
export type TTypedArray =
| Float32Array
| Float64Array
| Int8Array
| Int16Array
| Int32Array
| Uint8Array
| Uint8ClampedArray
| Uint16Array
| Uint32Array;
+5
View File
@@ -0,0 +1,5 @@
import { IValueMap } from '../interfaces';
export type TValueMap<ValueMap extends IValueMap = IValueMap> = {
[P in keyof ValueMap]: ValueMap[P];
};
+5
View File
@@ -0,0 +1,5 @@
import { IValueArray } from '../interfaces';
import { TTypedArray } from './typed-array';
import { TValueMap } from './value-map';
export type TValue = boolean | null | number | string | IValueArray | RegExp | TTypedArray | TValueMap | Transferable;
@@ -0,0 +1,3 @@
import { IDefaultWorkerDefinition, IWorkerDefinition } from '../interfaces';
export type TWorkerDefinition<WorkerDefinition extends IWorkerDefinition> = WorkerDefinition & IDefaultWorkerDefinition;
@@ -0,0 +1,6 @@
import { IWorkerDefinition } from '../interfaces';
import { TMessageReceiver } from './message-receiver';
export type TWorkerImplementation<WorkerDefinition extends IWorkerDefinition> = {
[P in keyof WorkerDefinition]: TMessageReceiver<WorkerDefinition[P]>;
};
+3
View File
@@ -0,0 +1,3 @@
import { IWorkerErrorMessage, IWorkerResultMessage } from '../interfaces';
export type TWorkerMessage = IWorkerErrorMessage | IWorkerResultMessage;