Skip to content

Commit b0304b5

Browse files
committed
feat: boxed implementation
1 parent 2677169 commit b0304b5

17 files changed

Lines changed: 2084 additions & 155 deletions

File tree

‎packages/worker-boxed/README.md‎

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# @temporal-contract/worker-boxed
2+
3+
Type-safe Temporal worker implementation with **Result/Future pattern** from [@swan-io/boxed](https://github.com/swan-io/boxed) for explicit error handling in activities.
4+
5+
## Why worker-boxed?
6+
7+
This package extends `@temporal-contract/worker` with a functional approach to error handling using the **Result** and **Future** patterns from `@swan-io/boxed`.
8+
9+
### Benefits
10+
11+
- ✅ **Explicit error handling**: Activities return `Result<T, E>` instead of throwing
12+
- ✅ **Type-safe errors**: Error types are part of the signature
13+
- ✅ **Composable**: Use Result/Future combinators for elegant error handling
14+
- ✅ **Railway-oriented programming**: Chain operations safely
15+
- ✅ **All validation from worker**: Input/output Zod validation still included
16+
17+
## Installation
18+
19+
```bash
20+
pnpm add @temporal-contract/worker-boxed @swan-io/boxed
21+
pnpm add @temporalio/worker @temporalio/workflow zod
22+
```
23+
24+
## Usage
25+
26+
### 1. Implement Activities with Result Pattern
27+
28+
```typescript
29+
import { createBoxedActivitiesHandler, Result, Future } from '@temporal-contract/worker-boxed';
30+
import { orderContract } from './contract';
31+
32+
export const activitiesHandler = createBoxedActivitiesHandler({
33+
contract: orderContract,
34+
activities: {
35+
// ✅ Returns Result instead of throwing
36+
processPayment: (customerId, amount) => {
37+
return Future.make(async resolve => {
38+
try {
39+
const result = await paymentService.charge(customerId, amount);
40+
resolve(Result.Ok({
41+
transactionId: result.id,
42+
success: true,
43+
}));
44+
} catch (error) {
45+
// Explicit error handling
46+
resolve(Result.Error({
47+
code: 'PAYMENT_FAILED',
48+
message: error.message,
49+
details: { customerId, amount },
50+
}));
51+
}
52+
});
53+
},
54+
55+
// ✅ Railway-oriented programming
56+
validateInventory: (orderId) => {
57+
return Future.make(async resolve => {
58+
const stock = await inventory.check(orderId);
59+
60+
if (stock > 0) {
61+
resolve(Result.Ok({ available: true, quantity: stock }));
62+
} else {
63+
resolve(Result.Error({
64+
code: 'OUT_OF_STOCK',
65+
message: `Order ${orderId} is out of stock`,
66+
}));
67+
}
68+
});
69+
},
70+
},
71+
});
72+
```
73+
74+
### 2. Activities are Unwrapped in Workflows
75+
76+
In workflows, activities still throw errors (Result is automatically unwrapped), maintaining Temporal's native error handling:
77+
78+
```typescript
79+
import { createWorkflow } from '@temporal-contract/worker-boxed';
80+
81+
export const processOrder = createWorkflow({
82+
definition: orderContract.workflows.processOrder,
83+
contract: orderContract,
84+
implementation: async (context, order) => {
85+
try {
86+
// Activities throw on Error (unwrapped automatically)
87+
const payment = await context.activities.processPayment(
88+
order.customerId,
89+
order.total
90+
);
91+
92+
const inventory = await context.activities.validateInventory(order.id);
93+
94+
return {
95+
orderId: order.id,
96+
status: 'completed',
97+
transactionId: payment.transactionId,
98+
};
99+
} catch (error) {
100+
// Handle activity errors with error.code, error.details
101+
return {
102+
orderId: order.id,
103+
status: 'failed',
104+
error: error.code || 'UNKNOWN_ERROR',
105+
message: error.message,
106+
};
107+
}
108+
},
109+
});
110+
```
111+
112+
### 3. Setup Worker
113+
114+
```typescript
115+
import { Worker } from '@temporalio/worker';
116+
import { activitiesHandler } from './activities';
117+
118+
const worker = await Worker.create({
119+
workflowsPath: require.resolve('./workflows'),
120+
activities: activitiesHandler.activities,
121+
taskQueue: activitiesHandler.contract.taskQueue,
122+
});
123+
124+
await worker.run();
125+
```
126+
127+
## Error Structure
128+
129+
Activities return errors with a structured format:
130+
131+
```typescript
132+
interface ActivityError {
133+
code: string; // Error code (e.g., 'PAYMENT_FAILED')
134+
message: string; // Human-readable message
135+
details?: unknown; // Additional context
136+
}
137+
```
138+
139+
When an activity returns `Result.Error(...)`, it's automatically converted to a thrown exception in workflows with these properties accessible via `error.code`, `error.message`, and `error.details`.
140+
141+
## Pattern Comparison
142+
143+
### Standard worker (exception-based)
144+
145+
```typescript
146+
const sendEmail = async (to, subject, body) => {
147+
// Throws on error
148+
await emailService.send({ to, subject, body });
149+
return { sent: true };
150+
};
151+
```
152+
153+
### Boxed worker (Result-based)
154+
155+
```typescript
156+
const sendEmail = (to, subject, body) => {
157+
return Future.make(async resolve => {
158+
try {
159+
await emailService.send({ to, subject, body });
160+
resolve(Result.Ok({ sent: true }));
161+
} catch (error) {
162+
resolve(Result.Error({
163+
code: 'EMAIL_SEND_FAILED',
164+
message: error.message,
165+
details: error,
166+
}));
167+
}
168+
});
169+
};
170+
```
171+
172+
## Why Use This Pattern?
173+
174+
1. **Explicit errors**: Errors are part of the return type, not hidden in throws
175+
2. **Better testability**: Mock `Result.Ok` or `Result.Error` without try/catch
176+
3. **Functional composition**: Chain operations with `map`, `flatMap`, `getOr`, etc.
177+
4. **Type safety**: TypeScript knows about both success and error cases
178+
5. **Railway-oriented programming**: Build robust error handling pipelines
179+
180+
## Boxed Library Features
181+
182+
This package re-exports key utilities from [@swan-io/boxed](https://github.com/swan-io/boxed):
183+
184+
- **Result**: Success/Error sum type
185+
- **Future**: Promise wrapper with better composition
186+
- **Option**: Some/None for nullable values
187+
- **AsyncData**: Loading/Done/Error states
188+
189+
See the [boxed documentation](https://swan-io.github.io/boxed/) for full API.
190+
191+
## API
192+
193+
### `createBoxedActivitiesHandler(options)`
194+
195+
Creates an activities handler where implementations use Result pattern.
196+
197+
```typescript
198+
type BoxedActivityImplementation<T> = (
199+
...args: InferInput<T>
200+
) => Future<Result<InferOutput<T>, ActivityError>>;
201+
```
202+
203+
### `createWorkflow(options)`
204+
205+
Same as standard worker - workflows remain unchanged.
206+
207+
### `ActivityError`
208+
209+
Standard error structure for activity failures:
210+
211+
```typescript
212+
interface ActivityError {
213+
code: string;
214+
message: string;
215+
details?: unknown;
216+
}
217+
```
218+
219+
## License
220+
221+
MIT

‎packages/worker-boxed/package.json‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "@temporal-contract/worker-boxed",
3+
"version": "0.0.1",
4+
"description": "Type-safe Temporal worker with boxed Result/Future pattern for activities",
5+
"type": "module",
6+
"exports": {
7+
".": {
8+
"import": {
9+
"types": "./dist/index.d.mts",
10+
"default": "./dist/index.mjs"
11+
},
12+
"require": {
13+
"types": "./dist/index.d.cts",
14+
"default": "./dist/index.cjs"
15+
}
16+
}
17+
},
18+
"main": "./dist/index.cjs",
19+
"module": "./dist/index.mjs",
20+
"types": "./dist/index.d.ts",
21+
"files": [
22+
"dist"
23+
],
24+
"scripts": {
25+
"build": "tsdown src/index.ts --format cjs,esm --dts --clean",
26+
"typecheck": "tsc --noEmit",
27+
"dev": "tsdown src/index.ts --format cjs,esm --dts --watch"
28+
},
29+
"keywords": [
30+
"temporal",
31+
"workflow",
32+
"contract",
33+
"type-safe",
34+
"boxed",
35+
"result",
36+
"future"
37+
],
38+
"dependencies": {
39+
"@swan-io/boxed": "^3.2.1",
40+
"@temporal-contract/core": "workspace:*",
41+
"@temporalio/workflow": "catalog:",
42+
"zod": "catalog:"
43+
},
44+
"devDependencies": {
45+
"@types/node": "catalog:",
46+
"tsdown": "catalog:",
47+
"typescript": "catalog:"
48+
},
49+
"peerDependencies": {
50+
"@temporalio/worker": ">=1.0.0"
51+
}
52+
}

0 commit comments

Comments
 (0)