Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Welcome to the temporal-contract API documentation. This documentation is auto-g
- [@temporal-contract/client-nestjs](./client-nestjs/) - NestJS client module
- [@temporal-contract/worker-nestjs](./worker-nestjs/) - NestJS worker module

## EffectTS Integration

- [@temporal-contract/client-effect](./client-effect/) - EffectTS client module
- [@temporal-contract/contract-effect](./contract-effect/) - EffectTS contract module
- [@temporal-contract/worker-effect](./worker-effect/) - EffectTS worker module

## Testing

- [@temporal-contract/testing](./testing/) - Testing utilities with testcontainers
3 changes: 3 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
"dependencies": {
"@temporal-contract/boxed": "workspace:*",
"@temporal-contract/client": "workspace:*",
"@temporal-contract/client-effect": "workspace:*",
"@temporal-contract/client-nestjs": "workspace:*",
"@temporal-contract/contract": "workspace:*",
"@temporal-contract/contract-effect": "workspace:*",
"@temporal-contract/testing": "workspace:*",
"@temporal-contract/worker": "workspace:*",
"@temporal-contract/worker-effect": "workspace:*",
"@temporal-contract/worker-nestjs": "workspace:*"
},
"devDependencies": {
Expand Down
6 changes: 6 additions & 0 deletions docs/scripts/copy-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import "@temporal-contract/contract";
import "@temporal-contract/testing/global-setup";
import "@temporal-contract/worker/activity";
import "@temporal-contract/worker-nestjs";
import "@temporal-contract/contract-effect";
import "@temporal-contract/client-effect";
import "@temporal-contract/worker-effect";

import { cp, mkdir, rm } from "node:fs/promises";
import { dirname, join } from "node:path";
Expand All @@ -20,10 +23,13 @@ const packages = [
"boxed",
"client",
"client-nestjs",
"client-effect",
"contract",
"contract-effect",
"testing",
"worker",
"worker-nestjs",
"worker-effect",
];

async function copyDocs(): Promise<void> {
Expand Down
30 changes: 30 additions & 0 deletions examples/order-processing-worker-effect/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@temporal-contract/sample-order-processing-worker-effect",
"private": true,
"description": "Effect-native order processing worker using temporal-contract",
"type": "module",
"scripts": {
"test:integration": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@temporal-contract/contract-effect": "workspace:*",
"@temporal-contract/worker-effect": "workspace:*",
"@temporalio/worker": "catalog:",
"@temporalio/workflow": "catalog:",
"effect": "catalog:",
"pino": "catalog:",
"pino-pretty": "catalog:"
},
"devDependencies": {
"@temporal-contract/client-effect": "workspace:*",
"@temporal-contract/testing": "workspace:*",
"@temporal-contract/tsconfig": "workspace:*",
"@temporalio/client": "catalog:",
"@types/node": "catalog:",
"@vitest/coverage-v8": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Effect } from "effect";
import { declareActivitiesHandler, ActivityError } from "@temporal-contract/worker-effect";
import { orderEffectContract } from "../contract.js";
import { logger } from "../logger.js";
import {
processPaymentUseCase,
reserveInventoryUseCase,
releaseInventoryUseCase,
createShipmentUseCase,
sendNotificationUseCase,
refundPaymentUseCase,
} from "../dependencies.js";

export const activities = declareActivitiesHandler({
contract: orderEffectContract,
activities: {
log: ({ level, message }) =>
Effect.sync(() => {
logger.info({ level }, message);
}),

sendNotification: ({ customerId, subject, message }) =>
Effect.tryPromise({
try: () => sendNotificationUseCase.execute(customerId, subject, message),
catch: (error) =>
new ActivityError({
code: "NOTIFICATION_FAILED",
message: error instanceof Error ? error.message : "Failed to send notification",
cause: error,
}),
}),

processOrder: {
processPayment: ({ customerId, amount }) =>
Effect.tryPromise({
try: () => processPaymentUseCase.execute(customerId, amount),
catch: (error) =>
new ActivityError({
code: "PAYMENT_FAILED",
message: error instanceof Error ? error.message : "Payment processing failed",
cause: error,
}),
}),

reserveInventory: (items) =>
Effect.tryPromise({
try: () => reserveInventoryUseCase.execute([...items]),
catch: (error) =>
new ActivityError({
code: "INVENTORY_RESERVATION_FAILED",
message: error instanceof Error ? error.message : "Inventory reservation failed",
cause: error,
}),
}),

releaseInventory: (reservationId) =>
Effect.tryPromise({
try: () => releaseInventoryUseCase.execute(reservationId),
catch: (error) =>
new ActivityError({
code: "INVENTORY_RELEASE_FAILED",
message: error instanceof Error ? error.message : "Inventory release failed",
cause: error,
}),
}),

createShipment: ({ orderId, customerId }) =>
Effect.tryPromise({
try: () => createShipmentUseCase.execute(orderId, customerId),
catch: (error) =>
new ActivityError({
code: "SHIPMENT_CREATION_FAILED",
message: error instanceof Error ? error.message : "Shipment creation failed",
cause: error,
}),
}),

refundPayment: (transactionId) =>
Effect.tryPromise({
try: () => refundPaymentUseCase.execute(transactionId),
catch: (error) =>
new ActivityError({
code: "REFUND_FAILED",
message: error instanceof Error ? error.message : "Refund failed",
cause: error,
}),
}),
},
},
});
142 changes: 142 additions & 0 deletions examples/order-processing-worker-effect/src/application/workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { proxyActivities } from "@temporalio/workflow";

type LogLevel = "fatal" | "error" | "warn" | "info" | "debug" | "trace";

type OrderItem = { productId: string; quantity: number; price: number };

type Order = {
orderId: string;
customerId: string;
items: OrderItem[];
totalAmount: number;
};

type PaymentResult =
| { status: "success"; transactionId: string; paidAmount: number }
| { status: "failed" };

type InventoryReservation = { reserved: boolean; reservationId?: string };

type ShippingResult = { trackingNumber: string; estimatedDelivery: string };

type OrderResult = {
orderId: string;
status: "completed" | "failed";
transactionId?: string;
trackingNumber?: string;
failureReason?: string;
errorCode?: string;
};

type Activities = {
log(args: { level: LogLevel; message: string }): Promise<void>;
sendNotification(args: { customerId: string; subject: string; message: string }): Promise<void>;
processPayment(args: { customerId: string; amount: number }): Promise<PaymentResult>;
reserveInventory(items: OrderItem[]): Promise<InventoryReservation>;
releaseInventory(reservationId: string): Promise<void>;
createShipment(args: { orderId: string; customerId: string }): Promise<ShippingResult>;
refundPayment(transactionId: string): Promise<void>;
};

const activities = proxyActivities<Activities>({
startToCloseTimeout: "1 minute",
});

export async function processOrder(order: Order): Promise<OrderResult> {
let paymentTransactionId: string | undefined;

await activities.log({
level: "info",
message: `Starting order processing for ${order.orderId}`,
});

await activities.log({
level: "info",
message: `Processing payment of $${order.totalAmount}`,
});

const paymentResult = await activities.processPayment({
customerId: order.customerId,
amount: order.totalAmount,
});

if (paymentResult.status === "failed") {
await activities.log({ level: "error", message: "Payment failed: Card declined" });

await activities.sendNotification({
customerId: order.customerId,
subject: "Order Failed",
message: `Your order ${order.orderId} could not be processed. Payment was declined.`,
});

return {
orderId: order.orderId,
status: "failed" as const,
failureReason: "Payment was declined",
errorCode: "PAYMENT_FAILED",
};
}

paymentTransactionId = paymentResult.transactionId;
await activities.log({ level: "info", message: `Payment successful: ${paymentTransactionId}` });

await activities.log({ level: "info", message: "Reserving inventory" });
const inventoryResult = await activities.reserveInventory(order.items);

if (!inventoryResult.reserved) {
await activities.log({ level: "error", message: "Inventory reservation failed" });
await activities.log({ level: "info", message: "Rolling back: refunding payment" });
await activities.refundPayment(paymentTransactionId);

await activities.sendNotification({
customerId: order.customerId,
subject: "Order Failed",
message: `Your order ${order.orderId} could not be processed. Items out of stock. Payment refunded.`,
});

return {
orderId: order.orderId,
status: "failed" as const,
failureReason: "One or more items are out of stock",
errorCode: "OUT_OF_STOCK",
};
}

await activities.log({
level: "info",
message: `Inventory reserved: ${inventoryResult.reservationId}`,
});

await activities.log({ level: "info", message: "Creating shipment" });
const shippingResult = await activities.createShipment({
orderId: order.orderId,
customerId: order.customerId,
});

await activities.log({
level: "info",
message: `Shipment created: ${shippingResult.trackingNumber}`,
});

try {
await activities.sendNotification({
customerId: order.customerId,
subject: "Order Confirmed",
message: `Your order ${order.orderId} is confirmed. Tracking: ${shippingResult.trackingNumber}`,
});
} catch (error) {
await activities.log({
level: "warn",
message: `Failed to send confirmation: ${error}`,
});
}

await activities.log({ level: "info", message: `Order ${order.orderId} processed successfully` });

return {
orderId: order.orderId,
status: "completed" as const,
transactionId: paymentTransactionId,
trackingNumber: shippingResult.trackingNumber,
};
}
Loading