@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/channel.ts ADDED
@@ -0,0 +1,380 @@
1
+ import { timebox } from "@effectionx/timebox";
2
+ import {
3
+ type Operation,
4
+ type Subscription,
5
+ once,
6
+ race,
7
+ resource,
8
+ } from "effection";
9
+ import {
10
+ type ChannelAck,
11
+ type ChannelMessage,
12
+ type SerializedResult,
13
+ serializeError,
14
+ } from "./types.ts";
15
+
16
+ /**
17
+ * Options for creating a channel response.
18
+ */
19
+ export interface ChannelResponseOptions {
20
+ /** Optional timeout in milliseconds. If exceeded, throws an error. */
21
+ timeout?: number;
22
+ }
23
+
24
+ /**
25
+ * Requester side - creates channel, waits for SerializedResult response.
26
+ *
27
+ * This interface is both:
28
+ * - An object with a `port` property to transfer to the responder
29
+ * - An `Operation` that can be yielded to wait for the response
30
+ *
31
+ * The operation returns `SerializedResult<T>` which the caller must handle:
32
+ * - `{ ok: true, value: T }` for success
33
+ * - `{ ok: false, error: SerializedError }` for error
34
+ *
35
+ * For progress streaming, use the `progress` property which returns a Subscription
36
+ * that yields progress values and returns the final response.
37
+ *
38
+ * @template TResponse - The response type
39
+ * @template TProgress - The progress type (defaults to `never` for no progress)
40
+ */
41
+ export interface ChannelResponse<TResponse, TProgress = never>
42
+ extends Operation<SerializedResult<TResponse>> {
43
+ /** Port to transfer to the responder */
44
+ port: MessagePort;
45
+
46
+ /**
47
+ * Get a subscription that yields progress values and returns the final response.
48
+ * Use this when you want to receive progress updates during the request.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * const subscription = yield* response.progress;
53
+ * let next = yield* subscription.next();
54
+ * while (!next.done) {
55
+ * console.log("Progress:", next.value);
56
+ * next = yield* subscription.next();
57
+ * }
58
+ * const result = next.value; // SerializedResult<TResponse>
59
+ * ```
60
+ */
61
+ progress: Operation<Subscription<TProgress, SerializedResult<TResponse>>>;
62
+ }
63
+
64
+ /**
65
+ * Responder side - wraps port, sends response as SerializedResult.
66
+ *
67
+ * - `resolve(value)` wraps in `{ ok: true, value }` internally
68
+ * - `reject(error)` serializes error and wraps in `{ ok: false, error }` internally
69
+ *
70
+ * **Note:** `reject()` is for **application-level errors** that the requester will
71
+ * receive as `{ ok: false, error }`. It is not a transport-level failure - the
72
+ * response is successfully delivered and acknowledged. Use this when the operation
73
+ * completed but with an error result (e.g., validation failed, resource not found).
74
+ *
75
+ * Port cleanup is handled by the resource's finally block. The close event on
76
+ * MessagePort is used to detect requester cancellation; behavior may vary slightly
77
+ * across runtimes (Node.js worker_threads, browser, Deno).
78
+ *
79
+ * @template TResponse - The response type
80
+ * @template TProgress - The progress type (defaults to `never` for no progress)
81
+ */
82
+ export interface ChannelRequest<TResponse, TProgress = never> {
83
+ /** Send success response (wraps in SerializedResult internally) and wait for ACK */
84
+ resolve(value: TResponse): Operation<void>;
85
+
86
+ /**
87
+ * Send error response (serializes and wraps in SerializedResult internally) and wait for ACK.
88
+ *
89
+ * This is for **application-level errors** - the response is still successfully
90
+ * delivered. The requester receives `{ ok: false, error: SerializedError }`.
91
+ */
92
+ reject(error: Error): Operation<void>;
93
+
94
+ /**
95
+ * Send a progress update and wait for acknowledgement.
96
+ * This provides backpressure - the operation blocks until the requester acknowledges.
97
+ *
98
+ * If the requester cancels (port closes), this returns gracefully without throwing.
99
+ *
100
+ * @param data - The progress data to send
101
+ */
102
+ progress(data: TProgress): Operation<void>;
103
+ }
104
+
105
+ /**
106
+ * Create a MessageChannel for request-response communication.
107
+ * Returns a `ChannelResponse` that is both an object with a `port` property
108
+ * and an `Operation` that can be yielded to wait for the response.
109
+ *
110
+ * The operation:
111
+ * - Races between receiving a message and the port closing (responder crash detection)
112
+ * - Optionally applies a timeout if specified in options
113
+ * - Sends ACK after receiving response
114
+ * - Returns `SerializedResult<T>` that the caller must handle
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * const response = yield* useChannelResponse<string>();
119
+ *
120
+ * // Transfer port to responder
121
+ * worker.postMessage({ type: "request", response: response.port }, [response.port]);
122
+ *
123
+ * // Wait for response (automatically sends ACK)
124
+ * const result = yield* response;
125
+ * if (result.ok) {
126
+ * console.log(result.value);
127
+ * } else {
128
+ * throw errorFromSerialized("Request failed", result.error);
129
+ * }
130
+ * ```
131
+ *
132
+ * @example With timeout
133
+ * ```ts
134
+ * const response = yield* useChannelResponse<string>({ timeout: 5000 });
135
+ *
136
+ * // If responder doesn't respond within 5 seconds, throws error
137
+ * const result = yield* response;
138
+ * ```
139
+ *
140
+ * @example With progress streaming
141
+ * ```ts
142
+ * const response = yield* useChannelResponse<string, number>();
143
+ * const subscription = yield* response.progress;
144
+ * let next = yield* subscription.next();
145
+ * while (!next.done) {
146
+ * console.log("Progress:", next.value);
147
+ * next = yield* subscription.next();
148
+ * }
149
+ * const result = next.value; // SerializedResult<string>
150
+ * ```
151
+ */
152
+ export function useChannelResponse<TResponse, TProgress = never>(
153
+ options?: ChannelResponseOptions,
154
+ ): Operation<ChannelResponse<TResponse, TProgress>> {
155
+ return resource(function* (provide) {
156
+ const channel = new MessageChannel();
157
+ channel.port1.start();
158
+
159
+ try {
160
+ yield* provide({
161
+ port: channel.port2,
162
+
163
+ // Direct yield* response - ignores progress, waits for final response
164
+ *[Symbol.iterator]() {
165
+ function* waitForResponse(): Operation<SerializedResult<TResponse>> {
166
+ // Loop until we get a response (skip any progress messages)
167
+ while (true) {
168
+ // Race between message and port close (responder crashed/exited)
169
+ const event = yield* race([
170
+ once(channel.port1, "message"),
171
+ once(channel.port1, "close"),
172
+ ]);
173
+
174
+ // If port closed, responder never responded
175
+ if ((event as Event).type === "close") {
176
+ throw new Error("Channel closed before response received");
177
+ }
178
+
179
+ const msg = (event as MessageEvent).data as ChannelMessage<
180
+ TResponse,
181
+ TProgress
182
+ >;
183
+
184
+ // If it's a progress message, ACK it and continue waiting
185
+ if (msg.type === "progress") {
186
+ channel.port1.postMessage({
187
+ type: "progress_ack",
188
+ } satisfies ChannelAck);
189
+ continue;
190
+ }
191
+
192
+ // It's a response - send ACK and return
193
+ channel.port1.postMessage({ type: "ack" } satisfies ChannelAck);
194
+ return msg.result;
195
+ }
196
+ }
197
+
198
+ // If timeout specified, use timebox
199
+ if (options?.timeout !== undefined) {
200
+ const result = yield* timebox(options.timeout, waitForResponse);
201
+ if (result.timeout) {
202
+ throw new Error(
203
+ `Channel response timed out after ${options.timeout}ms`,
204
+ );
205
+ }
206
+ return result.value;
207
+ }
208
+
209
+ // No timeout - wait indefinitely (with close detection)
210
+ return yield* waitForResponse();
211
+ },
212
+
213
+ // Progress subscription - yields progress values, returns final response
214
+ get progress(): Operation<
215
+ Subscription<TProgress, SerializedResult<TResponse>>
216
+ > {
217
+ const port = channel.port1;
218
+ const timeout = options?.timeout;
219
+
220
+ return resource(function* (provide) {
221
+ // Create the subscription object
222
+ const subscription: Subscription<
223
+ TProgress,
224
+ SerializedResult<TResponse>
225
+ > = {
226
+ *next() {
227
+ function* waitForNext(): Operation<
228
+ IteratorResult<TProgress, SerializedResult<TResponse>>
229
+ > {
230
+ // Race between message and port close
231
+ const event = yield* race([
232
+ once(port, "message"),
233
+ once(port, "close"),
234
+ ]);
235
+
236
+ // If port closed, throw error
237
+ if ((event as Event).type === "close") {
238
+ throw new Error("Channel closed before response received");
239
+ }
240
+
241
+ const msg = (event as MessageEvent).data as ChannelMessage<
242
+ TResponse,
243
+ TProgress
244
+ >;
245
+
246
+ if (msg.type === "progress") {
247
+ // ACK the progress
248
+ port.postMessage({
249
+ type: "progress_ack",
250
+ } satisfies ChannelAck);
251
+ // Yield the progress value
252
+ return { done: false, value: msg.data };
253
+ }
254
+
255
+ // It's a response - ACK and return done with value
256
+ port.postMessage({ type: "ack" } satisfies ChannelAck);
257
+ return { done: true, value: msg.result };
258
+ }
259
+
260
+ // If timeout specified, use timebox.
261
+ // Note: timeout is applied per-call (each next() has its own timeout window),
262
+ // not cumulatively for the entire exchange. This means a slow stream with many
263
+ // progress updates could exceed the intended overall timeout if each individual
264
+ // update arrives within the per-call limit.
265
+ if (timeout !== undefined) {
266
+ const result = yield* timebox(timeout, waitForNext);
267
+ if (result.timeout) {
268
+ throw new Error(
269
+ `Channel response timed out after ${timeout}ms`,
270
+ );
271
+ }
272
+ return result.value;
273
+ }
274
+
275
+ return yield* waitForNext();
276
+ },
277
+ };
278
+
279
+ yield* provide(subscription);
280
+ });
281
+ },
282
+ });
283
+ } finally {
284
+ channel.port1.close();
285
+ }
286
+ });
287
+ }
288
+
289
+ /**
290
+ * Wrap a received MessagePort to send a response.
291
+ * Returns resolve/reject/progress operations to complete the request.
292
+ *
293
+ * All methods:
294
+ * - Use the appropriate message format for progress streaming
295
+ * - Race between ACK message and port close (requester cancellation detection)
296
+ * - Return gracefully if port is closed (requester cancelled)
297
+ *
298
+ * Port cleanup is handled by the resource's finally block.
299
+ *
300
+ * @example Basic usage
301
+ * ```ts
302
+ * const { resolve, reject } = yield* useChannelRequest<string>(msg.response);
303
+ *
304
+ * try {
305
+ * const result = yield* doWork(msg.value);
306
+ * yield* resolve(result); // Wrapped in { ok: true, value } internally
307
+ * } catch (error) {
308
+ * yield* reject(error as Error); // Serialized and wrapped in { ok: false, error } internally
309
+ * }
310
+ * ```
311
+ *
312
+ * @example With progress streaming
313
+ * ```ts
314
+ * const { resolve, progress } = yield* useChannelRequest<string, number>(msg.response);
315
+ *
316
+ * yield* progress(25); // Send progress, wait for ACK
317
+ * yield* progress(50);
318
+ * yield* progress(75);
319
+ * yield* resolve("complete");
320
+ * ```
321
+ */
322
+ export function useChannelRequest<TResponse, TProgress = never>(
323
+ port: MessagePort,
324
+ ): Operation<ChannelRequest<TResponse, TProgress>> {
325
+ return resource(function* (provide) {
326
+ port.start();
327
+
328
+ /**
329
+ * Wait for an ACK message from the requester, or exit gracefully if port closes.
330
+ * @param expectedType - The expected ACK type ("ack" or "progress_ack")
331
+ */
332
+ function* waitForAck(expectedType: ChannelAck["type"]): Operation<void> {
333
+ const event = yield* race([once(port, "message"), once(port, "close")]);
334
+
335
+ // If port closed, requester was cancelled - exit gracefully
336
+ if ((event as Event).type === "close") {
337
+ return;
338
+ }
339
+
340
+ // Validate ACK
341
+ const ack = (event as MessageEvent).data as ChannelAck;
342
+ if (ack?.type !== expectedType) {
343
+ throw new Error(`Expected ${expectedType}, got: ${ack?.type}`);
344
+ }
345
+ }
346
+
347
+ try {
348
+ yield* provide({
349
+ *resolve(value: TResponse) {
350
+ const msg: ChannelMessage<TResponse, TProgress> = {
351
+ type: "response",
352
+ result: { ok: true, value },
353
+ };
354
+ port.postMessage(msg);
355
+ yield* waitForAck("ack");
356
+ },
357
+
358
+ *reject(error: Error) {
359
+ const msg: ChannelMessage<TResponse, TProgress> = {
360
+ type: "response",
361
+ result: { ok: false, error: serializeError(error) },
362
+ };
363
+ port.postMessage(msg);
364
+ yield* waitForAck("ack");
365
+ },
366
+
367
+ *progress(data: TProgress) {
368
+ const msg: ChannelMessage<TResponse, TProgress> = {
369
+ type: "progress",
370
+ data,
371
+ };
372
+ port.postMessage(msg);
373
+ yield* waitForAck("progress_ack");
374
+ },
375
+ });
376
+ } finally {
377
+ port.close();
378
+ }
379
+ });
380
+ }
@@ -0,0 +1,167 @@
1
+ import { type Operation, type Subscription } from "effection";
2
+ import { type SerializedResult } from "./types.ts";
3
+ /**
4
+ * Options for creating a channel response.
5
+ */
6
+ export interface ChannelResponseOptions {
7
+ /** Optional timeout in milliseconds. If exceeded, throws an error. */
8
+ timeout?: number;
9
+ }
10
+ /**
11
+ * Requester side - creates channel, waits for SerializedResult response.
12
+ *
13
+ * This interface is both:
14
+ * - An object with a `port` property to transfer to the responder
15
+ * - An `Operation` that can be yielded to wait for the response
16
+ *
17
+ * The operation returns `SerializedResult<T>` which the caller must handle:
18
+ * - `{ ok: true, value: T }` for success
19
+ * - `{ ok: false, error: SerializedError }` for error
20
+ *
21
+ * For progress streaming, use the `progress` property which returns a Subscription
22
+ * that yields progress values and returns the final response.
23
+ *
24
+ * @template TResponse - The response type
25
+ * @template TProgress - The progress type (defaults to `never` for no progress)
26
+ */
27
+ export interface ChannelResponse<TResponse, TProgress = never> extends Operation<SerializedResult<TResponse>> {
28
+ /** Port to transfer to the responder */
29
+ port: MessagePort;
30
+ /**
31
+ * Get a subscription that yields progress values and returns the final response.
32
+ * Use this when you want to receive progress updates during the request.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const subscription = yield* response.progress;
37
+ * let next = yield* subscription.next();
38
+ * while (!next.done) {
39
+ * console.log("Progress:", next.value);
40
+ * next = yield* subscription.next();
41
+ * }
42
+ * const result = next.value; // SerializedResult<TResponse>
43
+ * ```
44
+ */
45
+ progress: Operation<Subscription<TProgress, SerializedResult<TResponse>>>;
46
+ }
47
+ /**
48
+ * Responder side - wraps port, sends response as SerializedResult.
49
+ *
50
+ * - `resolve(value)` wraps in `{ ok: true, value }` internally
51
+ * - `reject(error)` serializes error and wraps in `{ ok: false, error }` internally
52
+ *
53
+ * **Note:** `reject()` is for **application-level errors** that the requester will
54
+ * receive as `{ ok: false, error }`. It is not a transport-level failure - the
55
+ * response is successfully delivered and acknowledged. Use this when the operation
56
+ * completed but with an error result (e.g., validation failed, resource not found).
57
+ *
58
+ * Port cleanup is handled by the resource's finally block. The close event on
59
+ * MessagePort is used to detect requester cancellation; behavior may vary slightly
60
+ * across runtimes (Node.js worker_threads, browser, Deno).
61
+ *
62
+ * @template TResponse - The response type
63
+ * @template TProgress - The progress type (defaults to `never` for no progress)
64
+ */
65
+ export interface ChannelRequest<TResponse, TProgress = never> {
66
+ /** Send success response (wraps in SerializedResult internally) and wait for ACK */
67
+ resolve(value: TResponse): Operation<void>;
68
+ /**
69
+ * Send error response (serializes and wraps in SerializedResult internally) and wait for ACK.
70
+ *
71
+ * This is for **application-level errors** - the response is still successfully
72
+ * delivered. The requester receives `{ ok: false, error: SerializedError }`.
73
+ */
74
+ reject(error: Error): Operation<void>;
75
+ /**
76
+ * Send a progress update and wait for acknowledgement.
77
+ * This provides backpressure - the operation blocks until the requester acknowledges.
78
+ *
79
+ * If the requester cancels (port closes), this returns gracefully without throwing.
80
+ *
81
+ * @param data - The progress data to send
82
+ */
83
+ progress(data: TProgress): Operation<void>;
84
+ }
85
+ /**
86
+ * Create a MessageChannel for request-response communication.
87
+ * Returns a `ChannelResponse` that is both an object with a `port` property
88
+ * and an `Operation` that can be yielded to wait for the response.
89
+ *
90
+ * The operation:
91
+ * - Races between receiving a message and the port closing (responder crash detection)
92
+ * - Optionally applies a timeout if specified in options
93
+ * - Sends ACK after receiving response
94
+ * - Returns `SerializedResult<T>` that the caller must handle
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * const response = yield* useChannelResponse<string>();
99
+ *
100
+ * // Transfer port to responder
101
+ * worker.postMessage({ type: "request", response: response.port }, [response.port]);
102
+ *
103
+ * // Wait for response (automatically sends ACK)
104
+ * const result = yield* response;
105
+ * if (result.ok) {
106
+ * console.log(result.value);
107
+ * } else {
108
+ * throw errorFromSerialized("Request failed", result.error);
109
+ * }
110
+ * ```
111
+ *
112
+ * @example With timeout
113
+ * ```ts
114
+ * const response = yield* useChannelResponse<string>({ timeout: 5000 });
115
+ *
116
+ * // If responder doesn't respond within 5 seconds, throws error
117
+ * const result = yield* response;
118
+ * ```
119
+ *
120
+ * @example With progress streaming
121
+ * ```ts
122
+ * const response = yield* useChannelResponse<string, number>();
123
+ * const subscription = yield* response.progress;
124
+ * let next = yield* subscription.next();
125
+ * while (!next.done) {
126
+ * console.log("Progress:", next.value);
127
+ * next = yield* subscription.next();
128
+ * }
129
+ * const result = next.value; // SerializedResult<string>
130
+ * ```
131
+ */
132
+ export declare function useChannelResponse<TResponse, TProgress = never>(options?: ChannelResponseOptions): Operation<ChannelResponse<TResponse, TProgress>>;
133
+ /**
134
+ * Wrap a received MessagePort to send a response.
135
+ * Returns resolve/reject/progress operations to complete the request.
136
+ *
137
+ * All methods:
138
+ * - Use the appropriate message format for progress streaming
139
+ * - Race between ACK message and port close (requester cancellation detection)
140
+ * - Return gracefully if port is closed (requester cancelled)
141
+ *
142
+ * Port cleanup is handled by the resource's finally block.
143
+ *
144
+ * @example Basic usage
145
+ * ```ts
146
+ * const { resolve, reject } = yield* useChannelRequest<string>(msg.response);
147
+ *
148
+ * try {
149
+ * const result = yield* doWork(msg.value);
150
+ * yield* resolve(result); // Wrapped in { ok: true, value } internally
151
+ * } catch (error) {
152
+ * yield* reject(error as Error); // Serialized and wrapped in { ok: false, error } internally
153
+ * }
154
+ * ```
155
+ *
156
+ * @example With progress streaming
157
+ * ```ts
158
+ * const { resolve, progress } = yield* useChannelRequest<string, number>(msg.response);
159
+ *
160
+ * yield* progress(25); // Send progress, wait for ACK
161
+ * yield* progress(50);
162
+ * yield* progress(75);
163
+ * yield* resolve("complete");
164
+ * ```
165
+ */
166
+ export declare function useChannelRequest<TResponse, TProgress = never>(port: MessagePort): Operation<ChannelRequest<TResponse, TProgress>>;
167
+ //# sourceMappingURL=channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../channel.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,YAAY,EAIlB,MAAM,WAAW,CAAC;AACnB,OAAO,EAGL,KAAK,gBAAgB,EAEtB,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,KAAK,CAC3D,SAAQ,SAAS,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC9C,wCAAwC;IACxC,IAAI,EAAE,WAAW,CAAC;IAElB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,EAAE,SAAS,CAAC,YAAY,CAAC,SAAS,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;CAC3E;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,KAAK;IAC1D,oFAAoF;IACpF,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAE3C;;;;;OAKG;IACH,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAEtC;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,KAAK,EAC7D,OAAO,CAAC,EAAE,sBAAsB,GAC/B,SAAS,CAAC,eAAe,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAqIlD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,GAAG,KAAK,EAC5D,IAAI,EAAE,WAAW,GAChB,SAAS,CAAC,cAAc,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAwDjD"}