worker-handler
v0.2.12
Published
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.
Downloads
514
Maintainers
Readme
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-handler
Basic 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
transfer
is an array of transferable objects that will have their ownership transferred to theWorker
, used to specify thetransferable objects
inpayloads
that need to be transferred.If the value of
transfer
is"auto"
, then thetransferable objects
inpayloads
wil be automatically identified.The value of
timeout
is the timeout duration for this connection.After the timeout, the connection will be closed, no further responses will be received, and the
Promise
returned byAction
will becomerejected
.
The passing of the second parameter can also be simplified according to follow situations:
- If only
transfer
is needed, an array can be directly passed. - If only
timeout
is 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
return
is used, theaction
will 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()
, theaction
can 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 by
this.$post()can be specified not only by the return type of the
Action, but also by explicitly defining the type of
thiswithin
Action. You only need to set the type of
thisto the data type you intend to pass when defining the type of the
Action. This doesn't actually define the type of
thisdirectly; instead,
worker-handlerhandles it internally, modifying the parameter type of
this.$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 Proxy
inMain
, andWorker Proxy
will update these operations to its referenced data. - The currently implemented
traps
forWorker Proxy
are:get
,set
,apply
,construct
. - Since message passing is asynchronous, operations that return results such as
get
,apply
,construct
will return a new promise-like proxy object, representing the result of the operation. In environments that support theawait
syntax, addingawait
before 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 theawait
keyword once. - If the data accessed by operating the
Worker Proxy
still cannot be structured cloned, a newWorker Proxy
referencing 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 Proxy
that 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
ogArr
can 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
proxyArr
implements an asynchronous iterator (but not a regular iterator), so it can be iterated using thefor await...of
statement. 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
proxyArr
which 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
ogArr
can be modified by methods of the correspondingproxyArr
likeunshift()
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
), thenWP1
is 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, thenWP1
is aDerived Worker Proxy
(akaAdopted Child
) ofWP2
. - If
WP1
is aChild
of aChild
ofWP2
, thenWP1
is also aChild
ofWP2
. IfWP1
is a descendant ofWP2
, and there is anAdopted Child
in their relationship chain, thenWP1
is anAdopted Child
ofWP2
.
Worker Proxy
can be obtained through the following ways:
- When executing the
Action
inWorker
and receiving the data it sends, if the data cannot be structured cloned, aWorker Proxy
that references this data will be received inMain
. - If the data obtained through a
Worker Proxy
still cannot be structured cloned, a newWorker Proxy
that references the data will be obtained. There are two situations:- After performing a
get
operation on aWorker Proxy
, if aWorker Proxy
is obtained asynchronously, the latter is aChild
of the former. - After performing an
apply
or aconstruct
operation on aWorker Proxy
, if aWorker Proxy
is obtained asynchronously, the latter is anAdopted Child
of the former.
- After performing a
- If a
Worker Proxy
is aWorker Array Proxy
, then when it executes certain array methods that require a callback function, theitem
parameter in the callback function is aWorker Proxy
that references the corresponding target array item, and the latter is aChild
of 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 theWorkerHandler
instance:/** * 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 ): void
Using the method obtained with the
revokeSymbol
key 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 theMessageSource
was created, for example:const demoMessageSource = demoWorker.execute("demoAction") setTimeout(()=>{ demoMessageSource.addEventListener(...) }, 1000)
Doing so may result in the
this.$post()
in theAction
having 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 Proxy
inMain
. 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 Proxies
will be created for the same target data. As any one of theseWorker Proxies
is revoked (or garbage collected), the target data will be cleaned up and will no longer be accessible to the otherWorker Proxies
that 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
Promise
object through aterminating response
; - Directly responding with a
Promise
object through anonterminating response
; - Manipulating the
Promise
object 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
Worker
instance. Alternatively, if the environment can provide the path to the bundledWorker
script as astring
orURL
, thay can also be passed in asworkerSrc
.options
:Configuration options for creaing a
workerHandler
instance. Currently, there is only one property,autoCleanup
, which is aboolean
. It indicates whether to automatically clean up the target data referenced by theWorker Proxy
that has been garbage collected if the environment supportsFinalizationRegistry
. The default value istrue
.
Returns a
WorkerHandler
instance.
Instance methods:
execute(actionName, options, ...payloads)
:The
execute()
method will open a connection and call the targetAction
inWorker
.Parameters:
actionName
:The name of the target
Action
to be called.options
:The options for calling the
Action
.The complete form of
options
is an object that includes the propertiestransfer
andtimeout
:The value of
transfer
is an array of transferable objects that will have their ownership transferred to theWorker
, used to specify thetransferable objects
inpayloads
that need to be transferred.If the value of
transfer
is"auto"
, then thetransferable objects
inpayloads
wil be automatically identified.The value of
timeout
is 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
Promise
returned by theAction
will becomerejected
.A number less than or equal to
0
means no timeout.
If only one of
transfer
ortimeout
needs to take effect, you can directly pass the value of the one you need to theoptions
.If neither
transfer
nortimeout
needs 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 Proxy
and its relatedWorker Proxies
will be revoked, and the referenced target data they reference will be cleaned up.Parameters:
workerProxy
:The
Worker Proxy
to be revoked.options
:Optional configuration parameters
{ derived?: boolean }
, which can also be simplified to aboolean
value or0 | 1
. Iftrue
, it indicates recursively revoking theChildren
andAdopted Children
of theWorker Proxy
; otherwise, it only recursively revokes theChildren
.
MessageSource
MessageSource
is used to receive response messages from Action
.
Properties:
promise
:A
Promise
object.When a
terminating response
is made inAction
, thepromise
will becomefulfilled
and receive the response message.If an error is thrown in
Action
or theterminating response
message made byAction
cannot be structured cloned, and the current environment does not supportProxy
, thepromise
will becomerejected
and receive the error message.When the
promise
is settled, the connection is closed, andAction
will not make any more response messages (includingnonterminating response
messages).onmessage
:A callback function that is called when
Action
makes anonterminating response
message.It receives a parameter
e
, through which thenonterminating response
message made byAction
can be accessed viae.data
.onmessageerror
:A callback function that is called when the
nonterminating response
message made byAction
cannot 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 themessageerror
event.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
message
andmessageerror
.It extends
EventTarget.addEventListener()
and returns the correspondingMessageSource
object 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
:
proxyTypeSymbol
Used to obtain a string that representing the type of the current
Proxy
. See the example for usage.revokeSymbol
Only applicable to
Worker Proxy
, used to obtain a method to revoke the currentWorker Proxy
and 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
transfer
option is not specified, alltransferable objects
will be automatically identified from the message and placed intotransfer
.When sending a
terminating response
through the return value ofAction
, the return form of[messageData, [...transferable]]
from versionv0.1.x
is 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
,transfer
can 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
transfer
option is not specified,transferable objects
will 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.