@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 +92 -0
- package/README.md +337 -0
- package/package.json +66 -0
- package/src/WorkerClient.ts +92 -0
- package/src/index.ts +5 -0
- package/src/worker-helper.ts +71 -0
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,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
|
+
}
|