首次提交

This commit is contained in:
2026-07-13 12:06:16 +08:00
commit e3c810b1a6
1690 changed files with 228324 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Christoph Guttandin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
# worker-factory
**A little factory function to create a JSON-RPC based Web Worker implementation.**
[![version](https://img.shields.io/npm/v/worker-factory.svg?style=flat-square)](https://www.npmjs.com/package/worker-factory)
+70
View File
@@ -0,0 +1,70 @@
{
"author": "Christoph Guttandin",
"bugs": {
"url": "https://github.com/chrisguttandin/worker-factory/issues"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"dependencies": {
"@babel/runtime": "^7.29.2",
"fast-unique-numbers": "^9.0.27",
"tslib": "^2.8.1"
},
"description": "A little factory function to create a JSON-RPC based Web Worker implementation.",
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/plugin-external-helpers": "^7.27.1",
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@commitlint/cli": "^20.5.0",
"@commitlint/config-angular": "^20.5.0",
"@rollup/plugin-babel": "^7.0.0",
"@vitest/browser-webdriverio": "^4.1.0",
"commitizen": "^4.3.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.57.0",
"eslint-config-holy-grail": "^61.0.11",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"memory-fs": "^0.5.0",
"prettier": "^3.8.1",
"rimraf": "^6.1.3",
"rollup": "^4.59.0",
"tsconfig-holy-grail": "^15.0.3",
"tslint": "^6.1.3",
"tslint-config-holy-grail": "^56.0.7",
"typescript": "^5.9.3",
"vitest": "^4.1.0"
},
"files": [
"build/es2019/",
"build/es5/",
"src/"
],
"homepage": "https://github.com/chrisguttandin/worker-factory",
"license": "MIT",
"main": "build/es5/bundle.js",
"module": "build/es2019/module.js",
"name": "worker-factory",
"repository": {
"type": "git",
"url": "https://github.com/chrisguttandin/worker-factory.git"
},
"scripts": {
"build": "rimraf build/* && tsc --project src/tsconfig.json && rollup --config config/rollup/bundle.mjs",
"lint": "npm run lint:config && npm run lint:src && npm run lint:test",
"lint:config": "eslint --config config/eslint/config.json --ext .cjs --ext .js --ext .mjs --report-unused-disable-directives config/",
"lint:src": "tslint --config config/tslint/src.json --project src/tsconfig.json src/*.ts src/**/*.ts",
"lint:test": "eslint --config config/eslint/test.json --ext .js --report-unused-disable-directives test/",
"prepare": "husky",
"prepublishOnly": "npm run build",
"test": "npm run lint && npm run build && npm run test:integration && npm run test:unit",
"test:integration": "if [ \"$TYPE\" = \"\" -o \"$TYPE\" = \"integration\" ]; then npx vitest --config config/vitest/integration.ts; fi",
"test:unit": "if [ \"$TYPE\" = \"\" -o \"$TYPE\" = \"unit\" ]; then npx vitest --config config/vitest/unit.ts; fi"
},
"types": "build/es2019/module.d.ts",
"version": "7.0.49"
}
@@ -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;