@firtoz/worker-helper 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,92 @@
1
+ # @firtoz/worker-helper
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#22](https://github.com/firtoz/fullstack-toolkit/pull/22) [`cf12782`](https://github.com/firtoz/fullstack-toolkit/commit/cf1278236e484e6350eb614ce2381e0afcec326e) Thanks [@firtoz](https://github.com/firtoz)! - Initial release of `@firtoz/worker-helper` - Type-safe Web Worker communication with Zod validation for both client and worker sides.
8
+
9
+ > **⚠️ Early WIP Notice:** This package is in very early development and is **not production-ready**. It is TypeScript-only and may have breaking changes. While I (the maintainer) have limited time, I'm open to PRs for features, bug fixes, or additional support (like JS builds). Please feel free to try it out and contribute! See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
10
+
11
+ ## Worker-Side (`WorkerHelper`)
12
+
13
+ - **Abstract class pattern** for creating type-safe workers
14
+ - **Zod validation** for both incoming and outgoing messages
15
+ - **Mandatory error handlers** give complete control over error handling:
16
+ - `handleMessage` - Process validated messages
17
+ - `handleInputValidationError` - Handle input validation failures
18
+ - `handleOutputValidationError` - Handle output validation failures
19
+ - `handleProcessingError` - Handle runtime errors
20
+ - **Full async support** - All handlers support both sync and async operations
21
+ - **Type-safe `send()` method** - Automatically validates before sending
22
+ - Uses Bun's global `Worker` and `self` patterns
23
+
24
+ ## Client-Side (`WorkerClient`)
25
+
26
+ - **Type-safe wrapper** for Worker instances
27
+ - **Validates messages** sent TO the worker (client → worker)
28
+ - **Validates messages** received FROM the worker (worker → client)
29
+ - **Optional callbacks**:
30
+ - `onMessage` - Receive validated messages
31
+ - `onValidationError` - Handle validation failures
32
+ - `onError` - Handle worker errors
33
+ - **Worker lifecycle management** with `terminate()` and `getWorker()`
34
+ - Accepts existing Worker instances for maximum flexibility
35
+
36
+ ## Features
37
+
38
+ - Full TypeScript support with automatic type inference
39
+ - Works with discriminated unions for type-safe message routing
40
+ - Comprehensive test suite with 33 tests (18 for WorkerHelper, 15 for WorkerClient)
41
+ - Tests include async operations, validation errors, and error handling
42
+ - Uses `.worker.ts` extension convention for worker files
43
+ - Zero dependencies except Zod
44
+ - Built for Bun's Worker API
45
+
46
+ ## Example
47
+
48
+ ```typescript
49
+ // Define schemas
50
+ const InputSchema = z.discriminatedUnion("type", [
51
+ z.object({ type: z.literal("add"), a: z.number(), b: z.number() }),
52
+ ]);
53
+
54
+ const OutputSchema = z.discriminatedUnion("type", [
55
+ z.object({ type: z.literal("result"), value: z.number() }),
56
+ ]);
57
+
58
+ // Worker side (worker.worker.ts)
59
+ declare var self: Worker;
60
+
61
+ class MyWorker extends WorkerHelper<Input, Output> {
62
+ constructor() {
63
+ super(self, InputSchema, OutputSchema, {
64
+ handleMessage: (data) => {
65
+ if (data.type === "add") {
66
+ this.send({ type: "result", value: data.a + data.b });
67
+ }
68
+ },
69
+ handleInputValidationError: (error, originalData) => {
70
+ console.error("Invalid input:", error);
71
+ },
72
+ // ... other handlers
73
+ });
74
+ }
75
+ }
76
+
77
+ new MyWorker();
78
+
79
+ // Client side
80
+ const worker = new Worker(
81
+ new URL("./worker.worker.ts", import.meta.url).href
82
+ );
83
+
84
+ const client = new WorkerClient({
85
+ worker,
86
+ clientSchema: InputSchema,
87
+ serverSchema: OutputSchema,
88
+ onMessage: (msg) => console.log("Result:", msg.value),
89
+ });
90
+
91
+ client.send({ type: "add", a: 5, b: 3 }); // Type-safe!
92
+ ```
package/README.md ADDED
@@ -0,0 +1,337 @@
1
+ # @firtoz/worker-helper
2
+
3
+ Type-safe Web Worker helper with Zod validation for input and output messages. This package provides a simple way to create type-safe Web Workers with automatic validation of messages sent between the main thread and worker threads.
4
+
5
+ > **⚠️ Early WIP Notice:** This package is in very early development and is **not production-ready**. It is TypeScript-only and may have breaking changes. While I (the maintainer) have limited time, I'm open to PRs for features, bug fixes, or additional support (like JS builds). Please feel free to try it out and contribute! See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
6
+
7
+ ## Features
8
+
9
+ - 🔒 **Type-safe**: Full TypeScript support with automatic type inference
10
+ - ✅ **Zod Validation**: Automatic validation of both input and output messages
11
+ - 🎯 **Custom Error Handlers**: Mandatory error handlers give you complete control over error handling
12
+ - 🔄 **Async Support**: Built-in support for async message handlers
13
+ - 🧩 **Discriminated Unions**: Works great with Zod's discriminated unions for type-safe message routing
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ bun add @firtoz/worker-helper zod
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### 1. Define Your Schemas
24
+
25
+ First, define Zod schemas for your input and output messages:
26
+
27
+ ```typescript
28
+ import { z } from "zod";
29
+
30
+ const InputSchema = z.discriminatedUnion("type", [
31
+ z.object({
32
+ type: z.literal("add"),
33
+ a: z.number(),
34
+ b: z.number(),
35
+ }),
36
+ z.object({
37
+ type: z.literal("multiply"),
38
+ a: z.number(),
39
+ b: z.number(),
40
+ }),
41
+ ]);
42
+
43
+ const OutputSchema = z.discriminatedUnion("type", [
44
+ z.object({
45
+ type: z.literal("result"),
46
+ value: z.number(),
47
+ }),
48
+ z.object({
49
+ type: z.literal("error"),
50
+ message: z.string(),
51
+ }),
52
+ ]);
53
+
54
+ type Input = z.infer<typeof InputSchema>;
55
+ type Output = z.infer<typeof OutputSchema>;
56
+ ```
57
+
58
+ ### 2. Create Your Worker
59
+
60
+ Create a worker file (e.g., `worker.ts`):
61
+
62
+ ```typescript
63
+ import { WorkerHelper } from "@firtoz/worker-helper";
64
+ import { InputSchema, OutputSchema, type Input, type Output } from "./schemas";
65
+
66
+ // Declare self as Worker for TypeScript
67
+ declare var self: Worker;
68
+
69
+ new WorkerHelper<Input, Output>(self, InputSchema, OutputSchema, {
70
+ // Handle validated messages
71
+ handleMessage: (data, send) => {
72
+ switch (data.type) {
73
+ case "add":
74
+ send({
75
+ type: "result",
76
+ value: data.a + data.b,
77
+ });
78
+ break;
79
+
80
+ case "multiply":
81
+ send({
82
+ type: "result",
83
+ value: data.a * data.b,
84
+ });
85
+ break;
86
+ }
87
+ },
88
+
89
+ // Handle input validation errors
90
+ handleInputValidationError: (error, originalData) => {
91
+ console.error("Invalid input received:", error);
92
+ self.postMessage({
93
+ type: "error",
94
+ message: `Invalid input: ${error.message}`,
95
+ });
96
+ },
97
+
98
+ // Handle output validation errors
99
+ handleOutputValidationError: (error, originalData) => {
100
+ console.error("Invalid output attempted:", error);
101
+ self.postMessage({
102
+ type: "error",
103
+ message: `Internal error: invalid output`,
104
+ });
105
+ },
106
+
107
+ // Handle processing errors
108
+ handleProcessingError: (error, validatedData) => {
109
+ console.error("Processing error:", error);
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ self.postMessage({
112
+ type: "error",
113
+ message: `Processing failed: ${message}`,
114
+ });
115
+ },
116
+ });
117
+ ```
118
+
119
+ ### 3. Use Your Worker
120
+
121
+ In your main thread:
122
+
123
+ ```typescript
124
+ // Worker is a global in Bun, no need to import
125
+ const worker = new Worker(new URL("./worker.ts", import.meta.url).href);
126
+
127
+ // Send a message
128
+ worker.postMessage({
129
+ type: "add",
130
+ a: 5,
131
+ b: 3,
132
+ });
133
+
134
+ // Receive messages
135
+ worker.on("message", (result) => {
136
+ if (result.type === "result") {
137
+ console.log("Result:", result.value); // 8
138
+ } else if (result.type === "error") {
139
+ console.error("Error:", result.message);
140
+ }
141
+ });
142
+
143
+ // Clean up
144
+ worker.on("exit", () => {
145
+ console.log("Worker exited");
146
+ });
147
+ ```
148
+
149
+ ## API
150
+
151
+ ### `WorkerHelper<TInput, TOutput>`
152
+
153
+ The main class that manages worker message handling with validation.
154
+
155
+ #### Constructor Parameters
156
+
157
+ - `self: MessageTarget` - The worker's `self` object (or `parentPort` for Node.js compatibility)
158
+ - `inputSchema: ZodType<TInput>` - Zod schema for validating incoming messages
159
+ - `outputSchema: ZodType<TOutput>` - Zod schema for validating outgoing messages
160
+ - `handlers: WorkerHelperHandlers<TInput, TOutput>` - Object containing all message and error handlers
161
+
162
+ ### `WorkerHelperHandlers<TInput, TOutput>`
163
+
164
+ Interface defining all required handlers:
165
+
166
+ ```typescript
167
+ type WorkerHelperHandlers<TInput, TOutput> = {
168
+ // Handle validated messages
169
+ handleMessage: (
170
+ data: TInput,
171
+ send: (response: TOutput) => void,
172
+ ) => void | Promise<void>;
173
+
174
+ // Handle input validation errors
175
+ handleInputValidationError: (
176
+ error: ZodError<TInput>,
177
+ originalData: unknown,
178
+ ) => void | Promise<void>;
179
+
180
+ // Handle output validation errors
181
+ handleOutputValidationError: (
182
+ error: ZodError<TOutput>,
183
+ originalData: TOutput,
184
+ ) => void | Promise<void>;
185
+
186
+ // Handle processing errors (exceptions thrown in handleMessage)
187
+ handleProcessingError: (
188
+ error: unknown,
189
+ validatedData: TInput,
190
+ ) => void | Promise<void>;
191
+ };
192
+ ```
193
+
194
+ ## Advanced Usage
195
+
196
+ ### Async Message Handling
197
+
198
+ All handlers support both synchronous and asynchronous operations:
199
+
200
+ ```typescript
201
+ new WorkerHelper<Input, Output>(self, InputSchema, OutputSchema, {
202
+ handleMessage: async (data, send) => {
203
+ // Perform async operations
204
+ const result = await someAsyncOperation(data);
205
+ send(result);
206
+ },
207
+
208
+ handleInputValidationError: async (error, originalData) => {
209
+ // Log to remote service
210
+ await logError(error);
211
+ self.postMessage({ type: "error", message: "Invalid input" });
212
+ },
213
+
214
+ // ... other handlers
215
+ });
216
+ ```
217
+
218
+ ### Complex Message Types
219
+
220
+ Use discriminated unions for type-safe message routing:
221
+
222
+ ```typescript
223
+ const InputSchema = z.discriminatedUnion("type", [
224
+ z.object({
225
+ type: z.literal("compute"),
226
+ operation: z.enum(["add", "subtract", "multiply", "divide"]),
227
+ operands: z.array(z.number()),
228
+ }),
229
+ z.object({
230
+ type: z.literal("status"),
231
+ }),
232
+ z.object({
233
+ type: z.literal("config"),
234
+ settings: z.record(z.string(), z.unknown()),
235
+ }),
236
+ ]);
237
+
238
+ // TypeScript will narrow the type based on the discriminator
239
+ handleMessage: (data, send) => {
240
+ switch (data.type) {
241
+ case "compute":
242
+ // data is narrowed to { type: "compute", operation: ..., operands: ... }
243
+ break;
244
+ case "status":
245
+ // data is narrowed to { type: "status" }
246
+ break;
247
+ case "config":
248
+ // data is narrowed to { type: "config", settings: ... }
249
+ break;
250
+ }
251
+ };
252
+ ```
253
+
254
+ ### Custom Error Responses
255
+
256
+ You have full control over how errors are communicated back to the main thread:
257
+
258
+ ```typescript
259
+ handleInputValidationError: (error, originalData) => {
260
+ // Send structured error response
261
+ self.postMessage({
262
+ type: "error",
263
+ code: "VALIDATION_ERROR",
264
+ details: error.issues,
265
+ timestamp: Date.now(),
266
+ });
267
+ },
268
+
269
+ handleProcessingError: (error, validatedData) => {
270
+ // Send error with context
271
+ self.postMessage({
272
+ type: "error",
273
+ code: "PROCESSING_ERROR",
274
+ message: error instanceof Error ? error.message : String(error),
275
+ input: validatedData.type, // Include relevant context
276
+ });
277
+ },
278
+ ```
279
+
280
+ ## Error Handling
281
+
282
+ The WorkerHelper validates messages at three key points:
283
+
284
+ 1. **Input Validation**: Before your handler receives a message, it's validated against the input schema. If validation fails, `handleInputValidationError` is called.
285
+
286
+ 2. **Output Validation**: Before a message is sent from the worker, it's validated against the output schema. If validation fails, `handleOutputValidationError` is called.
287
+
288
+ 3. **Processing Errors**: If your `handleMessage` handler throws an error, `handleProcessingError` is called.
289
+
290
+ All error handlers are **mandatory**, ensuring you handle all error cases explicitly.
291
+
292
+ ## Best Practices
293
+
294
+ 1. **Use Discriminated Unions**: They provide type-safe message routing and better error messages.
295
+
296
+ 2. **Keep Schemas Strict**: Use strict schemas to catch errors early.
297
+
298
+ 3. **Log Errors Appropriately**: Use error handlers to log errors to your monitoring system.
299
+
300
+ 4. **Don't Swallow Errors**: Always communicate errors back to the main thread in some form.
301
+
302
+ 5. **Test Error Cases**: Use the error handlers to test how your application handles invalid inputs and processing errors.
303
+
304
+ ## Testing
305
+
306
+ The package includes comprehensive tests. Run them with:
307
+
308
+ ```bash
309
+ bun test
310
+ ```
311
+
312
+ See the test files for examples of testing workers with different scenarios:
313
+ - Valid message handling
314
+ - Input validation errors
315
+ - Output validation errors
316
+ - Processing errors
317
+ - Async operations
318
+ - Edge cases
319
+
320
+ ## License
321
+
322
+ MIT
323
+
324
+ ## Contributing
325
+
326
+ Contributions are welcome! Please feel free to submit a Pull Request.
327
+
328
+ ## Related Packages
329
+
330
+ - [@firtoz/maybe-error](../maybe-error) - Type-safe error handling pattern
331
+ - [@firtoz/hono-fetcher](../hono-fetcher) - Type-safe Hono API client
332
+ - [@firtoz/websocket-do](../websocket-do) - Type-safe WebSocket Durable Objects
333
+
334
+ ## Support
335
+
336
+ For issues and questions, please file an issue on [GitHub](https://github.com/firtoz/fullstack-toolkit/issues).
337
+
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@firtoz/worker-helper",
3
+ "version": "1.0.0",
4
+ "description": "Type-safe Web Worker helper with Zod validation for input and output messages",
5
+ "main": "./src/index.ts",
6
+ "module": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts",
12
+ "require": "./src/index.ts"
13
+ },
14
+ "./*": {
15
+ "types": "./src/*.ts",
16
+ "import": "./src/*.ts",
17
+ "require": "./src/*.ts"
18
+ }
19
+ },
20
+ "files": [
21
+ "src/**/*.ts",
22
+ "README.md",
23
+ "CHANGELOG.md"
24
+ ],
25
+ "scripts": {
26
+ "typecheck": "tsc --noEmit -p ./tsconfig.json",
27
+ "lint": "biome check --write src",
28
+ "lint:ci": "biome ci src",
29
+ "format": "biome format src --write",
30
+ "test": "bun test",
31
+ "test:watch": "bun test --watch"
32
+ },
33
+ "keywords": [
34
+ "typescript",
35
+ "web-worker",
36
+ "worker",
37
+ "zod",
38
+ "validation",
39
+ "type-safe"
40
+ ],
41
+ "author": "Firtina Ozbalikchi <firtoz@github.com>",
42
+ "license": "MIT",
43
+ "homepage": "https://github.com/firtoz/fullstack-toolkit#readme",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/firtoz/fullstack-toolkit.git",
47
+ "directory": "packages/worker-helper"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/firtoz/fullstack-toolkit/issues"
51
+ },
52
+ "engines": {
53
+ "node": ">=18.0.0"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "dependencies": {
59
+ "zod": "^4.1.12"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^24.10.1",
63
+ "@firtoz/maybe-error": "^1.5.1",
64
+ "bun-types": "^1.3.2"
65
+ }
66
+ }
@@ -0,0 +1,92 @@
1
+ import type { ZodType } from "zod/v4";
2
+
3
+ export interface WorkerClientOptions<TClientMessage, TServerMessage> {
4
+ /**
5
+ * Worker instance to wrap
6
+ */
7
+ worker: Worker;
8
+ /**
9
+ * Schema for validating messages sent to the worker
10
+ */
11
+ clientSchema: ZodType<TClientMessage>;
12
+ /**
13
+ * Schema for validating messages received from the worker
14
+ */
15
+ serverSchema: ZodType<TServerMessage>;
16
+ /**
17
+ * Callback for validated messages from the worker
18
+ */
19
+ onMessage?: (message: TServerMessage) => void;
20
+ /**
21
+ * Callback for when validation fails on incoming messages
22
+ */
23
+ onValidationError?: (error: Error, rawMessage: unknown) => void;
24
+ /**
25
+ * Callback for worker errors
26
+ */
27
+ onError?: (event: ErrorEvent) => void;
28
+ }
29
+
30
+ export class WorkerClient<TClientMessage, TServerMessage> {
31
+ private worker: Worker;
32
+ private readonly clientSchema: ZodType<TClientMessage>;
33
+ private readonly serverSchema: ZodType<TServerMessage>;
34
+ private readonly onMessageCallback?: (message: TServerMessage) => void;
35
+ private readonly onValidationErrorCallback?: (
36
+ error: Error,
37
+ rawMessage: unknown,
38
+ ) => void;
39
+
40
+ constructor(options: WorkerClientOptions<TClientMessage, TServerMessage>) {
41
+ this.clientSchema = options.clientSchema;
42
+ this.serverSchema = options.serverSchema;
43
+ this.onMessageCallback = options.onMessage;
44
+ this.onValidationErrorCallback = options.onValidationError;
45
+ this.worker = options.worker;
46
+
47
+ // Setup event handlers
48
+ this.worker.addEventListener("message", (event: MessageEvent) => {
49
+ this.handleMessage(event);
50
+ });
51
+
52
+ if (options.onError) {
53
+ this.worker.addEventListener("error", options.onError);
54
+ }
55
+ }
56
+
57
+ private handleMessage(event: MessageEvent): void {
58
+ try {
59
+ // Validate the incoming message
60
+ const validatedMessage = this.serverSchema.parse(event.data);
61
+ this.onMessageCallback?.(validatedMessage);
62
+ } catch (error) {
63
+ // Validation failed
64
+ const validationError =
65
+ error instanceof Error ? error : new Error(String(error));
66
+ this.onValidationErrorCallback?.(validationError, event.data);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Send a message to the worker with validation
72
+ */
73
+ public send(message: TClientMessage): void {
74
+ // Validate the outgoing message
75
+ const validatedMessage = this.clientSchema.parse(message);
76
+ this.worker.postMessage(validatedMessage);
77
+ }
78
+
79
+ /**
80
+ * Terminate the worker
81
+ */
82
+ public terminate(): void {
83
+ this.worker.terminate();
84
+ }
85
+
86
+ /**
87
+ * Get the underlying Worker instance
88
+ */
89
+ public getWorker(): Worker {
90
+ return this.worker;
91
+ }
92
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export {
2
+ WorkerClient,
3
+ type WorkerClientOptions,
4
+ } from "./WorkerClient";
5
+ export { WorkerHelper, type WorkerHelperHandlers } from "./worker-helper";
@@ -0,0 +1,71 @@
1
+ import type { ZodError, ZodType } from "zod/v4";
2
+
3
+ type MessageTarget = DedicatedWorkerGlobalScope;
4
+
5
+ export type WorkerHelperHandlers<TInput, TOutput> = {
6
+ handleMessage: (data: TInput) => void | Promise<void>;
7
+ handleInputValidationError: (
8
+ error: ZodError<TInput>,
9
+ originalData: unknown,
10
+ ) => void | Promise<void>;
11
+ handleOutputValidationError: (
12
+ error: ZodError<TOutput>,
13
+ originalData: TOutput,
14
+ ) => void | Promise<void>;
15
+ handleProcessingError: (
16
+ error: unknown,
17
+ validatedData: TInput,
18
+ ) => void | Promise<void>;
19
+ };
20
+
21
+ export abstract class WorkerHelper<TInput, TOutput> {
22
+ constructor(
23
+ private self: MessageTarget,
24
+ private inputSchema: ZodType<TInput>,
25
+ private outputSchema: ZodType<TOutput>,
26
+ private handlers: WorkerHelperHandlers<TInput, TOutput>,
27
+ ) {
28
+ this.setupMessageListener();
29
+ }
30
+
31
+ protected send = (response: TOutput) => {
32
+ // Validate output before sending
33
+ const outputValidation = this.outputSchema.safeParse(response);
34
+ if (!outputValidation.success) {
35
+ this.handlers.handleOutputValidationError(
36
+ outputValidation.error,
37
+ response,
38
+ );
39
+ return;
40
+ }
41
+
42
+ // Send as success response
43
+ this.self.postMessage(response);
44
+ };
45
+
46
+ private setupMessageListener(): void {
47
+ this.self.addEventListener("message", (event: MessageEvent) => {
48
+ this.handleMessage(event);
49
+ });
50
+ }
51
+
52
+ private async handleMessage(event: MessageEvent): Promise<void> {
53
+ // Validate input using safeParse
54
+ const validationResult = this.inputSchema.safeParse(event.data);
55
+
56
+ if (!validationResult.success) {
57
+ await this.handlers.handleInputValidationError(
58
+ validationResult.error,
59
+ event.data,
60
+ );
61
+ return;
62
+ }
63
+
64
+ // Handle the validated message
65
+ try {
66
+ await this.handlers.handleMessage(validationResult.data);
67
+ } catch (error) {
68
+ await this.handlers.handleProcessingError(error, validationResult.data);
69
+ }
70
+ }
71
+ }