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