Skip to content

Commit f634a9d

Browse files
CopilotbtraversCopilot
authored
docs: align examples with actual Future/Result implementation patterns (#84)
* Initial plan * Fix documentation examples for Future/Result pattern usage - Update Getting Started guide with correct activity implementation patterns - Fix Activity Handlers guide to show Future/Result pattern - Update Worker Implementation guide with proper error handling examples - Correct Result Pattern guide to clarify activities vs workflows - Fix Client Usage guide to show Future/Result pattern correctly - Update package READMEs with consistent examples - Fix main README with proper example Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * Fix worker-usage guide and sample READMEs - Remove references to non-existent createWorker function - Update to use standard Worker.create from @temporalio/worker - Fix child workflow examples to show Result pattern - Update sample READMEs to reflect actual samples - Remove references to non-existent worker-boxed sample Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * Fix homepage examples and remove createWorker references - Update index.md with correct Worker.create usage - Fix client example to show Future/Result pattern - Add proper Connection setup in client example - Ensure all worker examples use standard Temporal Worker API Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * Destructure context parameter in all workflow examples Changed all workflow implementations from: implementation: async (context, input) => {} to: implementation: async ({ activities, executeChildWorkflow, ... }, input) => {} This follows the codebase convention of destructuring the context parameter directly. Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * Changes before error encountered Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * Fix formatting issues (remove trailing whitespace) Run oxfmt to fix CI formatting issues. Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * Fix worker README to destructure context parameter Updated packages/worker/README.md to use destructured context parameter pattern: - Changed from: implementation: async (context, input) => {} - Changed to: implementation: async ({ activities }, input) => {} This ensures consistency across all documentation examples. Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * Update website/docs/guide/result-pattern.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Simplify variable naming: resultFuture -> future Changed variable name from 'resultFuture' to 'future' across all documentation to avoid redundancy since it's already clear from context that it's a Future. Updated files: - website/docs/index.md (2 instances) - website/docs/guide/getting-started.md - website/docs/guide/client-usage.md Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> Co-authored-by: Benoit Travers <benoit.travers.fr@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 8c4d56a commit f634a9d

13 files changed

Lines changed: 470 additions & 254 deletions

File tree

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,34 @@ const contract = defineContract({
3636
processOrder: {
3737
input: z.object({ orderId: z.string() }),
3838
output: z.object({ success: z.boolean() }),
39-
activities: { /* ... */ }
39+
activities: {
40+
processPayment: {
41+
input: z.object({ orderId: z.string() }),
42+
output: z.object({ transactionId: z.string() })
43+
}
44+
}
4045
}
4146
}
4247
});
4348

44-
// Fully typed everywhere
49+
// Implement activities with Future/Result pattern
50+
import { declareActivitiesHandler, ActivityError } from '@temporal-contract/worker/activity';
51+
import { Future } from '@swan-io/boxed';
52+
53+
const activities = declareActivitiesHandler({
54+
contract,
55+
activities: {
56+
processPayment: ({ orderId }) => {
57+
return Future.fromPromise(paymentService.process(orderId))
58+
.mapError((error) =>
59+
new ActivityError('PAYMENT_FAILED', 'Payment failed', error)
60+
)
61+
.mapOk((txId) => ({ transactionId: txId }));
62+
}
63+
}
64+
});
65+
66+
// Call from client - fully typed everywhere
4567
const result = await client.executeWorkflow('processOrder', {
4668
workflowId: 'order-123',
4769
args: { orderId: 'ORD-123' } // ✅ TypeScript knows!

packages/client/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ pnpm add @temporal-contract/client @temporal-contract/contract @temporalio/clien
1414

1515
```typescript
1616
import { TypedClient } from '@temporal-contract/client';
17-
import { Connection } from '@temporalio/client';
17+
import { Connection, Client } from '@temporalio/client';
1818

1919
const connection = await Connection.connect({ address: 'localhost:7233' });
20-
const client = TypedClient.create(myContract, { connection });
20+
const temporalClient = new Client({ connection });
21+
const client = TypedClient.create(myContract, temporalClient);
2122

2223
// Execute workflow (fully typed!)
2324
const result = await client.executeWorkflow('processOrder', {

packages/worker/README.md

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,23 @@ pnpm add @temporal-contract/worker @temporal-contract/contract @temporalio/workf
1414

1515
```typescript
1616
// activities.ts
17-
import { declareActivitiesHandler } from '@temporal-contract/worker/activity';
17+
import { declareActivitiesHandler, ActivityError } from '@temporal-contract/worker/activity';
18+
import { Future, Result } from '@swan-io/boxed';
1819

1920
export const activities = declareActivitiesHandler({
2021
contract: myContract,
2122
activities: {
22-
sendEmail: async ({ to, body }) => ({ sent: true })
23+
sendEmail: ({ to, body }) => {
24+
return Future.fromPromise(emailService.send({ to, body }))
25+
.mapError((error) =>
26+
new ActivityError(
27+
'EMAIL_FAILED',
28+
error instanceof Error ? error.message : 'Failed to send email',
29+
error
30+
)
31+
)
32+
.mapOk(() => ({ sent: true }));
33+
}
2334
}
2435
});
2536

@@ -29,28 +40,23 @@ import { declareWorkflow } from '@temporal-contract/worker/workflow';
2940
export const processOrder = declareWorkflow({
3041
workflowName: 'processOrder',
3142
contract: myContract,
32-
implementation: async (context, input) => {
33-
await context.activities.sendEmail({ to: 'user@example.com', body: 'Done!' });
43+
implementation: async ({ activities }, input) => {
44+
// Activities return plain values (Result is unwrapped internally)
45+
await activities.sendEmail({ to: 'user@example.com', body: 'Done!' });
3446
return { success: true };
3547
}
3648
});
3749

3850
// worker.ts
39-
import { NativeConnection } from '@temporalio/worker';
40-
import { createWorker } from '@temporal-contract/worker/worker';
51+
import { Worker } from '@temporalio/worker';
4152
import { activities } from './activities';
4253
import myContract from './contract';
4354

4455
async function run() {
45-
const connection = await NativeConnection.connect({
46-
address: 'localhost:7233',
47-
});
48-
49-
const worker = await createWorker({
50-
contract: myContract,
51-
connection,
56+
const worker = await Worker.create({
5257
workflowsPath: require.resolve('./workflows'),
5358
activities,
59+
taskQueue: myContract.taskQueue,
5460
});
5561

5662
await worker.run();
@@ -70,9 +76,9 @@ import { declareWorkflow } from '@temporal-contract/worker/workflow';
7076
export const parentWorkflow = declareWorkflow({
7177
workflowName: 'parentWorkflow',
7278
contract: myContract,
73-
implementation: async (context, input) => {
79+
implementation: async ({ executeChildWorkflow }, input) => {
7480
// Execute child workflow from same contract and wait for result
75-
const childResult = await context.executeChildWorkflow(myContract, 'processPayment', {
81+
const childResult = await executeChildWorkflow(myContract, 'processPayment', {
7682
workflowId: `payment-${input.orderId}`,
7783
args: { amount: input.totalAmount }
7884
});
@@ -83,13 +89,13 @@ export const parentWorkflow = declareWorkflow({
8389
});
8490

8591
// Execute child workflow from another contract (another worker)
86-
const notificationResult = await context.executeChildWorkflow(notificationContract, 'sendNotification', {
92+
const notificationResult = await executeChildWorkflow(notificationContract, 'sendNotification', {
8793
workflowId: `notification-${input.orderId}`,
8894
args: { message: 'Order received' }
8995
});
9096

9197
// Or start child workflow without waiting
92-
const handleResult = await context.startChildWorkflow(myContract, 'sendEmail', {
98+
const handleResult = await startChildWorkflow(myContract, 'sendEmail', {
9399
workflowId: `email-${input.orderId}`,
94100
args: { to: 'user@example.com', body: 'Order received' }
95101
});

samples/order-processing-client/README.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ This sample demonstrates that a single client can interact with any worker imple
88

99
This client package demonstrates that:
1010

11-
- Both `order-processing-worker` and `order-processing-worker-boxed` workers implement the **same unified contract**
11+
- Both `order-processing-worker` and `order-processing-worker-nestjs` workers implement the **same unified contract**
1212
- A single client works with any worker implementation
1313
- From the client's perspective, all workers are identical
14-
- The only difference is in how workers handle errors internally (Promise-based vs. Result/Future pattern)
14+
- Workers handle errors internally using the Result/Future pattern with ActivityError
1515

1616
## Running the Sample
1717

@@ -34,17 +34,17 @@ pnpm install && pnpm build
3434

3535
1. Start a worker (choose one):
3636

37-
**Option A: Basic Worker**
37+
**Option A: Standard Worker**
3838

3939
```bash
4040
cd ../order-processing-worker
4141
pnpm dev
4242
```
4343

44-
**Option B: Boxed Worker**
44+
**Option B: NestJS Worker**
4545

4646
```bash
47-
cd ../order-processing-worker-boxed
47+
cd ../order-processing-worker-nestjs
4848
pnpm dev
4949
```
5050

@@ -89,19 +89,19 @@ The unified contract (`orderProcessingContract`) defines:
8989

9090
### Worker Implementations
9191

92-
Both workers implement the exact same contract but with different patterns:
92+
Both workers implement the exact same contract but with different frameworks:
9393

94-
1. **Basic Worker** (`samples/order-processing-worker`)
94+
1. **Standard Worker** (`samples/order-processing-worker`)
9595
- Uses `@temporal-contract/worker`
96-
- Promise-based activities
97-
- Traditional error handling with try/catch
98-
- Simpler to understand and implement
99-
100-
2. **Boxed Worker** (`samples/order-processing-worker-boxed`)
101-
- Uses `@temporal-contract/worker-boxed`
102-
- Result/Future pattern with `@temporal-contract/boxed`
103-
- Explicit error types in function signatures
104-
- Better type safety and functional composition
96+
- Activities use Result/Future pattern with ActivityError
97+
- Clean Architecture with dependency injection
98+
- Standalone TypeScript application
99+
100+
2. **NestJS Worker** (`samples/order-processing-worker-nestjs`)
101+
- Uses `@temporal-contract/worker-nestjs`
102+
- Activities use Result/Future pattern with ActivityError
103+
- NestJS dependency injection and decorators
104+
- Better for NestJS-based applications
105105

106106
### Client Perspective
107107

samples/order-processing-worker/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Basic Order Processing
1+
# Order Processing Worker
22

3-
> Standard Promise-based workflow with Clean Architecture
3+
> Type-safe order processing worker with Clean Architecture and Result/Future pattern
44
55
## Running
66

@@ -15,6 +15,7 @@ pnpm dev
1515
## What It Demonstrates
1616

1717
- ✅ Type-safe contracts with Zod
18+
- ✅ Result/Future pattern with ActivityError for explicit error handling
1819
- ✅ Clean Architecture (Domain → Infrastructure → Application)
1920
- ✅ Dependency injection for testability
2021
- ✅ Error handling with compensating actions

website/docs/guide/activity-handlers.md

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,36 @@ Instead of defining activity implementations inline, you can extract types for r
1212

1313
```typescript
1414
import type { ActivityHandlers } from '@temporal-contract/worker/activity';
15+
import { declareActivitiesHandler, ActivityError } from '@temporal-contract/worker/activity';
16+
import { Future, Result } from '@swan-io/boxed';
1517
import { orderContract } from './contract';
1618

1719
// Extract all activity handler types from contract
1820
type OrderActivityHandlers = ActivityHandlers<typeof orderContract>;
1921

20-
// Implement activities with explicit types
21-
const sendEmail: OrderActivityHandlers['sendEmail'] = async ({ to, body }) => {
22-
await emailService.send({ to, body });
23-
return { sent: true };
22+
// Implement activities with explicit types using Future/Result pattern
23+
const sendEmail: OrderActivityHandlers['sendEmail'] = ({ to, body }) => {
24+
return Future.fromPromise(emailService.send({ to, body }))
25+
.mapError((error) =>
26+
new ActivityError(
27+
'EMAIL_FAILED',
28+
error instanceof Error ? error.message : 'Failed to send email',
29+
error
30+
)
31+
)
32+
.mapOk(() => ({ sent: true }));
2433
};
2534

26-
const processPayment: OrderActivityHandlers['processPayment'] = async ({ amount }) => {
27-
const txId = await paymentGateway.charge(amount);
28-
return { transactionId: txId };
35+
const processPayment: OrderActivityHandlers['processPayment'] = ({ amount }) => {
36+
return Future.fromPromise(paymentGateway.charge(amount))
37+
.mapError((error) =>
38+
new ActivityError(
39+
'PAYMENT_FAILED',
40+
error instanceof Error ? error.message : 'Payment failed',
41+
error
42+
)
43+
)
44+
.mapOk((txId) => ({ transactionId: txId }));
2945
};
3046

3147
// Use in handler
@@ -77,26 +93,44 @@ Implement activities in separate files:
7793
```typescript
7894
// activities/email.ts
7995
import type { ActivityHandlers } from '@temporal-contract/worker/activity';
96+
import { ActivityError } from '@temporal-contract/worker/activity';
97+
import { Future, Result } from '@swan-io/boxed';
8098
import { orderContract } from '../contracts/order.contract';
8199

82100
type Handlers = ActivityHandlers<typeof orderContract>;
83101

84-
export const sendEmail: Handlers['sendEmail'] = async ({ to, body }) => {
85-
await emailService.send({ to, body });
86-
return { sent: true };
102+
export const sendEmail: Handlers['sendEmail'] = ({ to, body }) => {
103+
return Future.fromPromise(emailService.send({ to, body }))
104+
.mapError((error) =>
105+
new ActivityError(
106+
'EMAIL_FAILED',
107+
error instanceof Error ? error.message : 'Failed to send email',
108+
error
109+
)
110+
)
111+
.mapOk(() => ({ sent: true }));
87112
};
88113
```
89114

90115
```typescript
91116
// activities/payment.ts
92117
import type { ActivityHandlers } from '@temporal-contract/worker/activity';
118+
import { ActivityError } from '@temporal-contract/worker/activity';
119+
import { Future, Result } from '@swan-io/boxed';
93120
import { orderContract } from '../contracts/order.contract';
94121

95122
type Handlers = ActivityHandlers<typeof orderContract>;
96123

97-
export const processPayment: Handlers['processPayment'] = async ({ amount }) => {
98-
const txId = await paymentGateway.charge(amount);
99-
return { transactionId: txId };
124+
export const processPayment: Handlers['processPayment'] = ({ amount }) => {
125+
return Future.fromPromise(paymentGateway.charge(amount))
126+
.mapError((error) =>
127+
new ActivityError(
128+
'PAYMENT_FAILED',
129+
error instanceof Error ? error.message : 'Payment failed',
130+
error
131+
)
132+
)
133+
.mapOk((txId) => ({ transactionId: txId }));
100134
};
101135
```
102136

@@ -165,13 +199,14 @@ Mock activities with correct types:
165199

166200
```typescript
167201
import type { ActivityHandlers } from '@temporal-contract/worker/activity';
202+
import { Future, Result } from '@swan-io/boxed';
168203

169204
type Handlers = ActivityHandlers<typeof orderContract>;
170205

171206
// Create mock activities for testing
172207
const mockActivities: Handlers = {
173-
sendEmail: async ({ to, body }) => ({ sent: true }),
174-
processPayment: async ({ amount }) => ({ transactionId: 'TEST-TXN' })
208+
sendEmail: ({ to, body }) => Future.value(Result.Ok({ sent: true })),
209+
processPayment: ({ amount }) => Future.value(Result.Ok({ transactionId: 'TEST-TXN' }))
175210
};
176211

177212
// Use in tests

0 commit comments

Comments
 (0)