Type-safe Web Worker helper with Zod validation and Cloudflare Workers utilities (cf-typegen)
npm install @firtoz/worker-helperType-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.
> ā ļø 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 for details.
- š Type-safe: Full TypeScript support with automatic type inference
- ā
Zod Validation: Automatic validation of both input and output messages
- šÆ Custom Error Handlers: Mandatory error handlers give you complete control over error handling
- š Async Support: Built-in support for async message handlers
- š§© Discriminated Unions: Works great with Zod's discriminated unions for type-safe message routing
- š§ cf-typegen: Automatic .env.local creation from wrangler.jsonc vars
- š Type Generation: Wrapper around wrangler types with env preparation
``bash`
bun add @firtoz/worker-helper zod
Automatic TypeScript type generation and .env.local management for Cloudflare Workers projects.
Add the script to your Cloudflare Workers package:
`json`
{
"scripts": {
"cf-typegen": "bun --cwd ../../packages/worker-helper cf-typegen $(pwd)"
}
}
1. Reads .env.local.example to find required env vars
2. Creates/updates .env.local with any missing vars (as empty strings)
3. Runs wrangler types to generate TypeScript definitions
`bash`
cd your-worker-package
bun run cf-typegen
Output:
``
Running CF typegen for: /path/to/your-worker
ā Added missing env vars: OPENROUTER_API_KEY, DATABASE_URL
Running wrangler types...
ā Wrangler types generated
ā CF typegen completed successfully
Generated .env.local:
`env`
OPENROUTER_API_KEY=
DATABASE_URL=
- Ensures wrangler types always succeeds (needs .env.local or .dev.vars).env.local
- Keeps in sync with .env.local.examplewrangler.jsonc
- Avoids accidentally binding empty vars at runtime via vars.env.local.example
- Developers can fill in actual values without committing them to git
- CI/CD can generate types without needing actual secrets
- serves as documentation for required env vars
First, define Zod schemas for your input and output messages:
`typescript
import { z } from "zod";
const InputSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("add"),
a: z.number(),
b: z.number(),
}),
z.object({
type: z.literal("multiply"),
a: z.number(),
b: z.number(),
}),
]);
const OutputSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("result"),
value: z.number(),
}),
z.object({
type: z.literal("error"),
message: z.string(),
}),
]);
type Input = z.infer
type Output = z.infer
`
Create a worker file (e.g., worker.ts):
`typescript
import { WorkerHelper } from "@firtoz/worker-helper";
import { InputSchema, OutputSchema, type Input, type Output } from "./schemas";
// Declare self as Worker for TypeScript
declare var self: Worker;
new WorkerHelper(self, InputSchema, OutputSchema, {
// Handle validated messages
handleMessage: (data, send) => {
switch (data.type) {
case "add":
send({
type: "result",
value: data.a + data.b,
});
break;
case "multiply":
send({
type: "result",
value: data.a * data.b,
});
break;
}
},
// Handle input validation errors
handleInputValidationError: (error, originalData) => {
console.error("Invalid input received:", error);
self.postMessage({
type: "error",
message: Invalid input: ${error.message},
});
},
// Handle output validation errors
handleOutputValidationError: (error, originalData) => {
console.error("Invalid output attempted:", error);
self.postMessage({
type: "error",
message: Internal error: invalid output,
});
},
// Handle processing errors
handleProcessingError: (error, validatedData) => {
console.error("Processing error:", error);
const message = error instanceof Error ? error.message : String(error);
self.postMessage({
type: "error",
message: Processing failed: ${message},`
});
},
});
In your main thread:
`typescript
// Worker is a global in Bun, no need to import
const worker = new Worker(new URL("./worker.ts", import.meta.url).href);
// Send a message
worker.postMessage({
type: "add",
a: 5,
b: 3,
});
// Receive messages
worker.on("message", (result) => {
if (result.type === "result") {
console.log("Result:", result.value); // 8
} else if (result.type === "error") {
console.error("Error:", result.message);
}
});
// Clean up
worker.on("exit", () => {
console.log("Worker exited");
});
`
The main class that manages worker message handling with validation.
#### Constructor Parameters
- self: MessageTarget - The worker's self object (or parentPort for Node.js compatibility)inputSchema: ZodType
- - Zod schema for validating incoming messagesoutputSchema: ZodType
- - Zod schema for validating outgoing messageshandlers: WorkerHelperHandlers
- - Object containing all message and error handlers
Interface defining all required handlers:
`typescript
type WorkerHelperHandlers
// Handle validated messages
handleMessage: (
data: TInput,
send: (response: TOutput) => void,
) => void | Promise
// Handle input validation errors
handleInputValidationError: (
error: ZodError
originalData: unknown,
) => void | Promise
// Handle output validation errors
handleOutputValidationError: (
error: ZodError
originalData: TOutput,
) => void | Promise
// Handle processing errors (exceptions thrown in handleMessage)
handleProcessingError: (
error: unknown,
validatedData: TInput,
) => void | Promise
};
`
All handlers support both synchronous and asynchronous operations:
`typescript
new WorkerHelper(self, InputSchema, OutputSchema, {
handleMessage: async (data, send) => {
// Perform async operations
const result = await someAsyncOperation(data);
send(result);
},
handleInputValidationError: async (error, originalData) => {
// Log to remote service
await logError(error);
self.postMessage({ type: "error", message: "Invalid input" });
},
// ... other handlers
});
`
Use discriminated unions for type-safe message routing:
`typescript
const InputSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("compute"),
operation: z.enum(["add", "subtract", "multiply", "divide"]),
operands: z.array(z.number()),
}),
z.object({
type: z.literal("status"),
}),
z.object({
type: z.literal("config"),
settings: z.record(z.string(), z.unknown()),
}),
]);
// TypeScript will narrow the type based on the discriminator
handleMessage: (data, send) => {
switch (data.type) {
case "compute":
// data is narrowed to { type: "compute", operation: ..., operands: ... }
break;
case "status":
// data is narrowed to { type: "status" }
break;
case "config":
// data is narrowed to { type: "config", settings: ... }
break;
}
};
`
You have full control over how errors are communicated back to the main thread:
`typescript
handleInputValidationError: (error, originalData) => {
// Send structured error response
self.postMessage({
type: "error",
code: "VALIDATION_ERROR",
details: error.issues,
timestamp: Date.now(),
});
},
handleProcessingError: (error, validatedData) => {
// Send error with context
self.postMessage({
type: "error",
code: "PROCESSING_ERROR",
message: error instanceof Error ? error.message : String(error),
input: validatedData.type, // Include relevant context
});
},
`
The WorkerHelper validates messages at three key points:
1. Input Validation: Before your handler receives a message, it's validated against the input schema. If validation fails, handleInputValidationError is called.
2. Output Validation: Before a message is sent from the worker, it's validated against the output schema. If validation fails, handleOutputValidationError is called.
3. Processing Errors: If your handleMessage handler throws an error, handleProcessingError is called.
All error handlers are mandatory, ensuring you handle all error cases explicitly.
1. Use Discriminated Unions: They provide type-safe message routing and better error messages.
2. Keep Schemas Strict: Use strict schemas to catch errors early.
3. Log Errors Appropriately: Use error handlers to log errors to your monitoring system.
4. Don't Swallow Errors: Always communicate errors back to the main thread in some form.
5. Test Error Cases: Use the error handlers to test how your application handles invalid inputs and processing errors.
The package includes comprehensive tests. Run them with:
`bash``
bun test
See the test files for examples of testing workers with different scenarios:
- Valid message handling
- Input validation errors
- Output validation errors
- Processing errors
- Async operations
- Edge cases
MIT
Contributions are welcome! Please feel free to submit a Pull Request.
- @firtoz/maybe-error - Type-safe error handling pattern
- @firtoz/hono-fetcher - Type-safe Hono API client
- @firtoz/websocket-do - Type-safe WebSocket Durable Objects
For issues and questions, please file an issue on GitHub.