worker-handler v0.2.12
worker-handler
Overview
Worker-handler provides a convenient capability for posting messages between the Main thread and the Worker thread when using Web Worker in javascript or typescript.
Through worker-handler, in Main, messages can be posted to and recieved from Worker just like network requests. Actions for handling these "requests" can be defined within Worker. There are two ways to obtain "responses": they can be acquired through Promise, which is similar to AJAX, or through EventTarget, which is similar to Server-sent events, and both ways of response can be used simultaneously in the same request.
Quick Start
Install
npm install worker-handlerBasic Usage
The following example demonstrates the most basic usage of worker-handler:
// demo.worker.js
import { createOnmessage } from "worker-handler/worker";
// Call `createOnmessage` with `Actions` to get the `onmessage` callback of worker.
onmessage = createOnmessage({
// Defining the `Action` with a async function is recommended if only responsing messages by `Promise`.
async someAction() {
// Any asynchronous process can be excuted in Actions.
......
// The value returned in the asynchronous `Action` will be posted to Main as the response message through `Promise`.
return "some messages";
}
});// demo.main.js
import { WorkerHandler } from "worker-handler"; // It can also be imported from "worker-handler/main".
// import workerUrl from "./demo.worker.js?worker&url"; // in vite
const demoWorker = new WorkerHandler(
// Pass an instance of Worker as the first argument.
new Worker(new URL("./demo.worker.js", import.meta.url)) // In webpack5, create an instance of Worker in this way.
// In Vite, you can pass the above `workerUrl`, WorkerHandler will convert it into an instance of Worker.
);
// Request `Worker` to execute someAction.
demoWorker.execute("someAction", []).promise.then((res) => {
// Receive the message responded through `Promise` from the `Action`.
console.log(res.data);
}).catch((err) => {
// Errors occurring in the `Action` will cause the `Promise` to be rejected.
console.log(err)
});Typescript
Worker-handler can be used with type supports in typescript. Once the type of Action is defined, it enables type detections and hints at both the posting and receiving ends when passing messages between Main and Worker.
The following is a simple example of using worker-handler in typescript:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler-test/worker";
/*
* Define the types for `Actions`, which will subsequently be passed as generic parameters in two places:
* - When using `createOnmessage()` in `Worker`.
* - When using `new WorkerHandler()` in `Main`.
*/
export type DemoActions = {
// Define an `Action` named `pingLater`, whose return type `ActionResult<string>` indicates that this `Action` can pass a message of string type to Main.
pingLater: (delay: number) => ActionResult<string>;
};
onmessage = createOnmessage<DemoActions>({
// After being called, `pingLater` will pass the message to Main after `delay` ms.
async pingLater(delay) {
await new Promise((resolve) => {
setTimeout(() => {
resolve(null);
}, delay);
});
return "Worker recieved a message from Main " + delay + "ms ago.";
}
});// demo.main.ts
import { WorkerHandler } from "worker-handler/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
demoWorker.execute("pingLater", null, 1000).promise.then((res) => {
console.log(res.data);
});Call Action
Calling excute() of a WorkerHandle instance in Main will create a connection with Worker and call an Action.
The parameters received by excute() from the third one onwards are all payloads, which will be passed to the target Action in Worker in order.
The second parameter is an object that specifies connection configuration options, which contains two properties named transfer and timeout:
The value of
transferis an array of transferable objects that will have their ownership transferred to theWorker, used to specify thetransferable objectsinpayloadsthat need to be transferred.If the value of
transferis"auto", then thetransferable objectsinpayloadswil be automatically identified.The value of
timeoutis the timeout duration for this connection.After the timeout, the connection will be closed, no further responses will be received, and the
Promisereturned byActionwill becomerejected.
The passing of the second parameter can also be simplified according to follow situations:
- If only
transferis needed, an array can be directly passed. - If only
timeoutis needed, a number can be directly passed. - If neither is needed, any of the following values can be passed:
null,undefined,[], any number less than or equal to0.
Responding Messages
Actions support responding with messages to Main through either Promise or EventTarget, and both ways can be used within the same Action.
Responding through Promise is suitable for situations where one request corresponds to a unique response, or that response will be the last response in the request.
Responding through EventTarget is suitable for situations where one request will recieve multiple responses.
Responding through Promise (terminating responses)
In Actions, you can respond to messages through Promise either by using return value of the Action or by calling this.$end().
Using return value of the Action
Return a Promise in an Action,as shown in the basic example.
It should be noted that this method of response cannot transfer transferable objects. Objects like OffscreenCanvas, which must be transferred to be used in different contexts, cannot be sent to the main thread in this way.
Calling this.$end()
Calling this.$end() within Action can also pass the message to Main through Promise.
The first parameter that $end() receives is the message data to be passed, and the optional second parameter is transfer (If "auto"is passed in, it will automatically identify all transferable objects in the message as transfer).
❗Attention: The Action cannot be defined as an arrow function if this.$end() needs to be called.
Once this.$end() is called correctly in the Action, it will immediately change the state of the corresponding Promise received in Main to fulfilled. After that, the Action will continue to execute, but the connection for the "request " will have been closed, and no further responses will be made (including responses through EventTarget). And the return value of the Action will be ignored.
It is more suitable for situations where Action needs to continue executing after making a response, or where a response needs to be made when excuting a callback function in Action.
For instance, in the Typescript example above, the pingLater Action is actually more suited to respond messages by calling this.$end():
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler-test/worker";
export type DemoActions = {
pingLater: (delay: number) => ActionResult<string>;
};
onmessage = createOnmessage<DemoActions>({
async pingLater(delay) {
setTimeout(() => {
this.$end("Worker received a message from Main " + delay + "ms ago.");
}, delay);
}
});Comparison of two ways to send terminating responses
Using the function return value:
- It's concise and convenient, and supports use in arrow functions.
- It has following limitations:
- Once
returnis used, theactionwill not execute further. - It cannot be used within the callback functions of
action. - It cannot transfer
transferable objects.
- Once
Using this.$end():
- It can flexibly match various situations, as reflected in:
- After using
this.$end(), theactioncan still execute further, but no further responses can be sent. - It can be used within the callback functions of
action. - It can transfer
transferable objects.
- After using
- It does not support use in arrow functions.
Responding without data
For compatibility with the way to respond by this.\$end() or this.\$post(), when no explicit value is returned in Action, or the data in the returned Promise is undefined, the state of the corresponding Promise received in Main remains unaffected by the Promise returned by Action. This allows this.$end() and this.$post() to control the response when there is no need to use the return value of Action for responding.
If an Action does not need to respond with any data through Promise, but needs to inform Main that the Action has been completed, then the following two ways can be referenced:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler-test/worker";
export type DemoActions = {
returnNull: () => ActionResult<null>;
};
onmessage = createOnmessage<DemoActions>({
async returnNull() {
// ...
return null
}
});// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler-test/worker";
export type DemoActions = {
returnVoid: () => ActionResult;
};
onmessage = createOnmessage<DemoActions>({
async returnVoid() {
// ...
this.$end();
}
});Responding through EventTarget (nonterminating responses)
Calling this.$post() within Action can pass the message to Main through EventTarget.
The first parameter that this.$opst() receives is the message data to be passed, and the optional second parameter is transfer (If "auto" is passed in, it will automatically identify all transferable objects in the message as transfer).
❗Attention: The Action cannot be defined as an arrow function if this.$post() needs to be called.
Once this.$post() is called correctly in the Action, it will immediately trigger the message event of the corresponding MessageSource (which extends methods similar to those in EventTarget) received in Main. The message can be received by setting the onmessage callback or by using addEventListener() to listen for the message event of MessageSource. If you need to receive the message through Promise as well, using addEventListener() it is recommended. MessageSource.addEventListener() will return MessageSource itself, allowing for convenient chaining to obtain the Promise.
Below is an example of responding with messages through both EventTarget and Promise:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";
export type DemoActions = {
// The type of the data passed through the `EventTarget` is also specified by the return type of the `Action` by default.
pingInterval: (
interval: number,
isImmediate: boolean,
duration: number
) => ActionResult<string>;
};
// After calling `pingInterval()`, a message will be posted every `interval` ms through `EventTarget`, and after `duration` ms, a message will be posted through `Promise` and the request connection will be closed.
onmessage = createOnmessage<DemoActions>({
async pingInterval(interval, isImmediate, duration) {
let counter = 0;
const genMsg = () => "ping " + ++counter;
if (isImmediate) this.$post(genMsg());
const intervalId = setInterval(() => {
this.$post(genMsg());
}, interval);
setTimeout(() => {
clearInterval(intervalId);
this.$end("no longer ping");
}, duration);
}
});// demo.main.ts
import { WorkerHandler } from "worker-handler/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
demoWorker
.execute("pingInterval", [], 1000, false, 5000) // A `MessageSource` will be obtained as the return value of `execute()`.
.addEventListener("message", (e) => {
console.log(e.data);
})
// If you use `addEventListener()` to listen for the `MessageSource`, it will return the `MessageSource` itself, allowing chaining calls.
.promise.then((res) => {
console.log(res.data);
});The type of data passed bythis.$post()can be specified not only by the return type of theAction, but also by explicitly defining the type ofthiswithinAction. You only need to set the type ofthisto the data type you intend to pass when defining the type of theAction. This doesn't actually define the type ofthisdirectly; instead,worker-handlerhandles it internally, modifying the parameter type ofthis.$post()` as well as the type of data received in the main thread.
Below is an example of responding with different data types of messages through both EventTarget and Promise:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";
export type DemoActions = {
postNumReturnStr: (this: number) => ActionResult<string>;
};
onmessage = createOnmessage<DemoActions>({
async postNumReturnStr() {
this.$post(1);
this.$end("1");
},
});// demo.main.ts
import { WorkerHandler } from "worker-handler/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
demoWorker
.execute("postNumReturnStr")
.addEventListener("message", (e) => {
console.log(e.data); // e.data will be inferred as a number type.
})
.promise.then((res) => {
console.log(res.data); // res.data will be inferred as a string type.
});Worker Proxy
Starting from v0.2.0, in environments that support Proxy, messages that cannot be handled by the structured clone algorithm is also allowed to be passed.
Basic Usage
If the data sent by Worker to Main cannot be structured cloned, then a Proxy that references this data (hereinafter referred to as Worker Proxy) will be created in Main as the received data:
- It is possible to operate on
Worker ProxyinMain, andWorker Proxywill update these operations to its referenced data. - The currently implemented
trapsforWorker Proxyare:get,set,apply,construct. - Since message passing is asynchronous, operations that return results such as
get,apply,constructwill return a new promise-like proxy object, representing the result of the operation. In environments that support theawaitsyntax, addingawaitbefore operating on theProxy(except forset) can simulate operations on its referenced data. In most cases, if you need to perform chained operations onWorker Proxy, you only need to use theawaitkeyword once. - If the data accessed by operating the
Worker Proxystill cannot be structured cloned, a newWorker Proxyreferencing that data will be obtained .
For example:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";
export type DemoActions = {
returnUncloneableData: () => ActionResult<{
f: () => string;
count: number;
increase: () => void;
Person: typeof Person;
layer1: { layer2: string; f: () => string };
}>;
};
class Person {
constructor(public name: string) {}
}
onmessage = createOnmessage<DemoActions>({
async returnUncloneableData() {
const data = {
f: () => "result of data.f()",
count: 0,
increase() {
this.count++;
},
Person,
layer1: { layer2: "nested value", f: () => "result of data.layer1.f()" },
};
return data
},
});// demo.main.ts
import { WorkerHandler } from "worker-handler/main";
import { DemoActions, UnwrapPromise } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
async function init() {
const { data } = await demoWorker.execute("returnUncloneableData").promise;
console.log(await data.f()); // "result of data.f()"
const person = await new data.Person("zzc6332");
console.log(await person.name); // "zzc6332"
console.log(await data.count); // 0
await data.increase();
console.log(await data.count); // 1
console.log(await data.layer1.layer2); // "nested value"
console.log(await data.layer1.f()); // "result of data.layer1.f()"
// The `set` operation of `Worker Proxy` currently does not fully support the type system, so type assertions are required. Either of the following two methods can be chosen:
(data.layer1.layer2 as any as UnwrapPromise<
typeof data.layer1.layer2
>) = "Hello Proxy!";
// data.layer1.layer2 = "Hello Proxy!" as any;
console.log(await data.layer1.layer2); // "Hello Proxy!"
}
init();Worker Proxy can be used as the payloads parameter of execute(), or as the parameters of methods called by other Worker Proxy. In this way, it will be parsed in the Worker as the original data referenced by the Worker Proxy.
Worker Array Proxy
Worker Array Proxy (supported from v0.2.1) is a special type of Worker Proxy. If the data referenced by a Worker Proxy is an array, then that Worker Proxy is a Worker Array Proxy.
Hereafter, the Worker Array Proxy will be referred to as proxyArr, and the array it references in the Worker will be referred to as ogArr.
All Main examples in this section are based on the following Worker example:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";
export type DemoActions = {
returnUncloneableArr: () => ActionResult<
{ index: number; f: () => string; layer1: { layer2: { index: number } } }[]
>;
};
onmessage = createOnmessage<DemoActions>({
async returnUncloneableArr() {
const ogArr = [0, 1, 2].map((_, index) => ({
index,
f: () => "result of index: " + index,
layer1: { layer2: { index } },
}));
return ogArr;
},
});Worker Array Proxy is an array-like object that can simulate some behaviors of arrays:
Accessing the item by index:
The
Worker Proxythat referencesogArr[index]can be accessed throughproxyArr[index]. For example:// demo.main.ts import { WorkerHandler } from "worker-handler/main"; import { DemoActions } from "./demo.worker"; const demoWorker = new WorkerHandler<DemoActions>( new Worker(new URL("./demo.worker.ts", import.meta.url)) ); async function init() { const { data: proxyArr } = await demoWorker.execute("returnUncloneableArr").promise; console.log(await proxyArr[0]); // Worker Proxy console.log(await proxyArr[0].index); // 0 console.log(await proxyArr[0].f()); // "result of index: 0" console.log(await proxyArr[0].layer1.layer2.index); // 0 } init();Getting the length of
proxyArr:The length of the
ogArrcan be accessed throughawait proxyArr.length. For example:// demo.main.ts import { WorkerHandler } from "worker-handler/main"; import { DemoActions } from "./demo.worker"; const demoWorker = new WorkerHandler<DemoActions>( new Worker(new URL("./demo.worker.ts", import.meta.url)) ); async function init() { const { data: proxyArr } = await demoWorker.execute("returnUncloneableArr").promise; console.log(await proxyArr.length); // 3 } init();Iterating over the items of
proxyArr:The
proxyArrimplements an asynchronous iterator (but not a regular iterator), so it can be iterated using thefor await...ofstatement. For example:// demo.main.ts import { WorkerHandler } from "worker-handler/main"; import { DemoActions } from "./demo.worker"; const demoWorker = new WorkerHandler<DemoActions>( new Worker(new URL("./demo.worker.ts", import.meta.url)) ); async function init() { const { data: proxyArr } = await demoWorker.execute("returnUncloneableArr") .promise; for await (const item of proxyArr) { console.log(await item.index); } console.log("Iteration executed by `for await...of` is completed!"); // --- The console output is as follows: --- // 0 // 1 // 2 // "Iteration executed by `for await...of` is completed!" // --- The console output is as above. --- } init();It can also be iterated by
proxyArr.forEach(). For example:// demo.main.ts import { WorkerHandler } from "worker-handler/main"; import { DemoActions } from "./demo.worker"; const demoWorker = new WorkerHandler<DemoActions>( new Worker(new URL("./demo.worker.ts", import.meta.url)) ); async function init() { const { data: proxyArr } = await demoWorker.execute("returnUncloneableArr") .promise; // `proxyArr.forEach()` is executed asynchronously. If you need to wait for the callback function in `forEach()` to complete, you can use the `await` keyword before `forEach()`. await proxyArr.forEach(async (item) => { console.log(await item.index); }); console.log("Iteration executed by `forEach()` is completed!"); // --- The console output is as follows: --- // 0 // 1 // 2 // "Iteration executed by `forEach()` is completed!" // --- The console output is as above. --- // 如果不使用 await 关键字,那么 forEach() 会晚于之后的同步代码执行 proxyArr.forEach(async (item) => { console.log(await item.index); }); console.log("Iteration executed by `forEach()` has not started!"); // --- The console output is as follows: --- // "Iteration executed by `forEach()` has not started!" // 0 // 1 // 2 // --- The console output is as above. --- } init();Using other array methods:
Any method with the same name of array methods can be called by
proxyArr, and all these methods are executed asynchronously.If the return value of the original array method is an array, then the method of
proxyArrwhich has the same name of the array method will also return an actual array. For example, usingproxyArr.map()can quickly convert proxyArr into an actual array:// demo.main.ts import { WorkerHandler } from "worker-handler/main"; import { DemoActions } from "./demo.worker"; const demoWorker = new WorkerHandler<DemoActions>( new Worker(new URL("./demo.worker.ts", import.meta.url)) ); async function init() { const { data: proxyArr } = await demoWorker.execute("returnUncloneableArr") .promise; const actualArr = await proxyArr.map((item) => item); console.log(actualArr); // [Worker Proxy, Worker Proxy, Worker Proxy] // Since `actualArr` is an actual array, it has a regular iterator interface and can be iterated using the `for...of` statement. for (const item of actualArr) { console.log(await item.index); } console.log("Iteration executed by `for...of` is completed!") // --- The console output is as follows: --- // 0 // 1 // 2 // "Iteration executed by `for...of` is completed!" // --- The console output is as above. --- // Note that when using `forEach()` to iterate over an actual array, if the callback function passed in is an asynchronous function, it will not wait for the asynchronous operations in the callback to complete. actualArr.forEach(async (item) => { console.log(await item.index); }); console.log("Iteration executed by `forEach()` has not started!"); // --- The console output is as follows: --- // "Iteration executed by `forEach()` has not started!" // 0 // 1 // 2 // --- The console output is as above. --- } init();The
ogArrcan be modified by methods of the correspondingproxyArrlikeunshift()orpush():// demo.main.ts import { WorkerHandler } from "worker-handler/main"; import { DemoActions } from "./demo.worker"; const demoWorker = new WorkerHandler<DemoActions>( new Worker(new URL("./demo.worker.ts", import.meta.url)) ); async function init() { const { data: proxyArr } = await demoWorker.execute("returnUncloneableArr") .promise; // Remove an item from the head of `ogArr`. console.log(await proxyArr.length); // 3 const shifted = await proxyArr.shift(); if (shifted) console.log(await shifted?.index); // 0 console.log(await proxyArr.length); // 2 for await (const item of proxyArr) { console.log(await item.index); } // --- The console output is as follows: --- // 1 // 2 // --- The console output is as above. --- // Insert an item at the tail of `ogArr`. if (shifted) console.log(await proxyArr.push(shifted)); // 3 for await (const item of proxyArr) { console.log(await item.index); } // --- The console output is as follows: --- // 1 // 2 // 0 // --- The console output is as above. --- } init();
Advanced
Worker Proxy Related Objects
The objects related to Worker Proxy are: Worker Proxy, Worker Array Proxy, and Carrier Proxy.
The relationships between Worker Proxy objects:
- If a
Worker Proxy(referred asWP1) references target data that exists with in the structure of the target data referenced by anotherWorker Proxy(referred asWP2), thenWP1is aChild Worker Proxy(akaChild) ofWP2. - If a
Worker Proxy(referred asWP1) references target data that is generated by the target data referenced by anotherWorker Proxy(referred asWP2) through function calls or class instantiation, thenWP1is aDerived Worker Proxy(akaAdopted Child) ofWP2. - If
WP1is aChildof aChildofWP2, thenWP1is also aChildofWP2. IfWP1is a descendant ofWP2, and there is anAdopted Childin their relationship chain, thenWP1is anAdopted ChildofWP2.
Worker Proxy can be obtained through the following ways:
- When executing the
ActioninWorkerand receiving the data it sends, if the data cannot be structured cloned, aWorker Proxythat references this data will be received inMain. - If the data obtained through a
Worker Proxystill cannot be structured cloned, a newWorker Proxythat references the data will be obtained. There are two situations:- After performing a
getoperation on aWorker Proxy, if aWorker Proxyis obtained asynchronously, the latter is aChildof the former. - After performing an
applyor aconstructoperation on aWorker Proxy, if aWorker Proxyis obtained asynchronously, the latter is anAdopted Childof the former.
- After performing a
- If a
Worker Proxyis aWorker Array Proxy, then when it executes certain array methods that require a callback function, theitemparameter in the callback function is aWorker Proxythat references the corresponding target array item, and the latter is aChildof the fommer.
The Worker Array Proxy is a special type of Worker Proxy. If a Worker Proxy references target data that is an array, then it is a Worker Array Proxy. It can execute array methods, and its other behaviors are the same as a regular Worker Proxy.
The Carrier Proxy is a promise-like object. Since operations on a Worker Proxy need to asynchronously take effect on the target data in Worker that it references, a carrier is needed to asynchronously obtain the result of the operation. The Carrier Proxy serves as this carrier. An operation on a Worker Proxy returns a Carrier Proxy, and the result of the operation is obtained asynchronously through this promise-like object. If further operations are performed on the Carrier Proxy, a new Carrier Proxy is also returned, enabling chain operations on the Worker Proxy.
If an operation on a Worker Proxy takes effect on its target data and results in a Promise object, the corresponding Carrier Proxy will simulate the behavior of this Promise object. See the example.
By accessing the proxyTypeSymbol key of the Worker Proxy related object, you can obtain a string that represents the type of this object:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";
export type DemoActions = {
returnUncloneableData: () => ActionResult<{
getString: () => string;
getUnclonable: () => { getString: () => string };
getArray: () => {
index: number;
f: () => string;
layer1: { layer2: { index: number } };
}[];
}>;
};
onmessage = createOnmessage<DemoActions>({
async returnUncloneableData() {
const data = {
getString: () => "result of getString()",
getUnclonable() {
return { getString: data.getString };
},
getArray: () =>
[0, 1, 2].map((_, index) => ({
index,
f: () => "index: " + index,
layer1: { layer2: { index } },
})),
};
return data;
},
});// demo.main.ts
import { proxyTypeSymbol, WorkerHandler } from "worker-handler/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
async function init() {
const { data } = await demoWorker.execute("returnUncloneableData")
.promise;
console.log(data[proxyTypeSymbol]); // "Worker Proxy"
console.log(data.getString[proxyTypeSymbol]); // "Carrier Proxy"
console.log((await data.getString)[proxyTypeSymbol]); // "Worker Proxy"
console.log((await data.getArray())[proxyTypeSymbol]); // "Worker Array Proxy"
console.log((await data.getUnclonable())[proxyTypeSymbol]); // "Worker Proxy"
const arrProxy = await data.getArray();
await arrProxy.forEach((item) => {
console.log(item[proxyTypeSymbol]); // "Worker Proxy"
});
}
init();Clean Up Target Data
When an Action in the Worker needs to post data that cannot be structured cloned to Main, a Worker Proxy referencing this data will be created in Main. The referenced data is stored and prevented from being garbage collected. Starting from v0.2.4, target data that is no longer in use can be cleaned up automatically or manually.
Auto Cleanup
In environments that support FinalizationRegistry, the referenced target data can be automatically cleaned up when the Worker Proxy is garbage collected. This feature can be enabled or disabled (enabled by default) when creating a WorkerHandler instance. For example:
// demo.main.ts
import { WorkerHandler } from "worker-handler/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url)),
{ autoCleanup: false } // disable autoCleanup
);Manual Cleanup
For environments that do not support FinalizationRegistry, the target data will be cleaned up when the Worker Proxy that referencing it is manually revoked.
When revoking a Worker Proxy, each of its Children will also be recursively revoked. Additionally, there is an option to specify whether to recursively revoke each of its Adopted Children.
There are two ways to revoke a Worker Proxy, and they have the same effect:
Using
revokeProxy()of theWorkerHandlerinstance:/** * Recursively revoke Worker Proxy and clean up the corresponding data. * @param proxy The Worker Proxy to be revoked * @param options Configuration parameter `{ derived?: boolean }`, and can also be simplified to just passing a boolean value or `0 | 1`. If `true`, it indicates recursively revoking the Worker Proxy’s Children and Adopted Children; otherwise, it only recursively revokes the Children. */ revokeProxy( proxy: WorkerProxy<any>, options?: { derived?: boolean } | boolean | 0 | 1 ): voidUsing the method obtained with the
revokeSymbolkey of theWorker Proxy:[revokeSymbol](options?: { derived?: boolean } | boolean | 0 | 1): void;
For example:
import { ActionResult, createOnmessage } from "worker-handler/worker";
export type DemoActions = {
returnUncloneableData: () => ActionResult<{
getString: () => string;
getUnclonableData: () => { getString: () => string };
getArray: () => {
index: number;
f: () => string;
layer1: { layer2: { index: number } };
}[];
}>;
};
onmessage = createOnmessage<DemoActions>({
async returnUncloneableData() {
const data = {
getString: () => "result of getString()",
getUnclonableData() {
return { getString: data.getString };
},
getArray: () =>
[0, 1, 2].map((_, index) => ({
index,
f: () => "index: " + index,
layer1: { layer2: { index } },
})),
};
return data;
},
});// demo.main.ts
import { proxyTypeSymbol, revokeSymbol, WorkerHandler } from "src/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url)),
{ autoCleanup: false }
);
// Revoke the data using different derived options and check the state of the different Proxy objects.
async function init(derived?: { derived?: boolean } | boolean | 0 | 1) {
const { data } = await demoWorker.execute("returnUncloneableData2").promise;
const getString = await data.getString;
const arrayProxy = await data.array;
const array = await arrayProxy.map((item) => item);
const derivedArrayProxy = await data.getArray();
const derivedArray = await derivedArrayProxy.map((item) => item);
const getStringOfUnclonableData = await data.getUnclonableData().getString;
data[revokeSymbol](derived); // Equivalent to `demoWorker.revokeProxy(data, derived);`
try {
console.log(data[proxyTypeSymbol]);
} catch (error) {
console.log(error); // Whether or not derived is enabled, it will output: "TypeError: Cannot perform 'get' on a proxy that has been revoked"
}
// `getString` is a Child of `data`
try {
console.log(getString());
} catch (error) {
console.log(error); // Whether or not derived is enabled, it will output: "TypeError: Cannot perform 'apply' on a proxy that has been revoked"
}
// `array[0]` is a Child of `data`
try {
console.log(array[0][proxyTypeSymbol]);
} catch (error) {
console.log(error); // Whether or not derived is enabled, it will output: "TypeError: Cannot perform 'get' on a proxy that has been revoked"
}
// `derivedArray[0]` is an Adopted Child of `data`
try {
console.log(derivedArray[0][proxyTypeSymbol]); // When derived is not enabled, it outputs: "Worker Proxy"
} catch (error) {
console.log(error); // When derived is enabled, it outputs: "TypeError: Cannot perform 'get' on a proxy that has been revoked"
}
// getStringOfUnclonableData 是 data 的 Adopted Child
try {
console.log(await getStringOfUnclonableData()); // When derived is not enabled, it outputs: "result of getString()"
} catch (error) {
console.log(error); // When derived is enabled, it outputs: "TypeError: Cannot perform 'apply' on a proxy that has been revoked"
}
}
init(1); // Equivalent to `init(true);` or `init({ derived: true });`
init(); // Equivalent to `init(0);` or `init(false);` or `init({ derived: false });`Not Recommended Practices
If the target data of a Worker Proxy is responded to by this.$post() in an Action, the following two practices may cause unexpected behavior in the cleanup of the target data when calling the corresponding MessageSource.addEventListener():
Do not call
addEventListener()asynchronously after a period of time has passed since theMessageSourcewas created, for example:const demoMessageSource = demoWorker.execute("demoAction") setTimeout(()=>{ demoMessageSource.addEventListener(...) }, 1000)Doing so may result in the
this.$post()in theActionhaving already executed by the time the listener is added. This will cause the target data to be stored and prevented from being garbage collected, but without creating the correspondingWorker ProxyinMain. Consequently, the target data cannot be cleaned up by revoking (or garbage collecting) the correspondingWorker Proxy.Do not call
addEventListener()multiple times on a singleMessageSource. Otherwise, multipleWorker Proxieswill be created for the same target data. As any one of theseWorker Proxiesis revoked (or garbage collected), the target data will be cleaned up and will no longer be accessible to the otherWorker Proxiesthat reference it.
Promise Object Messages
The Promise object messages described in this chapter do not refer to messages responded through Promise (terminating responses), but rather to messages which have a Promise object as their target data.
Starting from v0.2.5, support for Promise object messages has been added, allowing Main to intuitively handle Promise objects from Worker.
The generation of Promise object messages can be broadly categorized into the following three cases:
- Directly responding with a
Promiseobject through aterminating response; - Directly responding with a
Promiseobject through anonterminating response; - Manipulating the
Promiseobject in the target data through aWorker Proxy.
Promise Object Messages In Terminating Responses
If a Promise object is posted through a terminating response in Action, the corresponding MessageSource.promise in Main will simulate the behavior of that Promise object.
If the value that the Promise object to be resolved with cannot be structured cloned, the MessageSource.promise will be resolved with a Worker Proxy that references that value.
For example:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";
export type DemoActions = {
returnPromiseWithStr: () => ActionResult<Promise<string>>;
returnPromiseWithFn: () => ActionResult<Promise<() => string>>;
};
onmessage = createOnmessage<DemoActions>({
async returnPromiseWithStr() {
this.$end(
new Promise<string>((resolve, reject) => {
if (Math.random() >= 0.5) {
resolve('fulfilled test string of "returnPromiseWithStr"');
} else {
reject('rejected test string of "returnPromiseWithStr"');
}
})
);
},
async returnPromiseWithFn() {
this.$end(
new Promise<() => string>((resolve, reject) => {
if (Math.random() >= 0.5) {
resolve(() => 'fulfilled test string of "returnPromiseWithFn"');
} else {
reject('rejected test string of "returnPromiseWithFn"');
}
})
);
},
});// demo.main.ts
import { proxyTypeSymbol, WorkerHandler } from "src/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
async function init() {
try {
const { data } = await demoWorker.execute("returnPromiseWithStr").promise;
// In the case where the target Promise object is fulfilled, if the value which the target Promise object resolved with can be structured cloned, then the value can be directly obtained.
console.log(data); // 'fulfilled test string of "returnPromiseWithStr"'
} catch (error) {
// In the case where the target Promise object is rejected.
console.log(error); // 'rejected test string of "returnPromiseWithStr"'
}
try {
const { data } = await worker .execute("returnPromiseWithFn").promise;
// In the case where the target Promise object is fulfilled, if the value which the target Promise object resolved with can not be structured cloned, then a Worker Proxy referencing the value will be obtained.
console.log(data[proxyTypeSymbol]); // "Worker Proxy"
console.log(await data()); // 'fulfilled test string of "returnPromiseWithFn"'
} catch (error) {
// In the case where the target Promise object is rejected.
console.log(error); // 'rejected test string of "returnPromiseWithFn"'
}
}
init();If you use the return value of Action to respond with a Promise object, you need to explicitly annotate the return value type when defining the Action function:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";
export type DemoActions = {
returnPromiseWithStr: () => ActionResult<Promise<string>>;
};
onmessage = createOnmessage<DemoActions>({
// Explicitly annotate the return type of Action, which needs to match the type in DemoActions
async returnPromiseWithStr(): ActionResult<Promise<string>> {
return new Promise<string>((resolve, reject) => {
if (Math.random() >= 0.5) {
resolve('fulfilled test string of "returnPromiseWithStr"');
} else {
reject('rejected test string of "returnPromiseWithStr"');
}
});
}
});// demo.main.ts
import { proxyTypeSymbol, WorkerHandler } from "src/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
async function init() {
try {
const { data } = await demoWorker.execute("returnPromiseWithStr").promise;
// In the case where the target Promise object is fulfilled
console.log(data); // 'fulfilled test string of "returnPromiseWithStr"'
} catch (error) {
// In the case where the target Promise object is rejected
console.log(error); // 'rejected test string of "returnPromiseWithStr"'
}
}
init();Promise Object Messages In Nonterminating Responses
If a Promise object is posted through a nonterminating response in Action, a simulated Promise object can be obtained in Main by listening to the corresponding MessageSource.
If the value that the Promise object to be resolved with cannot be structured cloned, the MessageSource.promise will be resolved with a Worker Proxy that references that value.
For exampler:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";
export type DemoActions = {
postPromiseWithStr: () => ActionResult<Promise<string>>;
postPromiseWithFn: () => ActionResult<Promise<() => string>>;
};
onmessage = createOnmessage<DemoActions>({
async postPromiseWithStr() {
const promise = new Promise<string>((resolve, reject) => {
if (Math.random() >= 0.5) {
resolve('fulfilled test string of "postPromiseWithStr"');
} else {
reject('rejected test string of "postPromiseWithStr"');
}
});
this.$post(promise);
this.$end(promise);
},
async postPromiseWithFn() {
const promise = new Promise<() => string>((resolve, reject) => {
if (Math.random() >= 0.5) {
resolve(() => 'fulfilled test string of "postPromiseWithFn"');
} else {
reject('rejected test string of "postPromiseWithFn"');
}
});
this.$post(promise);
this.$end(promise);
},
});// demo.main.ts
import { proxyTypeSymbol, WorkerHandler } from "src/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
async function init() {
const messageSource1 = worker.execute("postPromiseWithStr");
messageSource1.addEventListener("message", async (e) => {
try {
// e.data is a Promise object that will simulate the target Promise object.
const resolvedValue = await e.data;
// In the case where the target Promise object is fulfilled, if the value which the target Promise object resolved with can be structured cloned, then the value can be directly obtained.
console.log(resolvedValue); // 'fulfilled test string of "postPromiseWithStr"'
} catch (error) {
// In the case where the target Promise object is rejected.
console.log(error); // 'rejected test string of "postPromiseWithStr"'
}
});
try {
const { data } = await messageSource1.promise;
// In the case where the target Promise object is fulfilled, if the value which the target Promise object resolved with can be structured cloned, then the value can be directly obtained.
console.log(data); // 'fulfilled test string of "postPromiseWithStr"'
} catch (error) {
// In the case where the target Promise object is rejected.
console.log(error); // 'rejected test string of "postPromiseWithStr"'
}
const messageSource2 = worker.execute("postPromiseWithFn");
messageSource2.addEventListener("message", async (e) => {
try {
// e.data is a Promise object that will simulate the target Promise object.
const data = await e.data;
// In the case where the target Promise object is fulfilled, if the value which the target Promise object resolved with can be structured cloned, then the value can be directly obtained.
console.log(data[proxyTypeSymbol]); // "Worker Proxy"
const resultStr = await data();
console.log(resultStr); // 'fulfilled test string of "returnPromiseWithFn"'
} catch (error) {
// In the case where the target Promise object is rejected.
console.log(error); // // 'rejected test string of "returnPromiseWithFn"'
}
});
try {
const { data } = await messageSource2.promise;
// In the case where the target Promise object is fulfilled, if the value which the target Promise object resolved with can be structured cloned, then the value can be directly obtained.
console.log(data[proxyTypeSymbol]); // "Worker Proxy"
console.log(await data()); // 'fulfilled test string of "returnPromiseWithFn"'
} catch (error) {
// In the case where the target Promise object is rejected.
console.log(error); // 'rejected test string of "returnPromiseWithFn"'
}
}
init();Promise Object Messages In Worker Proxies
If the Worker Proxy references target data that contains (or can generate) Promise objects, then when attempting to access the Promise objects through the Worker Proxy, you will get a Carrier Proxy that references the Promise object. This Carrier Proxy will simulate the behavior of the Promise object. For example:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";
export type DemoActions = {
getPromise: () => ActionResult<{ getPromise: () => Promise<string> }>;
};
onmessage = createOnmessage<DemoActions>({
async getPromise() {
this.$end({
getPromise: () =>
new Promise<string>((resolve, reject) => {
if (Math.random() >= 0.5) {
resolve('fulfilled test string of "getPromise"');
} else {
reject('rejected test string of "getPromise"');
}
}),
});
},
});// demo.main.ts
import { proxyTypeSymbol, WorkerHandler } from "src/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
async function init() {
const { data } = await worker.execute("getPromise").promise;
try {
// `data.getPromise()` will generate a Carrier Proxy that references the target Promise object, the Carrier Proxy will simulate the target Promise object.
console.log(data.getPromise()[proxyTypeSymbol]); // "Carrier Proxy"
const resolvedValue = await data.getPromise();
// In the case where the target Promise object is fulfilled.
console.log(resolvedValue); // 'fulfilled test string of "getPromise"'
} catch (error) {
// In the case where the target Promise object is rejected.
console.log(error); // 'rejected test string of "getPromise"'
}
}
init();APIs
worker-handler/main
WorkerHandler
Constructor:
Parameters:
workerSrc:A
Workerinstance. Alternatively, if the environment can provide the path to the bundledWorkerscript as astringorURL, thay can also be passed in asworkerSrc.options:Configuration options for creaing a
workerHandlerinstance. Currently, there is only one property,autoCleanup, which is aboolean. It indicates whether to automatically clean up the target data referenced by theWorker Proxythat has been garbage collected if the environment supportsFinalizationRegistry. The default value istrue.
Returns a
WorkerHandlerinstance.
Instance methods:
execute(actionName, options, ...payloads):The
execute()method will open a connection and call the targetActioninWorker.Parameters:
actionName:The name of the target
Actionto be called.options:The options for calling the
Action.The complete form of
optionsis an object that includes the propertiestransferandtimeout:The value of
transferis an array of transferable objects that will have their ownership transferred to theWorker, used to specify thetransferable objectsinpayloadsthat need to be transferred.If the value of
transferis"auto", then thetransferable objectsinpayloadswil be automatically identified.The value of
timeoutis a number of milliseconds representing the timeout duration for this connection.After the specified timeout, the connection will be closed, no further responses will be received, and the
Promisereturned by theActionwill becomerejected.A number less than or equal to
0means no timeout.
If only one of
transferortimeoutneeds to take effect, you can directly pass the value of the one you need to theoptions.If neither
transfernortimeoutneeds to take effect, you can omit the values when not passing anypayload. Otherwise, you can pass any of the following values:null,undefined,[], any number less than or equal to0....
payloads:The parameters required for the calling of the target
Action, passed in sequence.
Return value:
A
MessageSource.terminate()The
terminate()method will immediately terminate theWorker.revokeProxy(workerProxy, options?)Upon execution, the specified
Worker Proxyand its relatedWorker Proxieswill be revoked, and the referenced target data they reference will be cleaned up.Parameters:
workerProxy:The
Worker Proxyto be revoked.options:Optional configuration parameters
{ derived?: boolean }, which can also be simplified to abooleanvalue or0 | 1. Iftrue, it indicates recursively revoking theChildrenandAdopted Childrenof theWorker Proxy; otherwise, it only recursively revokes theChildren.
MessageSource
MessageSource is used to receive response messages from Action.
Properties:
promise:A
Promiseobject.When a
terminating responseis made inAction, thepromisewill becomefulfilledand receive the response message.If an error is thrown in
Actionor theterminating responsemessage made byActioncannot be structured cloned, and the current environment does not supportProxy, thepromisewill becomerejectedand receive the error message.When the
promiseis settled, the connection is closed, andActionwill not make any more response messages (includingnonterminating responsemessages).onmessage:A callback function that is called when
Actionmakes anonterminating responsemessage.It receives a parameter
e, through which thenonterminating responsemessage made byActioncan be accessed viae.data.onmessageerror:A callback function that is called when the
nonterminating responsemessage made byActioncannot be structured cloned and the current environment does not supportProxy.In
typescript, this situation is usually detected during type checking, so there is generally no need to listen for themessageerrorevent.readyState:A number representing the current state of the connection :
0—connecting,1—open,2—closed.
Methods:
addEventListener()Adds an event listener, which can listen for events such as
messageandmessageerror.It extends
EventTarget.addEventListener()and returns the correspondingMessageSourceobject after being called.
UnwrapPromise
UnwrapPromise is an utility type that can accept a Promise type or a PromiseLike type as a generic parameter and can extract the inner type. It is used for type assertion when performing set operations on a Worker Proxy or a Carrier Proxy.
ReceivedData
ReceivedData is a utility type that can accept any type (representing the type of data in the response from an Action) as a generic parameter. It obtains the type of corresponding data to be received in Main based on whether the generic parameter can be structured cloned (it is either the generic parameter type itself or a WorkerProxy type).
WorkerProxy / CarrierProxy
These two types indicate that it is either a Worker Proxy or a Carrier Proxy. They accept a generic parameter representing the type of the target data referenced by the Worker Proxy or the Carrier Proy.
In worker-handler/main, some symbol keys are provided. They can be used to access certain properties or methods of the Worker Proxy or the Carrier Proxy:
proxyTypeSymbolUsed to obtain a string that representing the type of the current
Proxy. See the example for usage.revokeSymbolOnly applicable to
Worker Proxy, used to obtain a method to revoke the currentWorker Proxyand clean up the corresponding data. See the example for usage.
worker-handler/worker
createOnmessage()
Define Actions within an object, which is passed to the createOnmessage() when called, and return a listener function for the message event of Worker.
Use this.$post() within Action to make nonterminating responses, and use this.$end() or return a value to make terminating responses.
ActionResult
ActionResult is a type that represents the return value of an Action. It requires a generic parameter that specifies the type of response message to be passed, and returns a Promise type.
When defining the Action type, ActionResult is required to generate the type of return value.
The generic parameter passed also affects the types of parameters received by this.$post() and this.$end() within the Action.
If no generic parameters are passed, it is equivalent to ActionResult<void>.
Significant Updates
v0.2.0
In environments that support Proxy, messages that cannot be handled by the structured clone algorithm can also be passed.
When passing messages, if the
transferoption is not specified, alltransferable objectswill be automatically identified from the message and placed intotransfer.When sending a
terminating responsethrough the return value ofAction, the return form of[messageData, [...transferable]]from versionv0.1.xis discontinued. This means that if the response data is an array, it can also be returned directly.It is because if using
this.$end()form to send aterminating response,transfercan be specified more intuitively, and everything that can be done using the return value form can also be done usingthis.$end(). Therefore, the use of the return value form has been simplified, making it more convenient to use in some situations to sendterminating responses.ActionResult<Data>is equivalent toActionResult<Data | void>from versionv0.1.x.
v0.2.1
- Add Worker Array Proxy feature.
- When passing messages, if the
transferoption is not specified,transferable objectswill not be transferred.
v0.2.4
- Supports cleaning up the target data referenced by the Worker Proxy that is no longer in use.
v0.2.5
- Supports Promise Object Message feature.
v0.2.10
- Allowing
this.$post()andthis.$end()to send different types of data.
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago