Skip to content
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,34 @@ const contract = defineContract({
processOrder: {
input: z.object({ orderId: z.string() }),
output: z.object({ success: z.boolean() }),
activities: { /* ... */ }
activities: {
processPayment: {
input: z.object({ orderId: z.string() }),
output: z.object({ transactionId: z.string() })
}
}
}
}
});

// Fully typed everywhere
// Implement activities with Future/Result pattern
import { declareActivitiesHandler, ActivityError } from '@temporal-contract/worker/activity';
import { Future } from '@swan-io/boxed';

const activities = declareActivitiesHandler({
contract,
activities: {
processPayment: ({ orderId }) => {
return Future.fromPromise(paymentService.process(orderId))
.mapError((error) =>
new ActivityError('PAYMENT_FAILED', 'Payment failed', error)
)
.mapOk((txId) => ({ transactionId: txId }));
}
}
});

// Call from client - fully typed everywhere
const result = await client.executeWorkflow('processOrder', {
workflowId: 'order-123',
args: { orderId: 'ORD-123' } // ✅ TypeScript knows!
Expand Down
5 changes: 3 additions & 2 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ pnpm add @temporal-contract/client @temporal-contract/contract @temporalio/clien

```typescript
import { TypedClient } from '@temporal-contract/client';
import { Connection } from '@temporalio/client';
import { Connection, Client } from '@temporalio/client';

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

// Execute workflow (fully typed!)
const result = await client.executeWorkflow('processOrder', {
Expand Down
40 changes: 23 additions & 17 deletions packages/worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,23 @@ pnpm add @temporal-contract/worker @temporal-contract/contract @temporalio/workf

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

export const activities = declareActivitiesHandler({
contract: myContract,
activities: {
sendEmail: async ({ to, body }) => ({ sent: true })
sendEmail: ({ to, body }) => {
return Future.fromPromise(emailService.send({ to, body }))
.mapError((error) =>
new ActivityError(
'EMAIL_FAILED',
error instanceof Error ? error.message : 'Failed to send email',
error
)
)
.mapOk(() => ({ sent: true }));
}
}
});

Expand All @@ -29,28 +40,23 @@ import { declareWorkflow } from '@temporal-contract/worker/workflow';
export const processOrder = declareWorkflow({
workflowName: 'processOrder',
contract: myContract,
implementation: async (context, input) => {
await context.activities.sendEmail({ to: 'user@example.com', body: 'Done!' });
implementation: async ({ activities }, input) => {
// Activities return plain values (Result is unwrapped internally)
await activities.sendEmail({ to: 'user@example.com', body: 'Done!' });
return { success: true };
}
});

// worker.ts
import { NativeConnection } from '@temporalio/worker';
import { createWorker } from '@temporal-contract/worker/worker';
import { Worker } from '@temporalio/worker';
import { activities } from './activities';
import myContract from './contract';

async function run() {
const connection = await NativeConnection.connect({
address: 'localhost:7233',
});

const worker = await createWorker({
contract: myContract,
connection,
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: myContract.taskQueue,
});

await worker.run();
Expand All @@ -70,9 +76,9 @@ import { declareWorkflow } from '@temporal-contract/worker/workflow';
export const parentWorkflow = declareWorkflow({
workflowName: 'parentWorkflow',
contract: myContract,
implementation: async (context, input) => {
implementation: async ({ executeChildWorkflow }, input) => {
// Execute child workflow from same contract and wait for result
const childResult = await context.executeChildWorkflow(myContract, 'processPayment', {
const childResult = await executeChildWorkflow(myContract, 'processPayment', {
workflowId: `payment-${input.orderId}`,
args: { amount: input.totalAmount }
});
Expand All @@ -83,13 +89,13 @@ export const parentWorkflow = declareWorkflow({
});

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

// Or start child workflow without waiting
const handleResult = await context.startChildWorkflow(myContract, 'sendEmail', {
const handleResult = await startChildWorkflow(myContract, 'sendEmail', {
workflowId: `email-${input.orderId}`,
args: { to: 'user@example.com', body: 'Order received' }
});
Expand Down
32 changes: 16 additions & 16 deletions samples/order-processing-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ This sample demonstrates that a single client can interact with any worker imple

This client package demonstrates that:

- Both `order-processing-worker` and `order-processing-worker-boxed` workers implement the **same unified contract**
- Both `order-processing-worker` and `order-processing-worker-nestjs` workers implement the **same unified contract**
Comment thread
btravers marked this conversation as resolved.
- A single client works with any worker implementation
- From the client's perspective, all workers are identical
- The only difference is in how workers handle errors internally (Promise-based vs. Result/Future pattern)
- Workers handle errors internally using the Result/Future pattern with ActivityError

## Running the Sample

Expand All @@ -34,17 +34,17 @@ pnpm install && pnpm build

1. Start a worker (choose one):

**Option A: Basic Worker**
**Option A: Standard Worker**

```bash
cd ../order-processing-worker
pnpm dev
```

**Option B: Boxed Worker**
**Option B: NestJS Worker**

```bash
cd ../order-processing-worker-boxed
cd ../order-processing-worker-nestjs
pnpm dev
```

Expand Down Expand Up @@ -89,19 +89,19 @@ The unified contract (`orderProcessingContract`) defines:

### Worker Implementations

Both workers implement the exact same contract but with different patterns:
Both workers implement the exact same contract but with different frameworks:

1. **Basic Worker** (`samples/order-processing-worker`)
1. **Standard Worker** (`samples/order-processing-worker`)
- Uses `@temporal-contract/worker`
- Promise-based activities
- Traditional error handling with try/catch
- Simpler to understand and implement

2. **Boxed Worker** (`samples/order-processing-worker-boxed`)
- Uses `@temporal-contract/worker-boxed`
- Result/Future pattern with `@temporal-contract/boxed`
- Explicit error types in function signatures
- Better type safety and functional composition
- Activities use Result/Future pattern with ActivityError
- Clean Architecture with dependency injection
- Standalone TypeScript application

2. **NestJS Worker** (`samples/order-processing-worker-nestjs`)
- Uses `@temporal-contract/worker-nestjs`
- Activities use Result/Future pattern with ActivityError
- NestJS dependency injection and decorators
- Better for NestJS-based applications

### Client Perspective

Expand Down
5 changes: 3 additions & 2 deletions samples/order-processing-worker/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Basic Order Processing
# Order Processing Worker

> Standard Promise-based workflow with Clean Architecture
> Type-safe order processing worker with Clean Architecture and Result/Future pattern

## Running

Expand All @@ -15,6 +15,7 @@ pnpm dev
## What It Demonstrates

- ✅ Type-safe contracts with Zod
- ✅ Result/Future pattern with ActivityError for explicit error handling
- ✅ Clean Architecture (Domain → Infrastructure → Application)
- ✅ Dependency injection for testability
- ✅ Error handling with compensating actions
Expand Down
65 changes: 50 additions & 15 deletions website/docs/guide/activity-handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,36 @@ Instead of defining activity implementations inline, you can extract types for r

```typescript
import type { ActivityHandlers } from '@temporal-contract/worker/activity';
import { declareActivitiesHandler, ActivityError } from '@temporal-contract/worker/activity';
import { Future, Result } from '@swan-io/boxed';
import { orderContract } from './contract';

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

// Implement activities with explicit types
const sendEmail: OrderActivityHandlers['sendEmail'] = async ({ to, body }) => {
await emailService.send({ to, body });
return { sent: true };
// Implement activities with explicit types using Future/Result pattern
const sendEmail: OrderActivityHandlers['sendEmail'] = ({ to, body }) => {
return Future.fromPromise(emailService.send({ to, body }))
.mapError((error) =>
new ActivityError(
'EMAIL_FAILED',
error instanceof Error ? error.message : 'Failed to send email',
error
)
)
.mapOk(() => ({ sent: true }));
};

const processPayment: OrderActivityHandlers['processPayment'] = async ({ amount }) => {
const txId = await paymentGateway.charge(amount);
return { transactionId: txId };
const processPayment: OrderActivityHandlers['processPayment'] = ({ amount }) => {
return Future.fromPromise(paymentGateway.charge(amount))
.mapError((error) =>
new ActivityError(
'PAYMENT_FAILED',
error instanceof Error ? error.message : 'Payment failed',
error
)
)
.mapOk((txId) => ({ transactionId: txId }));
};

// Use in handler
Expand Down Expand Up @@ -77,26 +93,44 @@ Implement activities in separate files:
```typescript
// activities/email.ts
import type { ActivityHandlers } from '@temporal-contract/worker/activity';
import { ActivityError } from '@temporal-contract/worker/activity';
import { Future, Result } from '@swan-io/boxed';
import { orderContract } from '../contracts/order.contract';

type Handlers = ActivityHandlers<typeof orderContract>;

export const sendEmail: Handlers['sendEmail'] = async ({ to, body }) => {
await emailService.send({ to, body });
return { sent: true };
export const sendEmail: Handlers['sendEmail'] = ({ to, body }) => {
return Future.fromPromise(emailService.send({ to, body }))
.mapError((error) =>
new ActivityError(
'EMAIL_FAILED',
error instanceof Error ? error.message : 'Failed to send email',
error
)
)
.mapOk(() => ({ sent: true }));
};
```

```typescript
// activities/payment.ts
import type { ActivityHandlers } from '@temporal-contract/worker/activity';
import { ActivityError } from '@temporal-contract/worker/activity';
import { Future, Result } from '@swan-io/boxed';
import { orderContract } from '../contracts/order.contract';

type Handlers = ActivityHandlers<typeof orderContract>;

export const processPayment: Handlers['processPayment'] = async ({ amount }) => {
const txId = await paymentGateway.charge(amount);
return { transactionId: txId };
export const processPayment: Handlers['processPayment'] = ({ amount }) => {
return Future.fromPromise(paymentGateway.charge(amount))
.mapError((error) =>
new ActivityError(
'PAYMENT_FAILED',
error instanceof Error ? error.message : 'Payment failed',
error
)
)
.mapOk((txId) => ({ transactionId: txId }));
};
```

Expand Down Expand Up @@ -165,13 +199,14 @@ Mock activities with correct types:

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

type Handlers = ActivityHandlers<typeof orderContract>;

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

// Use in tests
Expand Down
Loading
Loading