@effectionx/worker 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/channel.test.ts +902 -0
- package/channel.ts +380 -0
- package/dist/channel.d.ts +167 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +236 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +137 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +27 -1
- package/dist/worker-main.d.ts +16 -2
- package/dist/worker-main.d.ts.map +1 -1
- package/dist/worker-main.js +78 -6
- package/dist/worker.d.ts +34 -2
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +138 -22
- package/package.json +8 -4
- package/test-assets/bidirectional-worker.ts +19 -0
- package/test-assets/concurrent-requests-worker.ts +9 -0
- package/test-assets/error-cause-worker.ts +19 -0
- package/test-assets/error-handling-worker.ts +12 -0
- package/test-assets/error-throw-worker.ts +9 -0
- package/test-assets/no-requests-worker.ts +5 -0
- package/test-assets/send-inside-foreach-worker.ts +18 -0
- package/test-assets/sequential-requests-worker.ts +10 -0
- package/test-assets/single-request-worker.ts +8 -0
- package/test-assets/slow-request-worker.ts +8 -0
- package/tsconfig.json +3 -0
- package/types.ts +157 -3
- package/worker-main.ts +119 -8
- package/worker.test.ts +302 -4
- package/worker.ts +213 -29
- package/dist/message-channel.d.ts +0 -3
- package/dist/message-channel.d.ts.map +0 -1
- package/dist/message-channel.js +0 -13
- package/message-channel.ts +0 -13
package/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"}
|