@effectionx/worker 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/channel.test.ts +902 -0
- package/channel.ts +380 -0
- package/dist/channel.d.ts +167 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +236 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +137 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +27 -1
- package/dist/worker-main.d.ts +16 -2
- package/dist/worker-main.d.ts.map +1 -1
- package/dist/worker-main.js +78 -6
- package/dist/worker.d.ts +34 -2
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +138 -22
- package/package.json +8 -4
- package/test-assets/bidirectional-worker.ts +19 -0
- package/test-assets/concurrent-requests-worker.ts +9 -0
- package/test-assets/error-cause-worker.ts +19 -0
- package/test-assets/error-handling-worker.ts +12 -0
- package/test-assets/error-throw-worker.ts +9 -0
- package/test-assets/no-requests-worker.ts +5 -0
- package/test-assets/send-inside-foreach-worker.ts +18 -0
- package/test-assets/sequential-requests-worker.ts +10 -0
- package/test-assets/single-request-worker.ts +8 -0
- package/test-assets/slow-request-worker.ts +8 -0
- package/tsconfig.json +3 -0
- package/types.ts +157 -3
- package/worker-main.ts +119 -8
- package/worker.test.ts +302 -4
- package/worker.ts +213 -29
- package/dist/message-channel.d.ts +0 -3
- package/dist/message-channel.d.ts.map +0 -1
- package/dist/message-channel.js +0 -13
- package/message-channel.ts +0 -13
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { Operation } from "effection";
|
|
1
|
+
import type { Operation, Result, Subscription } from "effection";
|
|
2
|
+
/**
|
|
3
|
+
* Messages sent from host to worker (control messages).
|
|
4
|
+
*/
|
|
2
5
|
export type WorkerControl<TSend, TData> = {
|
|
3
6
|
type: "init";
|
|
4
7
|
data: TData;
|
|
@@ -9,15 +12,146 @@ export type WorkerControl<TSend, TData> = {
|
|
|
9
12
|
} | {
|
|
10
13
|
type: "close";
|
|
11
14
|
};
|
|
12
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Messages sent from worker to host.
|
|
17
|
+
*
|
|
18
|
+
* @template WRequest - value worker sends to host in requests
|
|
19
|
+
* @template TReturn - return value when worker completes
|
|
20
|
+
*/
|
|
21
|
+
export type WorkerToHost<WRequest, TReturn> = {
|
|
22
|
+
type: "open";
|
|
23
|
+
} | {
|
|
24
|
+
type: "request";
|
|
25
|
+
value: WRequest;
|
|
26
|
+
response: MessagePort;
|
|
27
|
+
} | {
|
|
28
|
+
type: "close";
|
|
29
|
+
result: Result<TReturn>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Serialized error format for cross-boundary communication.
|
|
33
|
+
* Error objects cannot be cloned via postMessage, so we serialize them.
|
|
34
|
+
*/
|
|
35
|
+
export interface SerializedError {
|
|
36
|
+
name: string;
|
|
37
|
+
message: string;
|
|
38
|
+
stack?: string;
|
|
39
|
+
cause?: SerializedError;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A Result type for cross-boundary communication where errors are serialized.
|
|
43
|
+
* Unlike effection's Result<T> which uses Error, this uses SerializedError.
|
|
44
|
+
*
|
|
45
|
+
* Used by channel primitives to send success/error responses over MessageChannel.
|
|
46
|
+
*/
|
|
47
|
+
export type SerializedResult<T> = {
|
|
48
|
+
ok: true;
|
|
49
|
+
value: T;
|
|
50
|
+
} | {
|
|
51
|
+
ok: false;
|
|
52
|
+
error: SerializedError;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Messages sent over a channel that supports progress streaming.
|
|
56
|
+
* Used by useChannelRequest to send progress updates and final response.
|
|
57
|
+
*/
|
|
58
|
+
export type ChannelMessage<TResponse, TProgress> = {
|
|
59
|
+
type: "progress";
|
|
60
|
+
data: TProgress;
|
|
61
|
+
} | {
|
|
62
|
+
type: "response";
|
|
63
|
+
result: SerializedResult<TResponse>;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Acknowledgement messages sent back over a channel.
|
|
67
|
+
* Used by useChannelResponse to acknowledge receipt of messages.
|
|
68
|
+
*/
|
|
69
|
+
export type ChannelAck = {
|
|
70
|
+
type: "ack";
|
|
71
|
+
} | {
|
|
72
|
+
type: "progress_ack";
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Context passed to forEach handler for progress streaming.
|
|
76
|
+
* Allows the handler to send progress updates back to the requester.
|
|
77
|
+
*
|
|
78
|
+
* @template TProgress - The progress data type
|
|
79
|
+
*/
|
|
80
|
+
export interface ForEachContext<TProgress> {
|
|
13
81
|
/**
|
|
14
|
-
*
|
|
82
|
+
* Send a progress update to the requester.
|
|
83
|
+
* This operation blocks until the requester acknowledges receipt (backpressure).
|
|
84
|
+
*
|
|
85
|
+
* @param data - The progress data to send
|
|
86
|
+
*/
|
|
87
|
+
progress(data: TProgress): Operation<void>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Serialize an Error for transmission via postMessage.
|
|
91
|
+
* Recursively serializes error.cause if present.
|
|
92
|
+
*/
|
|
93
|
+
export declare function serializeError(error: Error): SerializedError;
|
|
94
|
+
/**
|
|
95
|
+
* Create an Error from a serialized error, with original data in `cause`.
|
|
96
|
+
*
|
|
97
|
+
* @param context - Description of where the error occurred (e.g., "Host handler failed")
|
|
98
|
+
* @param serialized - The serialized error data
|
|
99
|
+
*/
|
|
100
|
+
export declare function errorFromSerialized(context: string, serialized: SerializedError): Error;
|
|
101
|
+
/**
|
|
102
|
+
* A send function that supports both simple request/response and progress streaming.
|
|
103
|
+
*
|
|
104
|
+
* @template WRequest - value worker sends to host
|
|
105
|
+
* @template WResponse - value worker receives from host
|
|
106
|
+
*/
|
|
107
|
+
export interface WorkerSend<WRequest, WResponse> {
|
|
108
|
+
/**
|
|
109
|
+
* Send a request to the host and wait for a response.
|
|
110
|
+
* Ignores any progress updates from the host.
|
|
111
|
+
*/
|
|
112
|
+
(value: WRequest): Operation<WResponse>;
|
|
113
|
+
/**
|
|
114
|
+
* Send a request to the host and receive a subscription that yields
|
|
115
|
+
* progress updates and returns the final response.
|
|
116
|
+
*
|
|
117
|
+
* @template WProgress - progress type from host
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* const subscription = yield* send.stream<number>(request);
|
|
122
|
+
* let next = yield* subscription.next();
|
|
123
|
+
* while (!next.done) {
|
|
124
|
+
* console.log("Progress:", next.value);
|
|
125
|
+
* next = yield* subscription.next();
|
|
126
|
+
* }
|
|
127
|
+
* const response = next.value;
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
stream<WProgress>(value: WRequest): Operation<Subscription<WProgress, WResponse>>;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Options passed to the worker's main function.
|
|
134
|
+
*
|
|
135
|
+
* @template TSend - value host sends to worker
|
|
136
|
+
* @template TRecv - value host receives from worker (response to host's send)
|
|
137
|
+
* @template TData - initial data passed to worker
|
|
138
|
+
* @template WRequest - value worker sends to host in requests
|
|
139
|
+
* @template WResponse - value worker receives from host (response to worker's send)
|
|
140
|
+
*/
|
|
141
|
+
export interface WorkerMainOptions<TSend, TRecv, TData, WRequest = never, WResponse = never> {
|
|
142
|
+
/**
|
|
143
|
+
* Namespace that provides APIs for working with incoming messages from host.
|
|
15
144
|
*/
|
|
16
145
|
messages: WorkerMessages<TSend, TRecv>;
|
|
17
146
|
/**
|
|
18
147
|
* Initial data received by the worker from the main thread used for initialization.
|
|
19
148
|
*/
|
|
20
149
|
data: TData;
|
|
150
|
+
/**
|
|
151
|
+
* Send a request to the host and wait for a response.
|
|
152
|
+
* Also supports progress streaming via `send.stream()`.
|
|
153
|
+
*/
|
|
154
|
+
send: WorkerSend<WRequest, WResponse>;
|
|
21
155
|
}
|
|
22
156
|
/**
|
|
23
157
|
* Object that represents messages the main thread
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
|
@@ -1 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Serialize an Error for transmission via postMessage.
|
|
3
|
+
* Recursively serializes error.cause if present.
|
|
4
|
+
*/
|
|
5
|
+
export function serializeError(error) {
|
|
6
|
+
const serialized = {
|
|
7
|
+
name: error.name,
|
|
8
|
+
message: error.message,
|
|
9
|
+
stack: error.stack,
|
|
10
|
+
};
|
|
11
|
+
// Recursively serialize cause if it's an Error
|
|
12
|
+
if (error.cause instanceof Error) {
|
|
13
|
+
serialized.cause = serializeError(error.cause);
|
|
14
|
+
}
|
|
15
|
+
return serialized;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create an Error from a serialized error, with original data in `cause`.
|
|
19
|
+
*
|
|
20
|
+
* @param context - Description of where the error occurred (e.g., "Host handler failed")
|
|
21
|
+
* @param serialized - The serialized error data
|
|
22
|
+
*/
|
|
23
|
+
export function errorFromSerialized(context, serialized) {
|
|
24
|
+
return new Error(`${context}: ${serialized.message}`, {
|
|
25
|
+
cause: serialized,
|
|
26
|
+
});
|
|
27
|
+
}
|
package/dist/worker-main.d.ts
CHANGED
|
@@ -44,14 +44,28 @@ import type { WorkerMainOptions } from "./types.ts";
|
|
|
44
44
|
* );
|
|
45
45
|
* ```
|
|
46
46
|
*
|
|
47
|
+
* @example Sending requests to the host
|
|
48
|
+
* ```ts
|
|
49
|
+
* import { workerMain } from "../worker.ts";
|
|
50
|
+
*
|
|
51
|
+
* await workerMain<never, never, string, void, string, string>(
|
|
52
|
+
* function* ({ send }) {
|
|
53
|
+
* const response = yield* send("hello");
|
|
54
|
+
* return `received: ${response}`;
|
|
55
|
+
* },
|
|
56
|
+
* );
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
47
59
|
* @template TSend - value main thread will send to the worker
|
|
48
60
|
* @template TRecv - value main thread will receive from the worker
|
|
49
61
|
* @template TReturn - worker operation return value
|
|
50
62
|
* @template TData - data passed from the main thread to the worker during initialization
|
|
51
|
-
* @
|
|
63
|
+
* @template WRequest - value worker sends to the host in requests
|
|
64
|
+
* @template WResponse - value worker receives from the host (response to worker's send)
|
|
65
|
+
* @param {(options: WorkerMainOptions<TSend, TRecv, TData, WRequest, WResponse>) => Operation<TReturn>} body
|
|
52
66
|
* @returns {Promise<void>}
|
|
53
67
|
*/
|
|
54
|
-
export declare function workerMain<TSend, TRecv, TReturn, TData>(body: (options: WorkerMainOptions<TSend, TRecv, TData>) => Operation<TReturn>): Promise<void>;
|
|
68
|
+
export declare function workerMain<TSend, TRecv, TReturn, TData, WRequest = never, WResponse = never>(body: (options: WorkerMainOptions<TSend, TRecv, TData, WRequest, WResponse>) => Operation<TReturn>): Promise<void>;
|
|
55
69
|
type New = {
|
|
56
70
|
type: "new";
|
|
57
71
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker-main.d.ts","sourceRoot":"","sources":["../worker-main.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,IAAI,
|
|
1
|
+
{"version":3,"file":"worker-main.d.ts","sourceRoot":"","sources":["../worker-main.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,IAAI,EAQV,MAAM,WAAW,CAAC;AAInB,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAkBpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EACL,KAAK,EACL,OAAO,EACP,KAAK,EACL,QAAQ,GAAG,KAAK,EAChB,SAAS,GAAG,KAAK,EAEjB,IAAI,EAAE,CACJ,OAAO,EAAE,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC,KACjE,SAAS,CAAC,OAAO,CAAC,GACtB,OAAO,CAAC,IAAI,CAAC,CA8Jf;AAED,KAAK,GAAG,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAC3B,KAAK,OAAO,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AACrD,KAAK,QAAQ,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AACrD,KAAK,OAAO,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAC/C,KAAK,WAAW,GAAG;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAEzD,KAAK,WAAW,GAAG,GAAG,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,WAAW,CAAC;AAEpE,UAAU,iBAAkB,SAAQ,WAAW,CAAC,WAAW,CAAC;IAC1D,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC5C,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC;IACnC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC;IAC7B,SAAS,IAAI,WAAW,CAAC;CAC1B;AAED,wBAAgB,wBAAwB,IAAI,SAAS,CAAC,iBAAiB,CAAC,CAkEvE"}
|
package/dist/worker-main.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { MessagePort, parentPort } from "node:worker_threads";
|
|
3
|
-
import { Err, Ok, createSignal, each, main, on, resource, spawn, } from "effection";
|
|
3
|
+
import { Err, Ok, createChannel, createSignal, each, main, on, resource, spawn, } from "effection";
|
|
4
|
+
import { useChannelRequest, useChannelResponse } from "./channel.js";
|
|
5
|
+
import { errorFromSerialized } from "./types.js";
|
|
4
6
|
// Get the appropriate worker port for the current environment as a resource
|
|
5
7
|
function useWorkerPort() {
|
|
6
8
|
return resource(function* (provide) {
|
|
@@ -58,17 +60,31 @@ function useWorkerPort() {
|
|
|
58
60
|
* );
|
|
59
61
|
* ```
|
|
60
62
|
*
|
|
63
|
+
* @example Sending requests to the host
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { workerMain } from "../worker.ts";
|
|
66
|
+
*
|
|
67
|
+
* await workerMain<never, never, string, void, string, string>(
|
|
68
|
+
* function* ({ send }) {
|
|
69
|
+
* const response = yield* send("hello");
|
|
70
|
+
* return `received: ${response}`;
|
|
71
|
+
* },
|
|
72
|
+
* );
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
61
75
|
* @template TSend - value main thread will send to the worker
|
|
62
76
|
* @template TRecv - value main thread will receive from the worker
|
|
63
77
|
* @template TReturn - worker operation return value
|
|
64
78
|
* @template TData - data passed from the main thread to the worker during initialization
|
|
65
|
-
* @
|
|
79
|
+
* @template WRequest - value worker sends to the host in requests
|
|
80
|
+
* @template WResponse - value worker receives from the host (response to worker's send)
|
|
81
|
+
* @param {(options: WorkerMainOptions<TSend, TRecv, TData, WRequest, WResponse>) => Operation<TReturn>} body
|
|
66
82
|
* @returns {Promise<void>}
|
|
67
83
|
*/
|
|
68
84
|
export async function workerMain(body) {
|
|
69
85
|
await main(function* () {
|
|
70
86
|
const port = yield* useWorkerPort();
|
|
71
|
-
let sent =
|
|
87
|
+
let sent = createChannel();
|
|
72
88
|
let worker = yield* createWorkerStatesSignal();
|
|
73
89
|
yield* spawn(function* () {
|
|
74
90
|
for (const message of yield* each(on(port, "message"))) {
|
|
@@ -78,24 +94,80 @@ export async function workerMain(body) {
|
|
|
78
94
|
case "init": {
|
|
79
95
|
worker.start(yield* spawn(function* () {
|
|
80
96
|
try {
|
|
97
|
+
// Helper to unwrap SerializedResult
|
|
98
|
+
function unwrapResult(result) {
|
|
99
|
+
if (result.ok) {
|
|
100
|
+
return result.value;
|
|
101
|
+
}
|
|
102
|
+
throw errorFromSerialized("Host handler failed", result.error);
|
|
103
|
+
}
|
|
104
|
+
// Create send function for worker-initiated requests
|
|
105
|
+
function send(requestValue) {
|
|
106
|
+
return {
|
|
107
|
+
*[Symbol.iterator]() {
|
|
108
|
+
const response = yield* useChannelResponse();
|
|
109
|
+
port.postMessage({
|
|
110
|
+
type: "request",
|
|
111
|
+
value: requestValue,
|
|
112
|
+
response: response.port,
|
|
113
|
+
},
|
|
114
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-env MessagePort compatibility
|
|
115
|
+
[response.port]);
|
|
116
|
+
const result = yield* response;
|
|
117
|
+
return unwrapResult(result);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Add stream method for progress streaming
|
|
122
|
+
send.stream = (requestValue) => ({
|
|
123
|
+
*[Symbol.iterator]() {
|
|
124
|
+
const response = yield* useChannelResponse();
|
|
125
|
+
port.postMessage({
|
|
126
|
+
type: "request",
|
|
127
|
+
value: requestValue,
|
|
128
|
+
response: response.port,
|
|
129
|
+
},
|
|
130
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-env MessagePort compatibility
|
|
131
|
+
[response.port]);
|
|
132
|
+
// Get the progress subscription
|
|
133
|
+
const progressSubscription = yield* response.progress;
|
|
134
|
+
// Wrap it to unwrap the SerializedResult at the end
|
|
135
|
+
const wrappedSubscription = {
|
|
136
|
+
*next() {
|
|
137
|
+
const result = yield* progressSubscription.next();
|
|
138
|
+
if (result.done) {
|
|
139
|
+
// Unwrap the SerializedResult
|
|
140
|
+
return {
|
|
141
|
+
done: true,
|
|
142
|
+
value: unwrapResult(result.value),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
return wrappedSubscription;
|
|
149
|
+
},
|
|
150
|
+
});
|
|
81
151
|
let value = yield* body({
|
|
82
152
|
data: control.data,
|
|
83
153
|
messages: {
|
|
84
154
|
*forEach(fn) {
|
|
85
155
|
for (let { value, response } of yield* each(sent)) {
|
|
86
156
|
yield* spawn(function* () {
|
|
157
|
+
const { resolve, reject } = yield* useChannelRequest(response);
|
|
87
158
|
try {
|
|
88
159
|
let result = yield* fn(value);
|
|
89
|
-
|
|
160
|
+
yield* resolve(result);
|
|
90
161
|
}
|
|
91
162
|
catch (error) {
|
|
92
|
-
|
|
163
|
+
yield* reject(error);
|
|
93
164
|
}
|
|
94
165
|
});
|
|
95
166
|
yield* each.next();
|
|
96
167
|
}
|
|
97
168
|
},
|
|
98
169
|
},
|
|
170
|
+
send,
|
|
99
171
|
});
|
|
100
172
|
worker.complete(value);
|
|
101
173
|
}
|
|
@@ -109,7 +181,7 @@ export async function workerMain(body) {
|
|
|
109
181
|
let { value, response } = control;
|
|
110
182
|
// Ensure that response is a proper MessagePort (DOM)
|
|
111
183
|
assert(response instanceof MessagePort, "Expect response to be an instance of MessagePort");
|
|
112
|
-
sent.send({ value, response });
|
|
184
|
+
yield* sent.send({ value, response });
|
|
113
185
|
break;
|
|
114
186
|
}
|
|
115
187
|
case "close": {
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,13 +1,45 @@
|
|
|
1
1
|
import { type Operation } from "effection";
|
|
2
|
+
import { type ForEachContext } from "./types.ts";
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
+
* Resource returned by useWorker, providing APIs for worker communication.
|
|
4
5
|
*
|
|
5
6
|
* @template TSend - value main thread will send to the worker
|
|
6
7
|
* @template TRecv - value main thread will receive from the worker
|
|
7
|
-
* @template
|
|
8
|
+
* @template TReturn - worker operation return value
|
|
8
9
|
*/
|
|
9
10
|
export interface WorkerResource<TSend, TRecv, TReturn> extends Operation<TReturn> {
|
|
11
|
+
/**
|
|
12
|
+
* Send a message to the worker and wait for a response.
|
|
13
|
+
*/
|
|
10
14
|
send(data: TSend): Operation<TRecv>;
|
|
15
|
+
/**
|
|
16
|
+
* Handle requests initiated by the worker.
|
|
17
|
+
* Only one forEach can be active at a time.
|
|
18
|
+
*
|
|
19
|
+
* The handler receives a context object with a `progress` method for
|
|
20
|
+
* sending progress updates back to the worker.
|
|
21
|
+
*
|
|
22
|
+
* @template WRequest - value worker sends to host
|
|
23
|
+
* @template WResponse - value host sends back to worker
|
|
24
|
+
* @template WProgress - progress type sent back to worker (optional)
|
|
25
|
+
*
|
|
26
|
+
* @example Basic usage (no progress)
|
|
27
|
+
* ```ts
|
|
28
|
+
* yield* worker.forEach(function* (request) {
|
|
29
|
+
* return computeResponse(request);
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example With progress streaming
|
|
34
|
+
* ```ts
|
|
35
|
+
* yield* worker.forEach(function* (request, ctx) {
|
|
36
|
+
* yield* ctx.progress({ step: 1, message: "Starting..." });
|
|
37
|
+
* yield* ctx.progress({ step: 2, message: "Processing..." });
|
|
38
|
+
* return { result: "done" };
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
forEach<WRequest, WResponse, WProgress = never>(fn: (request: WRequest, ctx: ForEachContext<WProgress>) => Operation<WResponse>): Operation<TReturn>;
|
|
11
43
|
}
|
|
12
44
|
/**
|
|
13
45
|
* Use on the main thread to create and exeecute a well behaved web worker.
|
package/dist/worker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../worker.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,SAAS,EAQf,MAAM,WAAW,CAAC;AAInB,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,YAAY,CAAC;AAGpB;;;;;;GAMG;AACH,MAAM,WAAW,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CACnD,SAAQ,SAAS,CAAC,OAAO,CAAC;IAC1B;;OAEG;IACH,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IACpC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,OAAO,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,GAAG,KAAK,EAC5C,EAAE,EAAE,CACF,OAAO,EAAE,QAAQ,EACjB,GAAG,EAAE,cAAc,CAAC,SAAS,CAAC,KAC3B,SAAS,CAAC,SAAS,CAAC,GACxB,SAAS,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EACpD,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,CAAC,EAAE,aAAa,GAAG;IAAE,IAAI,CAAC,EAAE,KAAK,CAAA;CAAE,GACzC,SAAS,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAgNlD"}
|
package/dist/worker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Err, Ok, on, once, resource, spawn, withResolvers, } from "effection";
|
|
1
|
+
import { Err, Ok, createChannel, on, once, resource, spawn, withResolvers, } from "effection";
|
|
3
2
|
import Worker from "web-worker";
|
|
4
|
-
import {
|
|
3
|
+
import { useChannelRequest, useChannelResponse } from "./channel.js";
|
|
4
|
+
import { errorFromSerialized, } from "./types.js";
|
|
5
5
|
/**
|
|
6
6
|
* Use on the main thread to create and exeecute a well behaved web worker.
|
|
7
7
|
*
|
|
@@ -53,22 +53,75 @@ import { useMessageChannel } from "./message-channel.js";
|
|
|
53
53
|
export function useWorker(url, options) {
|
|
54
54
|
return resource(function* (provide) {
|
|
55
55
|
let outcome = withResolvers();
|
|
56
|
+
let outcomeSettled = false;
|
|
57
|
+
const resolveOutcome = (value) => {
|
|
58
|
+
if (outcomeSettled) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
outcomeSettled = true;
|
|
62
|
+
outcome.resolve(value);
|
|
63
|
+
};
|
|
64
|
+
const rejectOutcome = (error) => {
|
|
65
|
+
if (outcomeSettled) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
outcomeSettled = true;
|
|
69
|
+
outcome.reject(error);
|
|
70
|
+
};
|
|
56
71
|
let worker = new Worker(url, options);
|
|
57
72
|
let subscription = yield* on(worker, "message");
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
// Channel for worker-initiated requests (buffered via eager subscription)
|
|
74
|
+
const requests = createChannel();
|
|
75
|
+
// Subscribe immediately so messages buffer before forEach is called
|
|
76
|
+
const requestSubscription = yield* requests;
|
|
77
|
+
// Flags for forEach state
|
|
78
|
+
let forEachInProgress = false;
|
|
79
|
+
let forEachCompleted = false;
|
|
80
|
+
let opened = false;
|
|
81
|
+
// Signal for when worker is ready (received "open" message)
|
|
82
|
+
const ready = withResolvers();
|
|
83
|
+
// Spawned message loop - handles incoming messages using each pattern
|
|
84
|
+
yield* spawn(function* () {
|
|
85
|
+
while (true) {
|
|
86
|
+
const next = yield* subscription.next();
|
|
87
|
+
if (next.done) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
const msg = next.value.data;
|
|
91
|
+
if (!opened && msg.type !== "open") {
|
|
92
|
+
const error = new Error(`expected first message to arrive from worker to be of type "open", but was: ${msg.type}`);
|
|
93
|
+
ready.reject(error);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
if (msg.type === "open") {
|
|
97
|
+
opened = true;
|
|
98
|
+
ready.resolve();
|
|
99
|
+
}
|
|
100
|
+
else if (msg.type === "close") {
|
|
101
|
+
const { result } = msg;
|
|
102
|
+
if (result.ok) {
|
|
103
|
+
resolveOutcome(result.value);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const serializedError = result.error;
|
|
107
|
+
rejectOutcome(errorFromSerialized("Worker failed", serializedError));
|
|
108
|
+
}
|
|
109
|
+
// Close channel so forEach terminates naturally
|
|
110
|
+
yield* requests.close(undefined);
|
|
63
111
|
}
|
|
64
|
-
else {
|
|
65
|
-
|
|
112
|
+
else if (msg.type === "request") {
|
|
113
|
+
yield* requests.send({ value: msg.value, response: msg.response });
|
|
66
114
|
}
|
|
67
115
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
116
|
+
if (!opened) {
|
|
117
|
+
const error = new Error("worker terminated before sending open message");
|
|
118
|
+
ready.reject(error);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
// Wait for "open" message before proceeding
|
|
123
|
+
yield* ready.operation;
|
|
124
|
+
// Handle worker errors
|
|
72
125
|
yield* spawn(function* () {
|
|
73
126
|
let event = yield* once(worker, "error");
|
|
74
127
|
event.preventDefault();
|
|
@@ -81,27 +134,90 @@ export function useWorker(url, options) {
|
|
|
81
134
|
});
|
|
82
135
|
yield* provide({
|
|
83
136
|
*send(value) {
|
|
84
|
-
|
|
137
|
+
const response = yield* useChannelResponse();
|
|
85
138
|
worker.postMessage({
|
|
86
139
|
type: "send",
|
|
87
140
|
value,
|
|
88
|
-
response:
|
|
89
|
-
}, [
|
|
90
|
-
|
|
91
|
-
let event = yield* once(channel.port1, "message");
|
|
92
|
-
let result = event.data;
|
|
141
|
+
response: response.port,
|
|
142
|
+
}, [response.port]);
|
|
143
|
+
const result = yield* response;
|
|
93
144
|
if (result.ok) {
|
|
94
145
|
return result.value;
|
|
95
146
|
}
|
|
96
|
-
throw result.error;
|
|
147
|
+
throw errorFromSerialized("Worker handler failed", result.error);
|
|
148
|
+
},
|
|
149
|
+
*forEach(fn) {
|
|
150
|
+
// Prevent calling forEach more than once
|
|
151
|
+
if (forEachCompleted) {
|
|
152
|
+
throw new Error("forEach has already completed");
|
|
153
|
+
}
|
|
154
|
+
// Prevent concurrent forEach
|
|
155
|
+
if (forEachInProgress) {
|
|
156
|
+
throw new Error("forEach is already in progress");
|
|
157
|
+
}
|
|
158
|
+
forEachInProgress = true;
|
|
159
|
+
try {
|
|
160
|
+
// Iterate until channel closes (when worker sends "close")
|
|
161
|
+
let next = yield* requestSubscription.next();
|
|
162
|
+
while (!next.done) {
|
|
163
|
+
const request = next.value;
|
|
164
|
+
// Track handler errors - we forward to worker but also re-throw to host
|
|
165
|
+
let handlerError;
|
|
166
|
+
// Create a task for this request and wait for it to complete
|
|
167
|
+
const task = yield* spawn(function* () {
|
|
168
|
+
const channelRequest = yield* useChannelRequest(request.response);
|
|
169
|
+
try {
|
|
170
|
+
// Create context with progress method
|
|
171
|
+
const ctx = {
|
|
172
|
+
progress: (data) => channelRequest.progress(data),
|
|
173
|
+
};
|
|
174
|
+
const result = yield* fn(request.value, ctx);
|
|
175
|
+
yield* channelRequest.resolve(result);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
// Forward error to worker so it knows the request failed
|
|
179
|
+
yield* channelRequest.reject(error);
|
|
180
|
+
// Store error to re-throw after forwarding (don't swallow host errors)
|
|
181
|
+
handlerError = error;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// Wait for the handler to complete
|
|
185
|
+
yield* task;
|
|
186
|
+
// If the handler failed, stop processing and re-throw
|
|
187
|
+
if (handlerError) {
|
|
188
|
+
throw handlerError;
|
|
189
|
+
}
|
|
190
|
+
next = yield* requestSubscription.next();
|
|
191
|
+
}
|
|
192
|
+
return yield* outcome.operation;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
forEachInProgress = false;
|
|
196
|
+
forEachCompleted = true;
|
|
197
|
+
}
|
|
97
198
|
},
|
|
98
199
|
[Symbol.iterator]: outcome.operation[Symbol.iterator],
|
|
99
200
|
});
|
|
100
201
|
}
|
|
101
202
|
finally {
|
|
102
203
|
worker.postMessage({ type: "close" });
|
|
204
|
+
if (!outcomeSettled) {
|
|
205
|
+
while (!outcomeSettled) {
|
|
206
|
+
const event = yield* once(worker, "message");
|
|
207
|
+
const msg = event.data;
|
|
208
|
+
if (msg.type === "close") {
|
|
209
|
+
const { result } = msg;
|
|
210
|
+
if (result.ok) {
|
|
211
|
+
resolveOutcome(result.value);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const serializedError = result.error;
|
|
215
|
+
rejectOutcome(errorFromSerialized("Worker failed", serializedError));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
103
220
|
yield* settled(outcome.operation);
|
|
104
|
-
worker.removeEventListener("message", onclose);
|
|
105
221
|
}
|
|
106
222
|
});
|
|
107
223
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectionx/worker",
|
|
3
3
|
"description": "Web Worker integration with two-way messaging and graceful shutdown",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/mod.js",
|
|
7
7
|
"types": "./dist/mod.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
+
"types": "./dist/mod.d.ts",
|
|
10
11
|
"development": "./mod.ts",
|
|
12
|
+
"import": "./dist/mod.js",
|
|
11
13
|
"default": "./dist/mod.js"
|
|
12
14
|
}
|
|
13
15
|
},
|
|
@@ -29,10 +31,12 @@
|
|
|
29
31
|
"sideEffects": false,
|
|
30
32
|
"dependencies": {
|
|
31
33
|
"web-worker": "^1",
|
|
32
|
-
"@effectionx/signals": "0.5.
|
|
34
|
+
"@effectionx/signals": "0.5.2",
|
|
35
|
+
"@effectionx/timebox": "0.4.2"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
35
|
-
"
|
|
36
|
-
"@effectionx/
|
|
38
|
+
"effection": "^4",
|
|
39
|
+
"@effectionx/bdd": "0.4.3",
|
|
40
|
+
"@effectionx/converge": "0.1.3"
|
|
37
41
|
}
|
|
38
42
|
}
|