@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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { spawn } from "effection";
|
|
2
|
+
import { workerMain } from "../worker-main.ts";
|
|
3
|
+
|
|
4
|
+
await workerMain<string, string, string, void, string, string>(function* ({
|
|
5
|
+
messages,
|
|
6
|
+
send,
|
|
7
|
+
}) {
|
|
8
|
+
// Spawn messages handler in background (it runs until worker closes)
|
|
9
|
+
yield* spawn(function* () {
|
|
10
|
+
yield* messages.forEach(function* (msg) {
|
|
11
|
+
return `worker-response: ${msg}`;
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Send request to host and get response
|
|
16
|
+
const fromHost = yield* send("from-worker");
|
|
17
|
+
|
|
18
|
+
return `done: ${fromHost}`;
|
|
19
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { workerMain } from "../worker-main.ts";
|
|
2
|
+
|
|
3
|
+
await workerMain<never, never, string, void, string, string>(function* ({
|
|
4
|
+
send,
|
|
5
|
+
}) {
|
|
6
|
+
try {
|
|
7
|
+
yield* send("trigger-error");
|
|
8
|
+
return "no error";
|
|
9
|
+
} catch (e) {
|
|
10
|
+
const error = e as Error & { cause?: unknown };
|
|
11
|
+
const cause = error.cause as
|
|
12
|
+
| { name: string; message: string; stack?: string }
|
|
13
|
+
| undefined;
|
|
14
|
+
if (cause?.name && cause.message) {
|
|
15
|
+
return `caught error with cause: ${cause.name} - ${cause.message}`;
|
|
16
|
+
}
|
|
17
|
+
return `caught error without proper cause: ${error.message}`;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { workerMain } from "../worker-main.ts";
|
|
2
|
+
|
|
3
|
+
await workerMain<never, never, string, void, string, string>(function* ({
|
|
4
|
+
send,
|
|
5
|
+
}) {
|
|
6
|
+
try {
|
|
7
|
+
yield* send("fail");
|
|
8
|
+
return "no error";
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return `caught: ${(e as Error).message}`;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { workerMain } from "../worker-main.ts";
|
|
2
|
+
|
|
3
|
+
// Worker that calls send() from inside messages.forEach handler
|
|
4
|
+
await workerMain<string, string, string, void, string, string>(function* ({
|
|
5
|
+
messages,
|
|
6
|
+
send,
|
|
7
|
+
}) {
|
|
8
|
+
let lastResponse = "";
|
|
9
|
+
|
|
10
|
+
yield* messages.forEach(function* (msg) {
|
|
11
|
+
// Call send() to host while handling a message from host
|
|
12
|
+
const hostResponse = yield* send(`worker-request-for: ${msg}`);
|
|
13
|
+
lastResponse = hostResponse;
|
|
14
|
+
return `processed: ${msg} with ${hostResponse}`;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return `final: ${lastResponse}`;
|
|
18
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { workerMain } from "../worker-main.ts";
|
|
2
|
+
|
|
3
|
+
await workerMain<never, never, number, void, string, number>(function* ({
|
|
4
|
+
send,
|
|
5
|
+
}) {
|
|
6
|
+
const a = yield* send("first");
|
|
7
|
+
const b = yield* send("second");
|
|
8
|
+
const c = yield* send("third");
|
|
9
|
+
return c;
|
|
10
|
+
});
|
package/tsconfig.json
CHANGED
package/types.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import type { Operation } from "effection";
|
|
1
|
+
import type { Operation, Result, Subscription } from "effection";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Messages sent from host to worker (control messages).
|
|
5
|
+
*/
|
|
3
6
|
export type WorkerControl<TSend, TData> =
|
|
4
7
|
| {
|
|
5
8
|
type: "init";
|
|
@@ -14,15 +17,166 @@ export type WorkerControl<TSend, TData> =
|
|
|
14
17
|
type: "close";
|
|
15
18
|
};
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Messages sent from worker to host.
|
|
22
|
+
*
|
|
23
|
+
* @template WRequest - value worker sends to host in requests
|
|
24
|
+
* @template TReturn - return value when worker completes
|
|
25
|
+
*/
|
|
26
|
+
export type WorkerToHost<WRequest, TReturn> =
|
|
27
|
+
| { type: "open" }
|
|
28
|
+
| { type: "request"; value: WRequest; response: MessagePort }
|
|
29
|
+
| { type: "close"; 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
|
+
/**
|
|
43
|
+
* A Result type for cross-boundary communication where errors are serialized.
|
|
44
|
+
* Unlike effection's Result<T> which uses Error, this uses SerializedError.
|
|
45
|
+
*
|
|
46
|
+
* Used by channel primitives to send success/error responses over MessageChannel.
|
|
47
|
+
*/
|
|
48
|
+
export type SerializedResult<T> =
|
|
49
|
+
| { ok: true; value: T }
|
|
50
|
+
| { ok: false; error: SerializedError };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Messages sent over a channel that supports progress streaming.
|
|
54
|
+
* Used by useChannelRequest to send progress updates and final response.
|
|
55
|
+
*/
|
|
56
|
+
export type ChannelMessage<TResponse, TProgress> =
|
|
57
|
+
| { type: "progress"; data: TProgress }
|
|
58
|
+
| { type: "response"; result: SerializedResult<TResponse> };
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Acknowledgement messages sent back over a channel.
|
|
62
|
+
* Used by useChannelResponse to acknowledge receipt of messages.
|
|
63
|
+
*/
|
|
64
|
+
export type ChannelAck = { type: "ack" } | { type: "progress_ack" };
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Context passed to forEach handler for progress streaming.
|
|
68
|
+
* Allows the handler to send progress updates back to the requester.
|
|
69
|
+
*
|
|
70
|
+
* @template TProgress - The progress data type
|
|
71
|
+
*/
|
|
72
|
+
export interface ForEachContext<TProgress> {
|
|
18
73
|
/**
|
|
19
|
-
*
|
|
74
|
+
* Send a progress update to the requester.
|
|
75
|
+
* This operation blocks until the requester acknowledges receipt (backpressure).
|
|
76
|
+
*
|
|
77
|
+
* @param data - The progress data to send
|
|
78
|
+
*/
|
|
79
|
+
progress(data: TProgress): Operation<void>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Serialize an Error for transmission via postMessage.
|
|
84
|
+
* Recursively serializes error.cause if present.
|
|
85
|
+
*/
|
|
86
|
+
export function serializeError(error: Error): SerializedError {
|
|
87
|
+
const serialized: SerializedError = {
|
|
88
|
+
name: error.name,
|
|
89
|
+
message: error.message,
|
|
90
|
+
stack: error.stack,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Recursively serialize cause if it's an Error
|
|
94
|
+
if (error.cause instanceof Error) {
|
|
95
|
+
serialized.cause = serializeError(error.cause);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return serialized;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create an Error from a serialized error, with original data in `cause`.
|
|
103
|
+
*
|
|
104
|
+
* @param context - Description of where the error occurred (e.g., "Host handler failed")
|
|
105
|
+
* @param serialized - The serialized error data
|
|
106
|
+
*/
|
|
107
|
+
export function errorFromSerialized(
|
|
108
|
+
context: string,
|
|
109
|
+
serialized: SerializedError,
|
|
110
|
+
): Error {
|
|
111
|
+
return new Error(`${context}: ${serialized.message}`, {
|
|
112
|
+
cause: serialized,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A send function that supports both simple request/response and progress streaming.
|
|
118
|
+
*
|
|
119
|
+
* @template WRequest - value worker sends to host
|
|
120
|
+
* @template WResponse - value worker receives from host
|
|
121
|
+
*/
|
|
122
|
+
export interface WorkerSend<WRequest, WResponse> {
|
|
123
|
+
/**
|
|
124
|
+
* Send a request to the host and wait for a response.
|
|
125
|
+
* Ignores any progress updates from the host.
|
|
126
|
+
*/
|
|
127
|
+
(value: WRequest): Operation<WResponse>;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Send a request to the host and receive a subscription that yields
|
|
131
|
+
* progress updates and returns the final response.
|
|
132
|
+
*
|
|
133
|
+
* @template WProgress - progress type from host
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* const subscription = yield* send.stream<number>(request);
|
|
138
|
+
* let next = yield* subscription.next();
|
|
139
|
+
* while (!next.done) {
|
|
140
|
+
* console.log("Progress:", next.value);
|
|
141
|
+
* next = yield* subscription.next();
|
|
142
|
+
* }
|
|
143
|
+
* const response = next.value;
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
stream<WProgress>(
|
|
147
|
+
value: WRequest,
|
|
148
|
+
): Operation<Subscription<WProgress, WResponse>>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Options passed to the worker's main function.
|
|
153
|
+
*
|
|
154
|
+
* @template TSend - value host sends to worker
|
|
155
|
+
* @template TRecv - value host receives from worker (response to host's send)
|
|
156
|
+
* @template TData - initial data passed to worker
|
|
157
|
+
* @template WRequest - value worker sends to host in requests
|
|
158
|
+
* @template WResponse - value worker receives from host (response to worker's send)
|
|
159
|
+
*/
|
|
160
|
+
export interface WorkerMainOptions<
|
|
161
|
+
TSend,
|
|
162
|
+
TRecv,
|
|
163
|
+
TData,
|
|
164
|
+
WRequest = never,
|
|
165
|
+
WResponse = never,
|
|
166
|
+
> {
|
|
167
|
+
/**
|
|
168
|
+
* Namespace that provides APIs for working with incoming messages from host.
|
|
20
169
|
*/
|
|
21
170
|
messages: WorkerMessages<TSend, TRecv>;
|
|
22
171
|
/**
|
|
23
172
|
* Initial data received by the worker from the main thread used for initialization.
|
|
24
173
|
*/
|
|
25
174
|
data: TData;
|
|
175
|
+
/**
|
|
176
|
+
* Send a request to the host and wait for a response.
|
|
177
|
+
* Also supports progress streaming via `send.stream()`.
|
|
178
|
+
*/
|
|
179
|
+
send: WorkerSend<WRequest, WResponse>;
|
|
26
180
|
}
|
|
27
181
|
|
|
28
182
|
/**
|
package/worker-main.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Ok,
|
|
7
7
|
type Operation,
|
|
8
8
|
type Task,
|
|
9
|
+
createChannel,
|
|
9
10
|
createSignal,
|
|
10
11
|
each,
|
|
11
12
|
main,
|
|
@@ -14,7 +15,14 @@ import {
|
|
|
14
15
|
spawn,
|
|
15
16
|
} from "effection";
|
|
16
17
|
|
|
17
|
-
import type {
|
|
18
|
+
import type { Subscription } from "effection";
|
|
19
|
+
import { useChannelRequest, useChannelResponse } from "./channel.ts";
|
|
20
|
+
import type {
|
|
21
|
+
SerializedResult,
|
|
22
|
+
WorkerControl,
|
|
23
|
+
WorkerMainOptions,
|
|
24
|
+
} from "./types.ts";
|
|
25
|
+
import { errorFromSerialized } from "./types.ts";
|
|
18
26
|
|
|
19
27
|
// Get the appropriate worker port for the current environment as a resource
|
|
20
28
|
function useWorkerPort(): Operation<MessagePort> {
|
|
@@ -74,19 +82,42 @@ function useWorkerPort(): Operation<MessagePort> {
|
|
|
74
82
|
* );
|
|
75
83
|
* ```
|
|
76
84
|
*
|
|
85
|
+
* @example Sending requests to the host
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { workerMain } from "../worker.ts";
|
|
88
|
+
*
|
|
89
|
+
* await workerMain<never, never, string, void, string, string>(
|
|
90
|
+
* function* ({ send }) {
|
|
91
|
+
* const response = yield* send("hello");
|
|
92
|
+
* return `received: ${response}`;
|
|
93
|
+
* },
|
|
94
|
+
* );
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
77
97
|
* @template TSend - value main thread will send to the worker
|
|
78
98
|
* @template TRecv - value main thread will receive from the worker
|
|
79
99
|
* @template TReturn - worker operation return value
|
|
80
100
|
* @template TData - data passed from the main thread to the worker during initialization
|
|
81
|
-
* @
|
|
101
|
+
* @template WRequest - value worker sends to the host in requests
|
|
102
|
+
* @template WResponse - value worker receives from the host (response to worker's send)
|
|
103
|
+
* @param {(options: WorkerMainOptions<TSend, TRecv, TData, WRequest, WResponse>) => Operation<TReturn>} body
|
|
82
104
|
* @returns {Promise<void>}
|
|
83
105
|
*/
|
|
84
|
-
export async function workerMain<
|
|
85
|
-
|
|
106
|
+
export async function workerMain<
|
|
107
|
+
TSend,
|
|
108
|
+
TRecv,
|
|
109
|
+
TReturn,
|
|
110
|
+
TData,
|
|
111
|
+
WRequest = never,
|
|
112
|
+
WResponse = never,
|
|
113
|
+
>(
|
|
114
|
+
body: (
|
|
115
|
+
options: WorkerMainOptions<TSend, TRecv, TData, WRequest, WResponse>,
|
|
116
|
+
) => Operation<TReturn>,
|
|
86
117
|
): Promise<void> {
|
|
87
118
|
await main(function* () {
|
|
88
119
|
const port = yield* useWorkerPort();
|
|
89
|
-
let sent =
|
|
120
|
+
let sent = createChannel<{ value: TSend; response: MessagePort }>();
|
|
90
121
|
let worker = yield* createWorkerStatesSignal();
|
|
91
122
|
|
|
92
123
|
yield* spawn(function* () {
|
|
@@ -98,23 +129,103 @@ export async function workerMain<TSend, TRecv, TReturn, TData>(
|
|
|
98
129
|
worker.start(
|
|
99
130
|
yield* spawn(function* () {
|
|
100
131
|
try {
|
|
132
|
+
// Helper to unwrap SerializedResult
|
|
133
|
+
function unwrapResult<T>(result: SerializedResult<T>): T {
|
|
134
|
+
if (result.ok) {
|
|
135
|
+
return result.value;
|
|
136
|
+
}
|
|
137
|
+
throw errorFromSerialized(
|
|
138
|
+
"Host handler failed",
|
|
139
|
+
result.error,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create send function for worker-initiated requests
|
|
144
|
+
function send(requestValue: WRequest): Operation<WResponse> {
|
|
145
|
+
return {
|
|
146
|
+
*[Symbol.iterator]() {
|
|
147
|
+
const response = yield* useChannelResponse<WResponse>();
|
|
148
|
+
port.postMessage(
|
|
149
|
+
{
|
|
150
|
+
type: "request",
|
|
151
|
+
value: requestValue,
|
|
152
|
+
response: response.port,
|
|
153
|
+
},
|
|
154
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-env MessagePort compatibility
|
|
155
|
+
[response.port] as any,
|
|
156
|
+
);
|
|
157
|
+
const result = yield* response;
|
|
158
|
+
return unwrapResult(result);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Add stream method for progress streaming
|
|
164
|
+
send.stream = <WProgress>(
|
|
165
|
+
requestValue: WRequest,
|
|
166
|
+
): Operation<Subscription<WProgress, WResponse>> => ({
|
|
167
|
+
*[Symbol.iterator]() {
|
|
168
|
+
const response = yield* useChannelResponse<
|
|
169
|
+
WResponse,
|
|
170
|
+
WProgress
|
|
171
|
+
>();
|
|
172
|
+
port.postMessage(
|
|
173
|
+
{
|
|
174
|
+
type: "request",
|
|
175
|
+
value: requestValue,
|
|
176
|
+
response: response.port,
|
|
177
|
+
},
|
|
178
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-env MessagePort compatibility
|
|
179
|
+
[response.port] as any,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Get the progress subscription
|
|
183
|
+
const progressSubscription = yield* response.progress;
|
|
184
|
+
|
|
185
|
+
// Wrap it to unwrap the SerializedResult at the end
|
|
186
|
+
const wrappedSubscription: Subscription<
|
|
187
|
+
WProgress,
|
|
188
|
+
WResponse
|
|
189
|
+
> = {
|
|
190
|
+
*next() {
|
|
191
|
+
const result = yield* progressSubscription.next();
|
|
192
|
+
if (result.done) {
|
|
193
|
+
// Unwrap the SerializedResult
|
|
194
|
+
return {
|
|
195
|
+
done: true as const,
|
|
196
|
+
value: unwrapResult(result.value),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return wrappedSubscription;
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
101
207
|
let value = yield* body({
|
|
102
208
|
data: control.data,
|
|
103
209
|
messages: {
|
|
104
210
|
*forEach(fn: (value: TSend) => Operation<TRecv>) {
|
|
105
211
|
for (let { value, response } of yield* each(sent)) {
|
|
106
212
|
yield* spawn(function* () {
|
|
213
|
+
const { resolve, reject } =
|
|
214
|
+
yield* useChannelRequest<TRecv>(
|
|
215
|
+
response as unknown as globalThis.MessagePort,
|
|
216
|
+
);
|
|
107
217
|
try {
|
|
108
218
|
let result = yield* fn(value);
|
|
109
|
-
|
|
219
|
+
yield* resolve(result);
|
|
110
220
|
} catch (error) {
|
|
111
|
-
|
|
221
|
+
yield* reject(error as Error);
|
|
112
222
|
}
|
|
113
223
|
});
|
|
114
224
|
yield* each.next();
|
|
115
225
|
}
|
|
116
226
|
},
|
|
117
227
|
},
|
|
228
|
+
send,
|
|
118
229
|
});
|
|
119
230
|
|
|
120
231
|
worker.complete(value);
|
|
@@ -132,7 +243,7 @@ export async function workerMain<TSend, TRecv, TReturn, TData>(
|
|
|
132
243
|
response instanceof MessagePort,
|
|
133
244
|
"Expect response to be an instance of MessagePort",
|
|
134
245
|
);
|
|
135
|
-
sent.send({ value, response });
|
|
246
|
+
yield* sent.send({ value, response });
|
|
136
247
|
break;
|
|
137
248
|
}
|
|
138
249
|
case "close": {
|