@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/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
- export interface WorkerMainOptions<TSend, TRecv, TData> {
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
- * Namespace that provides APIs for working with incoming messages
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
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,MAAM,aAAa,CAAC,KAAK,EAAE,KAAK,IAClC;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,CAAC;CACb,GACD;IACE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,WAAW,CAAC;CACvB,GACD;IACE,IAAI,EAAE,OAAO,CAAC;CACf,CAAC;AAEN,MAAM,WAAW,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK;IACpD;;OAEG;IACH,QAAQ,EAAE,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACvC;;OAEG;IACH,IAAI,EAAE,KAAK,CAAC;CACb;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc,CAAC,KAAK,EAAE,KAAK;IAC1C,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;CACpE"}
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
- export {};
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
+ }
@@ -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
- * @param {(options: WorkerMainOptions<TSend, TRecv, TData>) => Operation<TReturn>} body
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,EAOV,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAiB,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAiBnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAC3D,IAAI,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,SAAS,CAAC,OAAO,CAAC,GAC5E,OAAO,CAAC,IAAI,CAAC,CA8Ef;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"}
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"}
@@ -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
- * @param {(options: WorkerMainOptions<TSend, TRecv, TData>) => Operation<TReturn>} body
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 = createSignal();
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
- response.postMessage(Ok(result));
160
+ yield* resolve(result);
90
161
  }
91
162
  catch (error) {
92
- response.postMessage(Err(error));
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
- * Argument received by workerMain function
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 TData - data passed from the main thread to the worker during initialization
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.
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../worker.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,SAAS,EAKf,MAAM,WAAW,CAAC;AAKnB;;;;;;GAMG;AACH,MAAM,WAAW,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CACnD,SAAQ,SAAS,CAAC,OAAO,CAAC;IAC1B,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;CACrC;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,CAkElD"}
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 assert from "node:assert";
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 { useMessageChannel } from "./message-channel.js";
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
- let onclose = (event) => {
59
- if (event.data.type === "close") {
60
- let { result } = event.data;
61
- if (result.ok) {
62
- outcome.resolve(result.value);
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
- outcome.reject(result.error);
112
+ else if (msg.type === "request") {
113
+ yield* requests.send({ value: msg.value, response: msg.response });
66
114
  }
67
115
  }
68
- };
69
- worker.addEventListener("message", onclose);
70
- let first = yield* subscription.next();
71
- assert(first.value.data.type === "open", `expected first message to arrive from worker to be of type "open", but was: ${first.value.data.type}`);
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
- let channel = yield* useMessageChannel();
137
+ const response = yield* useChannelResponse();
85
138
  worker.postMessage({
86
139
  type: "send",
87
140
  value,
88
- response: channel.port2,
89
- }, [channel.port2]);
90
- channel.port1.start();
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@effectionx/worker",
3
3
  "description": "Web Worker integration with two-way messaging and graceful shutdown",
4
- "version": "0.4.2",
4
+ "version": "0.5.0",
5
5
  "type": "module",
6
6
  "main": "./dist/mod.js",
7
7
  "types": "./dist/mod.d.ts",
@@ -29,9 +29,11 @@
29
29
  "sideEffects": false,
30
30
  "dependencies": {
31
31
  "web-worker": "^1",
32
+ "@effectionx/timebox": "0.4.1",
32
33
  "@effectionx/signals": "0.5.1"
33
34
  },
34
35
  "devDependencies": {
36
+ "effection": "^4",
35
37
  "@effectionx/bdd": "0.4.2",
36
38
  "@effectionx/converge": "0.1.2"
37
39
  }
@@ -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
+ });