UNPKG

nestjs-temporal-core

Version:

Complete NestJS integration for Temporal.io with auto-discovery, declarative scheduling, enhanced monitoring, and enterprise-ready features

1,379 lines (1,083 loc) 39.1 kB
# NestJS Temporal Core <div align="center"> A comprehensive NestJS integration framework for Temporal.io that provides enterprise-ready workflow orchestration with automatic discovery, declarative decorators, and robust monitoring capabilities. ![Statements](https://img.shields.io/badge/statements-99.75%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-92.12%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-100%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-99.74%25-brightgreen.svg?style=flat) [Documentation](https://harsh-simform.github.io/nestjs-temporal-core/) • [NPM](https://www.npmjs.com/package/nestjs-temporal-core) • [GitHub](https://github.com/harsh-simform/nestjs-temporal-core) • [Example Project](https://github.com/harsh-simform/nestjs-temporal-core-example) </div> --- ## Table of Contents - [Overview](#overview) - [Features](#features) - [Installation](#installation) - [Quick Start](#quick-start) - [Module Variants](#module-variants) - [Configuration](#configuration) - [Basic Configuration](#basic-configuration) - [Multiple Workers](#multiple-workers-configuration) - [Async Configuration](#async-configuration) - [TLS Configuration](#tls-configuration-temporal-cloud) - [Core Concepts](#core-concepts) - [Activities](#activities) - [Workflows](#workflows) - [Signals and Queries](#signals-and-queries) - [API Reference](#api-reference) - [Examples](#examples) - [Advanced Usage](#advanced-usage) - [Best Practices](#best-practices) - [Health Monitoring](#health-monitoring) - [Troubleshooting](#troubleshooting) - [Migration Guide](#migration-guide) - [Contributing](#contributing) - [License](#license) ## Overview NestJS Temporal Core bridges NestJS's powerful dependency injection system with Temporal.io's robust workflow orchestration engine. It provides a declarative approach to building distributed, fault-tolerant applications with automatic service discovery, enterprise-grade monitoring, and seamless integration. ### Why NestJS Temporal Core? | Feature | Description | |---------|-------------| | **Seamless Integration** | Native NestJS decorators and dependency injection support | | **Auto-Discovery** | Automatic registration of activities and workflows via decorators | | **Type Safety** | Full TypeScript support with comprehensive type definitions | | **Enterprise Ready** | Built-in health checks, monitoring, and error handling | | **Zero Configuration** | Smart defaults with extensive customization options | | **Modular Architecture** | Use client-only, worker-only, or full-stack configurations | | **Production Grade** | Connection pooling, graceful shutdown, and fault tolerance | ## Features ### Core Capabilities - **Declarative Decorators** - Use `@Activity()` and `@ActivityMethod()` for clean, intuitive activity definitions - **Automatic Discovery** - Runtime discovery and registration of activities with zero configuration - **Schedule Management** - Programmatic schedule creation, updates, and monitoring - **Health Monitoring** - Built-in health checks and comprehensive status reporting ### Enterprise Features - **Connection Management** - Automatic connection pooling and lifecycle management - **Error Handling** - Structured error handling with detailed logging and retry policies - **Performance Monitoring** - Built-in metrics, statistics, and performance tracking - **Graceful Shutdown** - Clean resource cleanup and connection termination ### Flexibility & Scalability - **Modular Design** - Use only what you need (client-only, worker-only, or combined) - **Multiple Workers** - Support for multiple workers with different task queues - **Advanced Configuration** - Extensive customization for production environments - **TLS Support** - Secure connections for Temporal Cloud deployments [🔝 Back to top](#table-of-contents) ## Installation ```bash npm install nestjs-temporal-core @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/common ``` ### Peer Dependencies ```bash npm install @nestjs/common @nestjs/core reflect-metadata rxjs ``` [🔝 Back to top](#table-of-contents) ## Quick Start ### 1. Enable Shutdown Hooks Enable shutdown hooks in your `main.ts` for proper Temporal resource cleanup: ```typescript // main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Required for graceful Temporal connection cleanup app.enableShutdownHooks(); await app.listen(3000); } bootstrap(); ``` ### 2. Configure the Module Import and configure `TemporalModule` in your app module: ```typescript // app.module.ts import { Module } from '@nestjs/common'; import { TemporalModule } from 'nestjs-temporal-core'; import { PaymentActivity } from './activities/payment.activity'; import { EmailActivity } from './activities/email.activity'; @Module({ imports: [ TemporalModule.register({ connection: { address: 'localhost:7233', namespace: 'default', }, taskQueue: 'my-task-queue', worker: { workflowsPath: require.resolve('./workflows'), activityClasses: [PaymentActivity, EmailActivity], autoStart: true, }, }), ], providers: [PaymentActivity, EmailActivity], }) export class AppModule {} ``` ### 3. Define Activities Create activities using `@Activity()` and `@ActivityMethod()` decorators: ```typescript // payment.activity.ts import { Injectable } from '@nestjs/common'; import { Activity, ActivityMethod } from 'nestjs-temporal-core'; export interface PaymentData { amount: number; currency: string; customerId: string; } @Injectable() @Activity({ name: 'payment-activities' }) export class PaymentActivity { @ActivityMethod('processPayment') async processPayment(data: PaymentData): Promise<{ transactionId: string }> { // Payment processing logic with full NestJS DI support console.log(`Processing payment: $${data.amount} ${data.currency}`); // Simulate payment processing await new Promise(resolve => setTimeout(resolve, 1000)); return { transactionId: `txn_${Date.now()}` }; } @ActivityMethod('refundPayment') async refundPayment(transactionId: string): Promise<{ refundId: string }> { // Refund logic console.log(`Refunding transaction: ${transactionId}`); return { refundId: `ref_${Date.now()}` }; } } ``` ### 4. Define Workflows Create workflows as pure Temporal functions (NOT NestJS services): ```typescript // payment.workflow.ts import { proxyActivities, defineSignal, defineQuery, setHandler } from '@temporalio/workflow'; import type { PaymentActivity } from './payment.activity'; // Create activity proxies const { processPayment, refundPayment } = proxyActivities<typeof PaymentActivity.prototype>({ startToCloseTimeout: '5m', retry: { maximumAttempts: 3, initialInterval: '1s', }, }); // Define signals and queries export const cancelPaymentSignal = defineSignal<[string]>('cancelPayment'); export const getPaymentStatusQuery = defineQuery<string>('getPaymentStatus'); export async function processPaymentWorkflow(data: PaymentData): Promise<any> { let status = 'processing'; let transactionId: string | undefined; // Set up signal and query handlers setHandler(cancelPaymentSignal, (reason: string) => { status = 'cancelled'; }); setHandler(getPaymentStatusQuery, () => status); try { // Execute payment activity const result = await processPayment(data); transactionId = result.transactionId; status = 'completed'; return { success: true, transactionId, status, }; } catch (error) { status = 'failed'; // Compensate if needed if (transactionId) { await refundPayment(transactionId); } throw error; } } ``` ### 5. Use in Services Inject `TemporalService` to start and manage workflows: ```typescript // payment.service.ts import { Injectable } from '@nestjs/common'; import { TemporalService } from 'nestjs-temporal-core'; @Injectable() export class PaymentService { constructor(private readonly temporal: TemporalService) {} async processPayment(paymentData: any) { // Start workflow const result = await this.temporal.startWorkflow( 'processPaymentWorkflow', [paymentData], { workflowId: `payment-${Date.now()}`, taskQueue: 'my-task-queue', } ); return { workflowId: result.result.workflowId, runId: result.result.runId, }; } async checkPaymentStatus(workflowId: string) { // Query workflow const statusResult = await this.temporal.queryWorkflow( workflowId, 'getPaymentStatus' ); return { status: statusResult.result }; } async cancelPayment(workflowId: string, reason: string) { // Send signal await this.temporal.signalWorkflow( workflowId, 'cancelPayment', [reason] ); } } ``` [🔝 Back to top](#table-of-contents) ## Module Variants The package provides modular architecture with separate modules for different use cases: ### 1. Unified Module (Recommended) Complete integration with both client and worker capabilities: ```typescript import { TemporalModule } from 'nestjs-temporal-core'; TemporalModule.register({ connection: { address: 'localhost:7233' }, taskQueue: 'my-queue', worker: { workflowsPath: require.resolve('./workflows'), activityClasses: [PaymentActivity, EmailActivity], }, }) ``` ### 2. Client-Only Module For services that only need to start/query workflows: ```typescript import { TemporalClientModule } from 'nestjs-temporal-core/client'; TemporalClientModule.register({ connection: { address: 'localhost:7233' }, namespace: 'default', }) ``` ### 3. Worker-Only Module For dedicated worker processes: ```typescript import { TemporalWorkerModule } from 'nestjs-temporal-core/worker'; TemporalWorkerModule.register({ connection: { address: 'localhost:7233' }, taskQueue: 'worker-queue', worker: { workflowsPath: require.resolve('./workflows'), activityClasses: [BackgroundActivity], }, }) ``` ### 4. Activity-Only Module For standalone activity management: ```typescript import { TemporalActivityModule } from 'nestjs-temporal-core/activity'; TemporalActivityModule.register({ activityClasses: [DataProcessingActivity], }) ``` ### 5. Schedules-Only Module For managing Temporal schedules: ```typescript import { TemporalSchedulesModule } from 'nestjs-temporal-core/schedules'; TemporalSchedulesModule.register({ connection: { address: 'localhost:7233' }, }) ``` [🔝 Back to top](#table-of-contents) ## Configuration ### Basic Configuration ```typescript TemporalModule.register({ connection: { address: 'localhost:7233', namespace: 'default', }, taskQueue: 'my-task-queue', worker: { workflowsPath: require.resolve('./workflows'), activityClasses: [PaymentActivity, EmailActivity], autoStart: true, maxConcurrentActivityExecutions: 100, }, logLevel: 'info', enableLogger: true, }) ``` ### Multiple Workers Configuration **New in 3.0.12**: Support for multiple workers with different task queues in the same process. ```typescript TemporalModule.register({ connection: { address: 'localhost:7233', namespace: 'default', }, workers: [ { taskQueue: 'payments-queue', workflowsPath: require.resolve('./workflows/payments'), activityClasses: [PaymentActivity, RefundActivity], autoStart: true, workerOptions: { maxConcurrentActivityTaskExecutions: 100, }, }, { taskQueue: 'notifications-queue', workflowsPath: require.resolve('./workflows/notifications'), activityClasses: [EmailActivity, SmsActivity], autoStart: true, workerOptions: { maxConcurrentActivityTaskExecutions: 50, }, }, { taskQueue: 'background-jobs', workflowsPath: require.resolve('./workflows/jobs'), activityClasses: [DataProcessingActivity], autoStart: false, // Start manually later }, ], logLevel: 'info', enableLogger: true, }) ``` #### Accessing Multiple Workers ```typescript import { Injectable } from '@nestjs/common'; import { TemporalService } from 'nestjs-temporal-core'; @Injectable() export class WorkerManagementService { constructor(private readonly temporal: TemporalService) {} async checkWorkerStatus() { // Get all workers info const workersInfo = this.temporal.getAllWorkers(); console.log(`Total workers: ${workersInfo.totalWorkers}`); console.log(`Running workers: ${workersInfo.runningWorkers}`); // Get specific worker status const paymentWorkerStatus = this.temporal.getWorkerStatusByTaskQueue('payments-queue'); if (paymentWorkerStatus?.isHealthy) { console.log('Payment worker is healthy'); } } async controlWorkers() { // Start a specific worker await this.temporal.startWorkerByTaskQueue('background-jobs'); // Stop a specific worker await this.temporal.stopWorkerByTaskQueue('notifications-queue'); } async registerNewWorker() { // Dynamically register a new worker at runtime const result = await this.temporal.registerWorker({ taskQueue: 'new-queue', workflowsPath: require.resolve('./workflows/new'), activityClasses: [NewActivity], autoStart: true, }); if (result.success) { console.log(`Worker registered for queue: ${result.taskQueue}`); } } } ``` ### Manual Worker Creation (Advanced) For users who need full control, you can access the native Temporal connection to create custom workers: ```typescript import { Injectable, OnModuleInit } from '@nestjs/common'; import { TemporalService } from 'nestjs-temporal-core'; import { Worker } from '@temporalio/worker'; @Injectable() export class CustomWorkerService implements OnModuleInit { private customWorker: Worker; constructor(private readonly temporal: TemporalService) {} async onModuleInit() { const workerManager = this.temporal.getWorkerManager(); const connection = workerManager.getConnection(); if (!connection) { throw new Error('No connection available'); } // Create your custom worker using the native Temporal SDK this.customWorker = await Worker.create({ connection, taskQueue: 'custom-task-queue', namespace: 'default', workflowsPath: require.resolve('./workflows/custom'), activities: { myCustomActivity: async (data: string) => { return `Processed: ${data}`; }, }, }); // Start the worker await this.customWorker.run(); } } ``` ### Async Configuration For dynamic configuration using environment variables or config services: ```typescript // config/temporal.config.ts import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TemporalOptionsFactory, TemporalOptions } from 'nestjs-temporal-core'; @Injectable() export class TemporalConfigService implements TemporalOptionsFactory { constructor(private configService: ConfigService) {} createTemporalOptions(): TemporalOptions { return { connection: { address: this.configService.get('TEMPORAL_ADDRESS', 'localhost:7233'), namespace: this.configService.get('TEMPORAL_NAMESPACE', 'default'), }, taskQueue: this.configService.get('TEMPORAL_TASK_QUEUE', 'default'), worker: { workflowsPath: require.resolve('../workflows'), activityClasses: [], // Populated by module maxConcurrentActivityExecutions: 100, }, }; } } // app.module.ts import { ConfigModule } from '@nestjs/config'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), TemporalModule.registerAsync({ imports: [ConfigModule], useClass: TemporalConfigService, }), ], }) export class AppModule {} ``` ### Alternative Async Pattern (useFactory) ```typescript TemporalModule.registerAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ connection: { address: configService.get('TEMPORAL_ADDRESS', 'localhost:7233'), namespace: configService.get('TEMPORAL_NAMESPACE', 'default'), }, taskQueue: configService.get('TEMPORAL_TASK_QUEUE', 'default'), worker: { workflowsPath: require.resolve('./workflows'), activityClasses: [PaymentActivity, EmailActivity], }, }), inject: [ConfigService], }) ``` ### TLS Configuration (Temporal Cloud) For secure connections to Temporal Cloud: ```typescript import * as fs from 'fs'; TemporalModule.register({ connection: { address: 'your-namespace.your-account.tmprl.cloud:7233', namespace: 'your-namespace.your-account', tls: { clientCertPair: { crt: fs.readFileSync('/path/to/client.crt'), key: fs.readFileSync('/path/to/client.key'), }, }, }, taskQueue: 'my-task-queue', worker: { workflowsPath: require.resolve('./workflows'), activityClasses: [PaymentActivity], }, }) ``` ### Configuration Options Reference ```typescript interface TemporalOptions { // Connection settings connection: { address: string; // Temporal server address (default: 'localhost:7233') namespace?: string; // Temporal namespace (default: 'default') tls?: TLSConfig; // TLS configuration for secure connections }; // Task queue name taskQueue?: string; // Default task queue (default: 'default') // Worker configuration worker?: { workflowsPath?: string; // Path to workflow definitions (use require.resolve) activityClasses?: any[]; // Array of activity classes to register autoStart?: boolean; // Auto-start worker on module init (default: true) maxConcurrentActivityExecutions?: number; // Max concurrent activities (default: 100) maxActivitiesPerSecond?: number; // Rate limit for activities }; // Logging logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error'; // Log level (default: 'info') enableLogger?: boolean; // Enable logging (default: true) // Advanced isGlobal?: boolean; // Make module global (default: false) autoRestart?: boolean; // Auto-restart worker on failure (default: true) } ``` [🔝 Back to top](#table-of-contents) ## Core Concepts ### Activities Activities are NestJS services decorated with `@Activity()` that perform actual work. They have full access to NestJS dependency injection and can interact with external systems. **Key Points:** - Activities are NestJS services (`@Injectable()`) - Use `@Activity()` decorator at class level - Use `@ActivityMethod()` decorator for methods to be registered - Activities should be idempotent and handle retries gracefully - Full access to NestJS DI (inject services, repositories, etc.) ```typescript @Injectable() @Activity({ name: 'order-activities' }) export class OrderActivity { constructor( private readonly orderRepository: OrderRepository, private readonly emailService: EmailService, ) {} @ActivityMethod('createOrder') async createOrder(orderData: CreateOrderData): Promise<Order> { // Database operations with full DI support const order = await this.orderRepository.create(orderData); await this.emailService.sendConfirmation(order); return order; } @ActivityMethod('validateInventory') async validateInventory(items: OrderItem[]): Promise<boolean> { // Business logic with injected services return await this.orderRepository.checkInventory(items); } } ``` ### Workflows Workflows are **pure Temporal functions** (NOT NestJS services) that orchestrate activities. They must be deterministic and use Temporal's workflow APIs. **Important:** Workflows are NOT decorated with `@Injectable()` and should NOT use NestJS dependency injection. ```typescript // order.workflow.ts import { proxyActivities, defineSignal, defineQuery, setHandler } from '@temporalio/workflow'; import type { OrderActivity } from './order.activity'; // Create activity proxies with proper typing const { createOrder, validateInventory } = proxyActivities<typeof OrderActivity.prototype>({ startToCloseTimeout: '5m', retry: { maximumAttempts: 3, initialInterval: '1s', maximumInterval: '30s', }, }); // Define signals and queries at module level export const cancelOrderSignal = defineSignal<[string]>('cancelOrder'); export const getOrderStatusQuery = defineQuery<string>('getOrderStatus'); // Workflow function (exported, not a class) export async function processOrderWorkflow(orderData: CreateOrderData): Promise<OrderResult> { let status = 'pending'; // Set up signal handler setHandler(cancelOrderSignal, (reason: string) => { status = 'cancelled'; }); // Set up query handler setHandler(getOrderStatusQuery, () => status); try { // Validate inventory const isValid = await validateInventory(orderData.items); if (!isValid) { throw new Error('Insufficient inventory'); } // Create order status = 'processing'; const order = await createOrder(orderData); status = 'completed'; return { orderId: order.id, status, }; } catch (error) { status = 'failed'; throw error; } } ``` ### Signals and Queries Signals allow external systems to send events to workflows, while queries provide read-only access to workflow state. ```typescript import { defineSignal, defineQuery, setHandler, condition } from '@temporalio/workflow'; // Define at module level export const updateStatusSignal = defineSignal<[string]>('updateStatus'); export const addItemSignal = defineSignal<[Item]>('addItem'); export const getItemsQuery = defineQuery<Item[]>('getItems'); export const getStatusQuery = defineQuery<string>('getStatus'); export async function myWorkflow(): Promise<void> { let status = 'pending'; const items: Item[] = []; // Set up handlers setHandler(updateStatusSignal, (newStatus: string) => { status = newStatus; }); setHandler(addItemSignal, (item: Item) => { items.push(item); }); setHandler(getItemsQuery, () => items); setHandler(getStatusQuery, () => status); // Wait for completion signal await condition(() => status === 'completed'); } ``` ### Using Workflows in Services Inject `TemporalService` in your NestJS services to interact with workflows: ```typescript @Injectable() export class OrderService { constructor(private readonly temporal: TemporalService) {} async createOrder(orderData: CreateOrderData) { // Start workflow - note the method signature const result = await this.temporal.startWorkflow( 'processOrderWorkflow', // Workflow function name [orderData], // Arguments array { // Options workflowId: `order-${Date.now()}`, taskQueue: 'order-queue', } ); return { workflowId: result.result.workflowId, runId: result.result.runId, }; } async queryOrderStatus(workflowId: string) { const result = await this.temporal.queryWorkflow( workflowId, 'getOrderStatus' ); return result.result; } async cancelOrder(workflowId: string, reason: string) { await this.temporal.signalWorkflow( workflowId, 'cancelOrder', [reason] ); } } ``` [🔝 Back to top](#table-of-contents) ## API Reference For detailed API documentation, visit the [Full API Documentation](https://harsh-simform.github.io/nestjs-temporal-core/). ### TemporalService The main unified service providing access to all Temporal functionality. See the [API Documentation](https://harsh-simform.github.io/nestjs-temporal-core/) for complete method signatures and examples. Key methods: - `startWorkflow()` - Start a workflow execution - `signalWorkflow()` - Send a signal to a running workflow - `queryWorkflow()` - Query a running workflow - `getWorkflowHandle()` - Get a workflow handle to interact with it - `terminateWorkflow()` - Terminate a workflow execution - `cancelWorkflow()` - Cancel a workflow execution - `getHealth()` - Get service health status - `createSchedule()` - Create a schedule - `listSchedules()` - List all schedules - `deleteSchedule()` - Delete a schedule [🔝 Back to top](#table-of-contents) ## Examples ### 🚀 Example Project Check out our **[complete example repository](https://github.com/harsh-simform/nestjs-temporal-core-example)** featuring: - ✅ **Real-world implementations** - Production-ready examples - ✅ **Multiple use cases** - E-commerce, notifications, reports, and more - ✅ **Best practices** - Following all recommended patterns - ✅ **Docker setup** - Ready-to-run with docker-compose - ✅ **Test coverage** - Comprehensive test examples ### 📚 Documentation Examples For more examples, visit our [documentation](https://harsh-simform.github.io/nestjs-temporal-core/). Key example scenarios include: 1. **E-commerce Order Processing** - Complete example with compensation logic 2. **Scheduled Reports** - Creating and managing scheduled workflows 3. **Activity Retry Configuration** - Custom retry policies 4. **Child Workflows** - Organizing complex workflows 5. **Continue-As-New** - For long-running workflows 6. **Custom Error Handling** - Implementing custom error types [🔝 Back to top](#table-of-contents) ## Advanced Usage ### Activity Retry Configuration ```typescript // workflow.ts const paymentActivities = proxyActivities<typeof PaymentActivity.prototype>({ startToCloseTimeout: '5m', retry: { maximumAttempts: 5, initialInterval: '1s', maximumInterval: '1m', backoffCoefficient: 2, nonRetryableErrorTypes: ['InvalidPaymentMethod', 'InsufficientFunds'], }, }); ``` ### Workflow Testing ```typescript import { TestWorkflowEnvironment } from '@temporalio/testing'; import { Worker } from '@temporalio/worker'; import { processOrderWorkflow } from './order.workflow'; describe('Order Workflow', () => { let testEnv: TestWorkflowEnvironment; beforeAll(async () => { testEnv = await TestWorkflowEnvironment.createTimeSkipping(); }); afterAll(async () => { await testEnv?.teardown(); }); it('should process order successfully', async () => { const { client, nativeConnection } = testEnv; // Mock activities const mockOrderActivity = { validatePayment: async () => ({ valid: true }), reserveInventory: async () => ({ reservationId: 'res-123' }), chargePayment: async () => ({ transactionId: 'txn-123' }), sendConfirmationEmail: async () => {}, }; const worker = await Worker.create({ connection: nativeConnection, taskQueue: 'test', workflowsPath: require.resolve('./order.workflow'), activities: mockOrderActivity, }); await worker.runUntil(async () => { const result = await client.workflow.execute(processOrderWorkflow, { workflowId: 'test-order-1', taskQueue: 'test', args: [{ orderId: 'order-123', payment: { amount: 100, currency: 'USD' }, items: [{ id: '1', quantity: 1 }], }], }); expect(result.status).toBe('completed'); expect(result.transactionId).toBe('txn-123'); }); }); }); ``` [🔝 Back to top](#table-of-contents) ## Best Practices ### 1. Workflow Design **✅ DO:** - Keep workflows deterministic (no random numbers, current time, network calls) - Use activities for any non-deterministic operations - Keep workflow history size manageable (use continue-as-new for long-running workflows) - Export workflow functions (not classes) - Use `defineSignal` and `defineQuery` at module level **❌ DON'T:** - Don't use `@Injectable()` on workflow functions - Don't inject NestJS services in workflows - Don't use `Math.random()` or `Date.now()` directly in workflows - Don't make HTTP calls or database queries directly in workflows ### 2. Activity Design **✅ DO:** - Make activities idempotent (safe to retry) - Use `@Injectable()` and leverage NestJS DI - Use `@Activity()` and `@ActivityMethod()` decorators - Handle errors appropriately - Log activity execution for debugging **❌ DON'T:** - Don't make activities too granular (network overhead) - Don't rely on activity execution order guarantees - Don't share mutable state between activity invocations ### 3. Configuration **✅ DO:** - Use async configuration for environment-based setup - Configure appropriate timeouts for your use case - Set up proper retry policies - Enable graceful shutdown hooks - Use task queues to organize work **❌ DON'T:** - Don't hardcode connection strings - Don't use the same task queue for all workflows - Don't ignore timeout configurations ### 4. Error Handling **✅ DO:** - Implement compensation logic in workflows - Use appropriate retry policies - Log errors with context - Define non-retryable error types - Handle activity failures gracefully **❌ DON'T:** - Don't swallow errors silently - Don't retry indefinitely - Don't ignore business-level failures ### 5. Testing **✅ DO:** - Write unit tests for activities - Use TestWorkflowEnvironment for integration tests - Mock external dependencies - Test failure scenarios - Test signal and query handlers **❌ DON'T:** - Don't skip workflow testing - Don't test against production Temporal server - Don't assume workflows are correct without testing [🔝 Back to top](#table-of-contents) ## Health Monitoring The package includes comprehensive health monitoring capabilities for production deployments. ### Using Built-in Health Module ```typescript // app.module.ts import { Module } from '@nestjs/common'; import { TemporalModule } from 'nestjs-temporal-core'; import { TemporalHealthModule } from 'nestjs-temporal-core/health'; @Module({ imports: [ TemporalModule.register({ connection: { address: 'localhost:7233' }, taskQueue: 'my-queue', worker: { workflowsPath: require.resolve('./workflows'), activityClasses: [MyActivity], }, }), TemporalHealthModule, // Adds /health/temporal endpoint ], }) export class AppModule {} ``` ### Custom Health Checks ```typescript @Controller('health') export class HealthController { constructor(private readonly temporal: TemporalService) {} @Get('/status') async getHealthStatus() { const health = this.temporal.getHealth(); return { status: health.overallHealth, timestamp: new Date(), services: { client: { healthy: health.client.status === 'healthy', connection: health.client.connectionStatus, }, worker: { healthy: health.worker.status === 'healthy', state: health.worker.state, activitiesRegistered: health.worker.activitiesCount, }, discovery: { healthy: health.discovery.status === 'healthy', activitiesDiscovered: health.discovery.activitiesDiscovered, }, }, uptime: health.uptime, }; } } ``` [🔝 Back to top](#table-of-contents) ## Troubleshooting ### Common Issues and Solutions #### 1. Connection Errors **Problem:** Cannot connect to Temporal server **Solutions:** ```typescript // Check connection configuration const health = temporalService.getHealth(); console.log('Connection status:', health.client.connectionStatus); // Verify Temporal server is running // docker ps | grep temporal // Check connection settings TemporalModule.register({ connection: { address: process.env.TEMPORAL_ADDRESS || 'localhost:7233', namespace: 'default', }, }) ``` #### 2. Activity Not Found **Problem:** Workflow cannot find registered activities **Solutions:** ```typescript // 1. Ensure activity is in activityClasses array TemporalModule.register({ worker: { activityClasses: [MyActivity], // Must include the activity class }, }) // 2. Verify activity is registered as provider @Module({ providers: [MyActivity], // Must be in providers array }) // 3. Check activity decorator @Activity({ name: 'my-activities' }) export class MyActivity { @ActivityMethod('myActivity') async myActivity() { } } // 4. Check discovery status const health = temporalService.getHealth(); console.log('Activities discovered:', health.discovery.activitiesDiscovered); ``` #### 3. Workflow Registration Issues **Problem:** Workflow not found or not executing **Solutions:** ```typescript // 1. Ensure workflowsPath is correct TemporalModule.register({ worker: { workflowsPath: require.resolve('./workflows'), // Must resolve to workflows file/directory }, }) // 2. Export workflow function properly // workflows/index.ts export { processOrderWorkflow } from './order.workflow'; export { reportWorkflow } from './report.workflow'; // 3. Use correct workflow name when starting await temporal.startWorkflow( 'processOrderWorkflow', // Must match exported function name [args], options ); ``` #### 4. Timeout Issues **Problem:** Activities or workflows timing out **Solutions:** ```typescript // Configure appropriate timeouts const activities = proxyActivities<typeof MyActivity.prototype>({ startToCloseTimeout: '10m', // Increase for long-running activities scheduleToCloseTimeout: '15m', // Total time including queuing scheduleToStartTimeout: '5m', // Time waiting in queue }); // For workflows await temporal.startWorkflow('myWorkflow', [args], { workflowExecutionTimeout: '24h', // Max total execution time workflowRunTimeout: '12h', // Max single run time workflowTaskTimeout: '10s', // Decision task timeout }); ``` ### Debug Mode Enable comprehensive debugging: ```typescript TemporalModule.register({ logLevel: 'debug', enableLogger: true, connection: { address: 'localhost:7233', }, worker: { debugMode: true, // If available }, }) // Check detailed health and statistics const health = temporalService.getHealth(); const stats = temporalService.getStatistics(); console.log('Health:', JSON.stringify(health, null, 2)); console.log('Stats:', JSON.stringify(stats, null, 2)); ``` ### Getting Help If you're still experiencing issues: 1. **Check the logs** - Enable debug logging to see detailed information 2. **Verify configuration** - Double-check all connection and worker settings 3. **Test connectivity** - Ensure Temporal server is accessible 4. **Review health status** - Use `getHealth()` to identify failing components 5. **Check GitHub Issues** - [Search existing issues](https://github.com/harsh-simform/nestjs-temporal-core/issues) 6. **Create an issue** - Provide logs, configuration, and minimal reproduction [🔝 Back to top](#table-of-contents) ## Migration Guide ### Migrating to v3.0.12+ (Multiple Workers Support) Version 3.0.12 introduces support for multiple workers without breaking existing single-worker configurations. #### No Changes Required for Single Worker Your existing configuration continues to work: ```typescript // ✅ This still works exactly as before TemporalModule.register({ connection: { address: 'localhost:7233' }, taskQueue: 'my-queue', worker: { workflowsPath: require.resolve('./workflows'), activityClasses: [MyActivity], }, }) ``` #### Migrating to Multiple Workers **After (v3.0.12):** ```typescript // Option 1: Configure multiple workers in module TemporalModule.register({ connection: { address: 'localhost:7233' }, workers: [ { taskQueue: 'main-queue', workflowsPath: require.resolve('./workflows/main'), activityClasses: [MainActivity], }, { taskQueue: 'schedule-queue', workflowsPath: require.resolve('./workflows/schedule'), activityClasses: [ScheduleActivity], }, ], }) ``` #### New APIs in v3.0.12 ```typescript // Get native connection for custom worker creation const workerManager = temporal.getWorkerManager(); const connection: NativeConnection | null = workerManager.getConnection(); // Get specific worker by task queue const worker: Worker | null = temporal.getWorker('payments-queue'); // Get all workers information const workersInfo: MultipleWorkersInfo = temporal.getAllWorkers(); console.log(`${workersInfo.runningWorkers}/${workersInfo.totalWorkers} workers running`); // Control specific workers await temporal.startWorkerByTaskQueue('payments-queue'); await temporal.stopWorkerByTaskQueue('notifications-queue'); // Register new worker dynamically const result = await temporal.registerWorker({ taskQueue: 'new-queue', workflowsPath: require.resolve('./workflows/new'), activityClasses: [NewActivity], autoStart: true, }); ``` [🔝 Back to top](#table-of-contents) ## Requirements - **Node.js**: >= 16.0.0 - **NestJS**: >= 9.0.0 - **Temporal Server**: >= 1.20.0 ## Contributing We welcome contributions! To contribute: 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Make your changes 4. Run tests (`npm test`) 5. Commit your changes (`git commit -m 'Add amazing feature'`) 6. Push to the branch (`git push origin feature/amazing-feature`) 7. Open a Pull Request ### Development Setup ```bash # Clone the repository git clone https://github.com/harsh-simform/nestjs-temporal-core.git cd nestjs-temporal-core # Install dependencies npm install # Run tests npm test # Run tests with coverage npm run test:cov # Build the package npm run build # Generate documentation npm run docs:generate ``` [🔝 Back to top](#table-of-contents) ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## Support and Resources - 📚 **Documentation**: [Full API Documentation](https://harsh-simform.github.io/nestjs-temporal-core/) - 🐛 **Issues**: [GitHub Issues](https://github.com/harsh-simform/nestjs-temporal-core/issues) - 💬 **Discussions**: [GitHub Discussions](https://github.com/harsh-simform/nestjs-temporal-core/discussions) - 📦 **NPM**: [nestjs-temporal-core](https://www.npmjs.com/package/nestjs-temporal-core) - 🔄 **Changelog**: [Releases](https://github.com/harsh-simform/nestjs-temporal-core/releases) - 📖 **Example Project**: [nestjs-temporal-core-example](https://github.com/harsh-simform/nestjs-temporal-core-example) ## Related Projects - [Temporal.io](https://temporal.io/) - The underlying workflow orchestration platform - [NestJS](https://nestjs.com/) - Progressive Node.js framework - [@temporalio/sdk](https://www.npmjs.com/package/@temporalio/client) - Official Temporal TypeScript SDK --- <div align="center"> **[⭐ Star us on GitHub](https://github.com/harsh-simform/nestjs-temporal-core)** if you find this project helpful! Made with ❤️ by the Harsh Simform </div>