@effectionx/worker 0.4.2 → 0.5.0
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 +3 -1
- 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/worker.test.ts
CHANGED
|
@@ -3,7 +3,15 @@ import { join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { beforeEach, describe, it } from "@effectionx/bdd";
|
|
5
5
|
import { when } from "@effectionx/converge";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
all,
|
|
8
|
+
scoped,
|
|
9
|
+
sleep,
|
|
10
|
+
spawn,
|
|
11
|
+
suspend,
|
|
12
|
+
until,
|
|
13
|
+
withResolvers,
|
|
14
|
+
} from "effection";
|
|
7
15
|
import { expect } from "expect";
|
|
8
16
|
|
|
9
17
|
import type { ShutdownWorkerParams } from "./test-assets/shutdown-worker.ts";
|
|
@@ -31,7 +39,7 @@ describe("worker", () => {
|
|
|
31
39
|
yield* worker.send();
|
|
32
40
|
} catch (e) {
|
|
33
41
|
expect(e).toBeInstanceOf(Error);
|
|
34
|
-
expect(e).
|
|
42
|
+
expect((e as Error).message).toContain("boom!");
|
|
35
43
|
}
|
|
36
44
|
});
|
|
37
45
|
it("produces its return value", function* () {
|
|
@@ -55,7 +63,7 @@ describe("worker", () => {
|
|
|
55
63
|
yield* worker;
|
|
56
64
|
} catch (e) {
|
|
57
65
|
expect(e).toBeInstanceOf(Error);
|
|
58
|
-
expect(e).
|
|
66
|
+
expect((e as Error).message).toContain("boom!");
|
|
59
67
|
}
|
|
60
68
|
});
|
|
61
69
|
describe("shutdown", () => {
|
|
@@ -130,7 +138,7 @@ describe("worker", () => {
|
|
|
130
138
|
yield* worker;
|
|
131
139
|
} catch (e) {
|
|
132
140
|
expect(e).toBeInstanceOf(Error);
|
|
133
|
-
expect(e).
|
|
141
|
+
expect((e as Error).message).toContain("worker terminated");
|
|
134
142
|
}
|
|
135
143
|
});
|
|
136
144
|
|
|
@@ -176,4 +184,294 @@ describe("worker", () => {
|
|
|
176
184
|
it.skip("crashes if there is a message error from the worker thread", function* () {
|
|
177
185
|
// don't know how to trigger
|
|
178
186
|
});
|
|
187
|
+
|
|
188
|
+
describe("worker-initiated requests", () => {
|
|
189
|
+
it("handles a single request from worker", function* () {
|
|
190
|
+
const worker = yield* useWorker<never, never, string, void>(
|
|
191
|
+
import.meta.resolve("./test-assets/single-request-worker.ts"),
|
|
192
|
+
{ type: "module" },
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const result = yield* worker.forEach<string, string>(function* (request) {
|
|
196
|
+
return `echo: ${request}`;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(result).toEqual("received: echo: hello");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("handles multiple sequential requests from worker", function* () {
|
|
203
|
+
const worker = yield* useWorker<never, never, number, void>(
|
|
204
|
+
import.meta.resolve("./test-assets/sequential-requests-worker.ts"),
|
|
205
|
+
{ type: "module" },
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
let counter = 0;
|
|
209
|
+
const result = yield* worker.forEach<string, number>(
|
|
210
|
+
function* (_request) {
|
|
211
|
+
counter += 1;
|
|
212
|
+
return counter;
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(result).toEqual(3);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("propagates errors from host handler to worker and crashes host", function* () {
|
|
220
|
+
const worker = yield* useWorker<never, never, string, void>(
|
|
221
|
+
import.meta.resolve("./test-assets/error-handling-worker.ts"),
|
|
222
|
+
{ type: "module" },
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Host should crash after forwarding error to worker
|
|
226
|
+
let hostError: Error | undefined;
|
|
227
|
+
try {
|
|
228
|
+
yield* worker.forEach<string, string>(function* (request) {
|
|
229
|
+
if (request === "fail") {
|
|
230
|
+
throw new Error("host error");
|
|
231
|
+
}
|
|
232
|
+
return "ok";
|
|
233
|
+
});
|
|
234
|
+
} catch (e) {
|
|
235
|
+
hostError = e as Error;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Verify host crashed with the original error
|
|
239
|
+
expect(hostError).toBeDefined();
|
|
240
|
+
expect(hostError?.message).toEqual("host error");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("handles concurrent requests from worker", function* () {
|
|
244
|
+
const worker = yield* useWorker<never, never, number[], void>(
|
|
245
|
+
import.meta.resolve("./test-assets/concurrent-requests-worker.ts"),
|
|
246
|
+
{ type: "module" },
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const result = yield* worker.forEach<number, number>(function* (request) {
|
|
250
|
+
yield* sleep(request * 10);
|
|
251
|
+
return request * 2;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(result).toEqual([6, 4, 2]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("supports bidirectional communication", function* () {
|
|
258
|
+
const worker = yield* useWorker<string, string, string, void>(
|
|
259
|
+
import.meta.resolve("./test-assets/bidirectional-worker.ts"),
|
|
260
|
+
{ type: "module" },
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
yield* spawn(function* () {
|
|
264
|
+
yield* worker.forEach<string, string>(function* (request) {
|
|
265
|
+
return `host-response: ${request}`;
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const hostResult = yield* worker.send("from-host");
|
|
270
|
+
expect(hostResult).toEqual("worker-response: from-host");
|
|
271
|
+
|
|
272
|
+
const finalResult = yield* worker;
|
|
273
|
+
expect(finalResult).toEqual("done: host-response: from-worker");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("existing workers without send still work", function* () {
|
|
277
|
+
const worker = yield* useWorker(
|
|
278
|
+
import.meta.resolve("./test-assets/echo-worker.ts"),
|
|
279
|
+
{ type: "module" },
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const result = yield* worker.send("hello world");
|
|
283
|
+
expect(result).toEqual("hello world");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("forEach completes with result when worker sends no requests", function* () {
|
|
287
|
+
const worker = yield* useWorker<never, never, string, void>(
|
|
288
|
+
import.meta.resolve("./test-assets/no-requests-worker.ts"),
|
|
289
|
+
{ type: "module" },
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
let handlerCalled = false;
|
|
293
|
+
const result = yield* worker.forEach<string, string>(
|
|
294
|
+
function* (_request) {
|
|
295
|
+
handlerCalled = true;
|
|
296
|
+
return "response";
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(result).toEqual("done without requests");
|
|
301
|
+
expect(handlerCalled).toBe(false);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("yield worker after forEach returns same result", function* () {
|
|
305
|
+
const worker = yield* useWorker<never, never, string, void>(
|
|
306
|
+
import.meta.resolve("./test-assets/single-request-worker.ts"),
|
|
307
|
+
{ type: "module" },
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const result1 = yield* worker.forEach<string, string>(
|
|
311
|
+
function* (request) {
|
|
312
|
+
return `echo: ${request}`;
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const result2 = yield* worker;
|
|
317
|
+
|
|
318
|
+
expect(result1).toEqual("received: echo: hello");
|
|
319
|
+
expect(result2).toEqual("received: echo: hello");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("yield forEach after worker returns cached result", function* () {
|
|
323
|
+
const worker = yield* useWorker<never, never, string, void>(
|
|
324
|
+
import.meta.resolve("./test-assets/no-requests-worker.ts"),
|
|
325
|
+
{ type: "module" },
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const result1 = yield* worker;
|
|
329
|
+
|
|
330
|
+
let handlerCalled = false;
|
|
331
|
+
const result2 = yield* worker.forEach<string, string>(
|
|
332
|
+
function* (_request) {
|
|
333
|
+
handlerCalled = true;
|
|
334
|
+
return "response";
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expect(result1).toEqual("done without requests");
|
|
339
|
+
expect(result2).toEqual("done without requests");
|
|
340
|
+
expect(handlerCalled).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("yield worker multiple times returns same result", function* () {
|
|
344
|
+
const worker = yield* useWorker<never, never, string, void>(
|
|
345
|
+
import.meta.resolve("./test-assets/no-requests-worker.ts"),
|
|
346
|
+
{ type: "module" },
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const result1 = yield* worker;
|
|
350
|
+
const result2 = yield* worker;
|
|
351
|
+
const result3 = yield* worker;
|
|
352
|
+
|
|
353
|
+
expect(result1).toEqual("done without requests");
|
|
354
|
+
expect(result2).toEqual("done without requests");
|
|
355
|
+
expect(result3).toEqual("done without requests");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("queues requests sent before forEach is called", function* () {
|
|
359
|
+
const worker = yield* useWorker<never, never, string, void>(
|
|
360
|
+
import.meta.resolve("./test-assets/single-request-worker.ts"),
|
|
361
|
+
{ type: "module" },
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Yield control to allow worker to send request before forEach is set up
|
|
365
|
+
// The channel implementation buffers requests, so sleep(0) is sufficient
|
|
366
|
+
yield* sleep(0);
|
|
367
|
+
|
|
368
|
+
const result = yield* worker.forEach<string, string>(function* (request) {
|
|
369
|
+
return `echo: ${request}`;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
expect(result).toEqual("received: echo: hello");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("throws error when forEach is called concurrently", function* () {
|
|
376
|
+
expect.assertions(1);
|
|
377
|
+
const worker = yield* useWorker<never, never, string, void>(
|
|
378
|
+
import.meta.resolve("./test-assets/slow-request-worker.ts"),
|
|
379
|
+
{ type: "module" },
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const forEachStarted = withResolvers<void>();
|
|
383
|
+
const allowHandlerToComplete = withResolvers<void>();
|
|
384
|
+
|
|
385
|
+
// Start first forEach in background
|
|
386
|
+
yield* spawn(function* () {
|
|
387
|
+
yield* worker.forEach<string, string>(function* (_request) {
|
|
388
|
+
forEachStarted.resolve();
|
|
389
|
+
// Block until test signals completion (deterministic latch instead of sleep)
|
|
390
|
+
yield* allowHandlerToComplete.operation;
|
|
391
|
+
return "slow response";
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Wait for first forEach to start handling a request
|
|
396
|
+
yield* forEachStarted.operation;
|
|
397
|
+
|
|
398
|
+
// Second forEach should throw
|
|
399
|
+
try {
|
|
400
|
+
yield* worker.forEach<string, string>(function* (_request) {
|
|
401
|
+
return "should not be called";
|
|
402
|
+
});
|
|
403
|
+
} catch (e) {
|
|
404
|
+
expect((e as Error).message).toEqual("forEach is already in progress");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Allow first handler to complete so test can clean up
|
|
408
|
+
allowHandlerToComplete.resolve();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("error cause contains name, message, and stack from host", function* () {
|
|
412
|
+
const worker = yield* useWorker<never, never, string, void>(
|
|
413
|
+
import.meta.resolve("./test-assets/error-cause-worker.ts"),
|
|
414
|
+
{ type: "module" },
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// Host should crash after forwarding error to worker
|
|
418
|
+
let hostError: Error | undefined;
|
|
419
|
+
try {
|
|
420
|
+
yield* worker.forEach<string, string>(function* (_request) {
|
|
421
|
+
const error = new TypeError("custom type error");
|
|
422
|
+
throw error;
|
|
423
|
+
});
|
|
424
|
+
} catch (e) {
|
|
425
|
+
hostError = e as Error;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Verify host crashed with the original error
|
|
429
|
+
expect(hostError).toBeDefined();
|
|
430
|
+
expect(hostError?.name).toEqual("TypeError");
|
|
431
|
+
expect(hostError?.message).toEqual("custom type error");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("error cause contains name, message, and stack from worker", function* () {
|
|
435
|
+
expect.assertions(4);
|
|
436
|
+
const worker = yield* useWorker<string, string, void, void>(
|
|
437
|
+
import.meta.resolve("./test-assets/error-throw-worker.ts"),
|
|
438
|
+
{ type: "module" },
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
yield* worker.send("trigger-error");
|
|
443
|
+
} catch (e) {
|
|
444
|
+
const error = e as Error & { cause?: unknown };
|
|
445
|
+
expect(error.message).toContain("Worker handler failed");
|
|
446
|
+
expect(error.cause).toBeDefined();
|
|
447
|
+
const cause = error.cause as {
|
|
448
|
+
name: string;
|
|
449
|
+
message: string;
|
|
450
|
+
stack?: string;
|
|
451
|
+
};
|
|
452
|
+
expect(cause.name).toEqual("RangeError");
|
|
453
|
+
expect(cause.message).toEqual("worker range error");
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("worker can call send inside messages.forEach handler", function* () {
|
|
458
|
+
const worker = yield* useWorker<string, string, string, void>(
|
|
459
|
+
import.meta.resolve("./test-assets/send-inside-foreach-worker.ts"),
|
|
460
|
+
{ type: "module" },
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// Handle worker-initiated requests
|
|
464
|
+
yield* spawn(function* () {
|
|
465
|
+
yield* worker.forEach<string, string>(function* (request) {
|
|
466
|
+
return `host-handled: ${request}`;
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Send message to worker, which triggers it to call send() back to host
|
|
471
|
+
const result = yield* worker.send("trigger");
|
|
472
|
+
expect(result).toEqual(
|
|
473
|
+
"processed: trigger with host-handled: worker-request-for: trigger",
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
179
477
|
});
|
package/worker.ts
CHANGED
|
@@ -1,29 +1,71 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
1
|
import {
|
|
3
2
|
Err,
|
|
4
3
|
Ok,
|
|
4
|
+
type Operation,
|
|
5
|
+
type Result,
|
|
6
|
+
createChannel,
|
|
5
7
|
on,
|
|
6
8
|
once,
|
|
7
|
-
type Operation,
|
|
8
9
|
resource,
|
|
9
|
-
type Result,
|
|
10
10
|
spawn,
|
|
11
11
|
withResolvers,
|
|
12
12
|
} from "effection";
|
|
13
13
|
import Worker from "web-worker";
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { useChannelRequest, useChannelResponse } from "./channel.ts";
|
|
16
|
+
import {
|
|
17
|
+
type ForEachContext,
|
|
18
|
+
type SerializedError,
|
|
19
|
+
errorFromSerialized,
|
|
20
|
+
} from "./types.ts";
|
|
21
|
+
// Note: Ok/Err still used for outcome handling; serializeError no longer needed here
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
|
-
*
|
|
24
|
+
* Resource returned by useWorker, providing APIs for worker communication.
|
|
19
25
|
*
|
|
20
26
|
* @template TSend - value main thread will send to the worker
|
|
21
27
|
* @template TRecv - value main thread will receive from the worker
|
|
22
|
-
* @template
|
|
28
|
+
* @template TReturn - worker operation return value
|
|
23
29
|
*/
|
|
24
30
|
export interface WorkerResource<TSend, TRecv, TReturn>
|
|
25
31
|
extends Operation<TReturn> {
|
|
32
|
+
/**
|
|
33
|
+
* Send a message to the worker and wait for a response.
|
|
34
|
+
*/
|
|
26
35
|
send(data: TSend): Operation<TRecv>;
|
|
36
|
+
/**
|
|
37
|
+
* Handle requests initiated by the worker.
|
|
38
|
+
* Only one forEach can be active at a time.
|
|
39
|
+
*
|
|
40
|
+
* The handler receives a context object with a `progress` method for
|
|
41
|
+
* sending progress updates back to the worker.
|
|
42
|
+
*
|
|
43
|
+
* @template WRequest - value worker sends to host
|
|
44
|
+
* @template WResponse - value host sends back to worker
|
|
45
|
+
* @template WProgress - progress type sent back to worker (optional)
|
|
46
|
+
*
|
|
47
|
+
* @example Basic usage (no progress)
|
|
48
|
+
* ```ts
|
|
49
|
+
* yield* worker.forEach(function* (request) {
|
|
50
|
+
* return computeResponse(request);
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @example With progress streaming
|
|
55
|
+
* ```ts
|
|
56
|
+
* yield* worker.forEach(function* (request, ctx) {
|
|
57
|
+
* yield* ctx.progress({ step: 1, message: "Starting..." });
|
|
58
|
+
* yield* ctx.progress({ step: 2, message: "Processing..." });
|
|
59
|
+
* return { result: "done" };
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
forEach<WRequest, WResponse, WProgress = never>(
|
|
64
|
+
fn: (
|
|
65
|
+
request: WRequest,
|
|
66
|
+
ctx: ForEachContext<WProgress>,
|
|
67
|
+
) => Operation<WResponse>,
|
|
68
|
+
): Operation<TReturn>;
|
|
27
69
|
}
|
|
28
70
|
|
|
29
71
|
/**
|
|
@@ -80,30 +122,93 @@ export function useWorker<TSend, TRecv, TReturn, TData>(
|
|
|
80
122
|
): Operation<WorkerResource<TSend, TRecv, TReturn>> {
|
|
81
123
|
return resource(function* (provide) {
|
|
82
124
|
let outcome = withResolvers<TReturn>();
|
|
125
|
+
let outcomeSettled = false;
|
|
126
|
+
|
|
127
|
+
const resolveOutcome = (value: TReturn) => {
|
|
128
|
+
if (outcomeSettled) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
outcomeSettled = true;
|
|
132
|
+
outcome.resolve(value);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const rejectOutcome = (error: Error) => {
|
|
136
|
+
if (outcomeSettled) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
outcomeSettled = true;
|
|
140
|
+
outcome.reject(error);
|
|
141
|
+
};
|
|
83
142
|
|
|
84
143
|
let worker = new Worker(url, options);
|
|
85
144
|
let subscription = yield* on(worker, "message");
|
|
86
145
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
146
|
+
// Channel for worker-initiated requests (buffered via eager subscription)
|
|
147
|
+
const requests = createChannel<
|
|
148
|
+
{ value: unknown; response: MessagePort },
|
|
149
|
+
void
|
|
150
|
+
>();
|
|
151
|
+
// Subscribe immediately so messages buffer before forEach is called
|
|
152
|
+
const requestSubscription = yield* requests;
|
|
153
|
+
|
|
154
|
+
// Flags for forEach state
|
|
155
|
+
let forEachInProgress = false;
|
|
156
|
+
let forEachCompleted = false;
|
|
157
|
+
let opened = false;
|
|
158
|
+
|
|
159
|
+
// Signal for when worker is ready (received "open" message)
|
|
160
|
+
const ready = withResolvers<void>();
|
|
161
|
+
|
|
162
|
+
// Spawned message loop - handles incoming messages using each pattern
|
|
163
|
+
yield* spawn(function* () {
|
|
164
|
+
while (true) {
|
|
165
|
+
const next = yield* subscription.next();
|
|
166
|
+
if (next.done) {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const msg = next.value.data;
|
|
171
|
+
if (!opened && msg.type !== "open") {
|
|
172
|
+
const error = new Error(
|
|
173
|
+
`expected first message to arrive from worker to be of type "open", but was: ${msg.type}`,
|
|
174
|
+
);
|
|
175
|
+
ready.reject(error);
|
|
176
|
+
throw error;
|
|
94
177
|
}
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
178
|
|
|
98
|
-
|
|
179
|
+
if (msg.type === "open") {
|
|
180
|
+
opened = true;
|
|
181
|
+
ready.resolve();
|
|
182
|
+
} else if (msg.type === "close") {
|
|
183
|
+
const { result } = msg as { result: Result<TReturn> };
|
|
184
|
+
if (result.ok) {
|
|
185
|
+
resolveOutcome(result.value);
|
|
186
|
+
} else {
|
|
187
|
+
const serializedError = result.error as unknown as SerializedError;
|
|
188
|
+
rejectOutcome(
|
|
189
|
+
errorFromSerialized("Worker failed", serializedError),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
// Close channel so forEach terminates naturally
|
|
193
|
+
yield* requests.close(undefined);
|
|
194
|
+
} else if (msg.type === "request") {
|
|
195
|
+
yield* requests.send({ value: msg.value, response: msg.response });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
99
198
|
|
|
100
|
-
|
|
199
|
+
if (!opened) {
|
|
200
|
+
const error = new Error(
|
|
201
|
+
"worker terminated before sending open message",
|
|
202
|
+
);
|
|
203
|
+
ready.reject(error);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
101
207
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
`expected first message to arrive from worker to be of type "open", but was: ${first.value.data.type}`,
|
|
105
|
-
);
|
|
208
|
+
// Wait for "open" message before proceeding
|
|
209
|
+
yield* ready.operation;
|
|
106
210
|
|
|
211
|
+
// Handle worker errors
|
|
107
212
|
yield* spawn(function* () {
|
|
108
213
|
let event = yield* once(worker, "error");
|
|
109
214
|
event.preventDefault();
|
|
@@ -118,29 +223,108 @@ export function useWorker<TSend, TRecv, TReturn, TData>(
|
|
|
118
223
|
|
|
119
224
|
yield* provide({
|
|
120
225
|
*send(value) {
|
|
121
|
-
|
|
226
|
+
const response = yield* useChannelResponse<TRecv>();
|
|
122
227
|
worker.postMessage(
|
|
123
228
|
{
|
|
124
229
|
type: "send",
|
|
125
230
|
value,
|
|
126
|
-
response:
|
|
231
|
+
response: response.port,
|
|
127
232
|
},
|
|
128
|
-
[
|
|
233
|
+
[response.port],
|
|
129
234
|
);
|
|
130
|
-
|
|
131
|
-
let event = yield* once(channel.port1, "message");
|
|
132
|
-
let result = (event as MessageEvent).data;
|
|
235
|
+
const result = yield* response;
|
|
133
236
|
if (result.ok) {
|
|
134
237
|
return result.value;
|
|
135
238
|
}
|
|
136
|
-
throw result.error;
|
|
239
|
+
throw errorFromSerialized("Worker handler failed", result.error);
|
|
137
240
|
},
|
|
241
|
+
|
|
242
|
+
*forEach<WRequest, WResponse, WProgress = never>(
|
|
243
|
+
fn: (
|
|
244
|
+
request: WRequest,
|
|
245
|
+
ctx: ForEachContext<WProgress>,
|
|
246
|
+
) => Operation<WResponse>,
|
|
247
|
+
): Operation<TReturn> {
|
|
248
|
+
// Prevent calling forEach more than once
|
|
249
|
+
if (forEachCompleted) {
|
|
250
|
+
throw new Error("forEach has already completed");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Prevent concurrent forEach
|
|
254
|
+
if (forEachInProgress) {
|
|
255
|
+
throw new Error("forEach is already in progress");
|
|
256
|
+
}
|
|
257
|
+
forEachInProgress = true;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
// Iterate until channel closes (when worker sends "close")
|
|
261
|
+
let next = yield* requestSubscription.next();
|
|
262
|
+
while (!next.done) {
|
|
263
|
+
const request = next.value;
|
|
264
|
+
// Track handler errors - we forward to worker but also re-throw to host
|
|
265
|
+
let handlerError: Error | undefined;
|
|
266
|
+
|
|
267
|
+
// Create a task for this request and wait for it to complete
|
|
268
|
+
const task = yield* spawn(function* () {
|
|
269
|
+
const channelRequest = yield* useChannelRequest<
|
|
270
|
+
WResponse,
|
|
271
|
+
WProgress
|
|
272
|
+
>(request.response);
|
|
273
|
+
try {
|
|
274
|
+
// Create context with progress method
|
|
275
|
+
const ctx: ForEachContext<WProgress> = {
|
|
276
|
+
progress: (data: WProgress) =>
|
|
277
|
+
channelRequest.progress(data),
|
|
278
|
+
};
|
|
279
|
+
const result = yield* fn(request.value as WRequest, ctx);
|
|
280
|
+
yield* channelRequest.resolve(result);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
// Forward error to worker so it knows the request failed
|
|
283
|
+
yield* channelRequest.reject(error as Error);
|
|
284
|
+
// Store error to re-throw after forwarding (don't swallow host errors)
|
|
285
|
+
handlerError = error as Error;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Wait for the handler to complete
|
|
290
|
+
yield* task;
|
|
291
|
+
|
|
292
|
+
// If the handler failed, stop processing and re-throw
|
|
293
|
+
if (handlerError) {
|
|
294
|
+
throw handlerError;
|
|
295
|
+
}
|
|
296
|
+
next = yield* requestSubscription.next();
|
|
297
|
+
}
|
|
298
|
+
return yield* outcome.operation;
|
|
299
|
+
} finally {
|
|
300
|
+
forEachInProgress = false;
|
|
301
|
+
forEachCompleted = true;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
|
|
138
305
|
[Symbol.iterator]: outcome.operation[Symbol.iterator],
|
|
139
306
|
});
|
|
140
307
|
} finally {
|
|
141
308
|
worker.postMessage({ type: "close" });
|
|
309
|
+
if (!outcomeSettled) {
|
|
310
|
+
while (!outcomeSettled) {
|
|
311
|
+
const event = yield* once(worker, "message");
|
|
312
|
+
const msg = event.data;
|
|
313
|
+
if (msg.type === "close") {
|
|
314
|
+
const { result } = msg as { result: Result<TReturn> };
|
|
315
|
+
if (result.ok) {
|
|
316
|
+
resolveOutcome(result.value);
|
|
317
|
+
} else {
|
|
318
|
+
const serializedError =
|
|
319
|
+
result.error as unknown as SerializedError;
|
|
320
|
+
rejectOutcome(
|
|
321
|
+
errorFromSerialized("Worker failed", serializedError),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
142
327
|
yield* settled(outcome.operation);
|
|
143
|
-
worker.removeEventListener("message", onclose);
|
|
144
328
|
}
|
|
145
329
|
});
|
|
146
330
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"message-channel.d.ts","sourceRoot":"","sources":["../message-channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,WAAW,CAAC;AAErD,wBAAgB,iBAAiB,IAAI,SAAS,CAAC,cAAc,CAAC,CAU7D"}
|
package/dist/message-channel.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { resource } from "effection";
|
|
2
|
-
export function useMessageChannel() {
|
|
3
|
-
return resource(function* (provide) {
|
|
4
|
-
let channel = new MessageChannel();
|
|
5
|
-
try {
|
|
6
|
-
yield* provide(channel);
|
|
7
|
-
}
|
|
8
|
-
finally {
|
|
9
|
-
channel.port1.close();
|
|
10
|
-
channel.port2.close();
|
|
11
|
-
}
|
|
12
|
-
});
|
|
13
|
-
}
|
package/message-channel.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { type Operation, resource } from "effection";
|
|
2
|
-
|
|
3
|
-
export function useMessageChannel(): Operation<MessageChannel> {
|
|
4
|
-
return resource(function* (provide) {
|
|
5
|
-
let channel = new MessageChannel();
|
|
6
|
-
try {
|
|
7
|
-
yield* provide(channel);
|
|
8
|
-
} finally {
|
|
9
|
-
channel.port1.close();
|
|
10
|
-
channel.port2.close();
|
|
11
|
-
}
|
|
12
|
-
});
|
|
13
|
-
}
|