nestjs-temporal-core 3.0.10 → 3.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1451 -721
- package/dist/constants.d.ts +49 -151
- package/dist/constants.js +38 -141
- package/dist/constants.js.map +1 -1
- package/dist/decorators/activity.decorator.js +75 -15
- package/dist/decorators/activity.decorator.js.map +1 -1
- package/dist/decorators/index.d.ts +1 -3
- package/dist/decorators/index.js +4 -13
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/workflow.decorator.d.ts +7 -3
- package/dist/decorators/workflow.decorator.js +161 -48
- package/dist/decorators/workflow.decorator.js.map +1 -1
- package/dist/health/temporal-health.controller.d.ts +7 -0
- package/dist/health/temporal-health.controller.js +77 -0
- package/dist/health/temporal-health.controller.js.map +1 -0
- package/dist/health/temporal-health.module.d.ts +2 -0
- package/dist/health/temporal-health.module.js +20 -0
- package/dist/health/temporal-health.module.js.map +1 -0
- package/dist/index.d.ts +10 -20
- package/dist/index.js +15 -30
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +740 -160
- package/dist/interfaces.js +1 -2
- package/dist/interfaces.js.map +1 -1
- package/dist/providers/temporal-connection.factory.d.ts +28 -0
- package/dist/providers/temporal-connection.factory.js +194 -0
- package/dist/providers/temporal-connection.factory.js.map +1 -0
- package/dist/services/temporal-client.service.d.ts +33 -0
- package/dist/services/temporal-client.service.js +285 -0
- package/dist/services/temporal-client.service.js.map +1 -0
- package/dist/services/temporal-discovery.service.d.ts +34 -0
- package/dist/services/temporal-discovery.service.js +348 -0
- package/dist/services/temporal-discovery.service.js.map +1 -0
- package/dist/services/temporal-metadata.service.d.ts +37 -0
- package/dist/services/temporal-metadata.service.js +512 -0
- package/dist/services/temporal-metadata.service.js.map +1 -0
- package/dist/services/temporal-schedule.service.d.ts +35 -0
- package/dist/services/temporal-schedule.service.js +380 -0
- package/dist/services/temporal-schedule.service.js.map +1 -0
- package/dist/services/temporal-worker.service.d.ts +54 -0
- package/dist/services/temporal-worker.service.js +605 -0
- package/dist/services/temporal-worker.service.js.map +1 -0
- package/dist/services/temporal.service.d.ts +85 -0
- package/dist/services/temporal.service.js +572 -0
- package/dist/services/temporal.service.js.map +1 -0
- package/dist/temporal.module.d.ts +6 -9
- package/dist/temporal.module.js +160 -109
- package/dist/temporal.module.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +5 -8
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +10 -4
- package/dist/utils/logger.js +77 -106
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/metadata.d.ts +1 -3
- package/dist/utils/metadata.js +8 -18
- package/dist/utils/metadata.js.map +1 -1
- package/dist/utils/validation.d.ts +16 -2
- package/dist/utils/validation.js +103 -9
- package/dist/utils/validation.js.map +1 -1
- package/package.json +37 -26
- package/dist/activity/index.d.ts +0 -2
- package/dist/activity/index.js +0 -19
- package/dist/activity/index.js.map +0 -1
- package/dist/activity/temporal-activity.module.d.ts +0 -11
- package/dist/activity/temporal-activity.module.js +0 -52
- package/dist/activity/temporal-activity.module.js.map +0 -1
- package/dist/activity/temporal-activity.service.d.ts +0 -46
- package/dist/activity/temporal-activity.service.js +0 -192
- package/dist/activity/temporal-activity.service.js.map +0 -1
- package/dist/client/index.d.ts +0 -3
- package/dist/client/index.js +0 -20
- package/dist/client/index.js.map +0 -1
- package/dist/client/temporal-client.module.d.ts +0 -18
- package/dist/client/temporal-client.module.js +0 -198
- package/dist/client/temporal-client.module.js.map +0 -1
- package/dist/client/temporal-client.service.d.ts +0 -35
- package/dist/client/temporal-client.service.js +0 -187
- package/dist/client/temporal-client.service.js.map +0 -1
- package/dist/client/temporal-schedule.service.d.ts +0 -41
- package/dist/client/temporal-schedule.service.js +0 -204
- package/dist/client/temporal-schedule.service.js.map +0 -1
- package/dist/decorators/parameter.decorator.d.ts +0 -5
- package/dist/decorators/parameter.decorator.js +0 -57
- package/dist/decorators/parameter.decorator.js.map +0 -1
- package/dist/decorators/scheduling.decorator.d.ts +0 -4
- package/dist/decorators/scheduling.decorator.js +0 -44
- package/dist/decorators/scheduling.decorator.js.map +0 -1
- package/dist/discovery/index.d.ts +0 -2
- package/dist/discovery/index.js +0 -19
- package/dist/discovery/index.js.map +0 -1
- package/dist/discovery/temporal-discovery.service.d.ts +0 -39
- package/dist/discovery/temporal-discovery.service.js +0 -191
- package/dist/discovery/temporal-discovery.service.js.map +0 -1
- package/dist/discovery/temporal-schedule-manager.service.d.ts +0 -41
- package/dist/discovery/temporal-schedule-manager.service.js +0 -238
- package/dist/discovery/temporal-schedule-manager.service.js.map +0 -1
- package/dist/schedules/index.d.ts +0 -2
- package/dist/schedules/index.js +0 -19
- package/dist/schedules/index.js.map +0 -1
- package/dist/schedules/temporal-schedules.module.d.ts +0 -11
- package/dist/schedules/temporal-schedules.module.js +0 -55
- package/dist/schedules/temporal-schedules.module.js.map +0 -1
- package/dist/schedules/temporal-schedules.service.d.ts +0 -52
- package/dist/schedules/temporal-schedules.service.js +0 -221
- package/dist/schedules/temporal-schedules.service.js.map +0 -1
- package/dist/temporal.service.d.ts +0 -77
- package/dist/temporal.service.js +0 -243
- package/dist/temporal.service.js.map +0 -1
- package/dist/worker/index.d.ts +0 -3
- package/dist/worker/index.js +0 -20
- package/dist/worker/index.js.map +0 -1
- package/dist/worker/temporal-metadata.accessor.d.ts +0 -32
- package/dist/worker/temporal-metadata.accessor.js +0 -208
- package/dist/worker/temporal-metadata.accessor.js.map +0 -1
- package/dist/worker/temporal-worker-manager.service.d.ts +0 -49
- package/dist/worker/temporal-worker-manager.service.js +0 -389
- package/dist/worker/temporal-worker-manager.service.js.map +0 -1
- package/dist/worker/temporal-worker.module.d.ts +0 -18
- package/dist/worker/temporal-worker.module.js +0 -156
- package/dist/worker/temporal-worker.module.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,85 +1,100 @@
|
|
|
1
1
|
# NestJS Temporal Core
|
|
2
2
|
|
|
3
|
-
A comprehensive NestJS integration for
|
|
3
|
+
A comprehensive NestJS integration framework for Temporal.io that provides enterprise-ready workflow orchestration with automatic discovery, declarative decorators, and robust monitoring capabilities.
|
|
4
4
|
|
|
5
5
|
[](https://badge.fury.io/js/nestjs-temporal-core)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[](http://www.typescriptlang.org/)
|
|
8
7
|
|
|
9
|
-
##
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
- [Overview](#overview)
|
|
11
|
+
- [Features](#features)
|
|
12
|
+
- [Installation](#installation)
|
|
13
|
+
- [Quick Start](#quick-start)
|
|
14
|
+
- [Module Variants](#module-variants)
|
|
15
|
+
- [Configuration](#configuration)
|
|
16
|
+
- [Core Concepts](#core-concepts)
|
|
17
|
+
- [API Reference](#api-reference)
|
|
18
|
+
- [Examples](#examples)
|
|
19
|
+
- [Advanced Usage](#advanced-usage)
|
|
20
|
+
- [Health Monitoring](#health-monitoring)
|
|
21
|
+
- [Best Practices](#best-practices)
|
|
22
|
+
- [Troubleshooting](#troubleshooting)
|
|
23
|
+
- [Contributing](#contributing)
|
|
24
|
+
- [License](#license)
|
|
10
25
|
|
|
11
|
-
|
|
26
|
+
## Overview
|
|
12
27
|
|
|
13
|
-
|
|
28
|
+
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.
|
|
14
29
|
|
|
15
|
-
|
|
30
|
+
### Key Benefits
|
|
16
31
|
|
|
17
|
-
|
|
32
|
+
- 🚀 **Seamless Integration**: Native NestJS decorators and dependency injection
|
|
33
|
+
- 🔍 **Auto-Discovery**: Automatic registration of activities via decorators
|
|
34
|
+
- 🛡️ **Type Safety**: Full TypeScript support with comprehensive type definitions
|
|
35
|
+
- 🏥 **Enterprise Ready**: Built-in health checks, monitoring, and error handling
|
|
36
|
+
- ⚙️ **Zero Configuration**: Smart defaults with extensive customization options
|
|
37
|
+
- 📦 **Modular Architecture**: Separate modules for client, worker, activities, and schedules
|
|
38
|
+
- 🔄 **Production Grade**: Connection pooling, graceful shutdown, and fault tolerance
|
|
18
39
|
|
|
19
|
-
|
|
40
|
+
## Features
|
|
20
41
|
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
42
|
+
- ✨ **Declarative Decorators**: `@Activity()` and `@ActivityMethod()` for clean activity definitions
|
|
43
|
+
- 🔎 **Automatic Discovery**: Runtime discovery and registration of activities
|
|
44
|
+
- 📅 **Schedule Management**: Programmatic schedule creation and management
|
|
45
|
+
- 🏥 **Health Monitoring**: Built-in health checks and status reporting
|
|
46
|
+
- 🔌 **Connection Management**: Automatic connection pooling and lifecycle management
|
|
47
|
+
- 🛠️ **Error Handling**: Comprehensive error handling with structured logging
|
|
48
|
+
- 📊 **Performance Monitoring**: Built-in metrics and performance tracking
|
|
49
|
+
- 🔚 **Graceful Shutdown**: Clean resource cleanup and connection termination
|
|
50
|
+
- 📦 **Modular Design**: Use only what you need (client-only, worker-only, etc.)
|
|
28
51
|
|
|
29
|
-
|
|
52
|
+
## Installation
|
|
30
53
|
|
|
31
|
-
|
|
54
|
+
```bash
|
|
55
|
+
npm install nestjs-temporal-core @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/common
|
|
56
|
+
```
|
|
32
57
|
|
|
33
|
-
|
|
34
|
-
- **🔍 Auto-Discovery** - Automatically finds and registers activities and schedules
|
|
35
|
-
- **📅 Declarative Scheduling** - Built-in cron and interval scheduling with validation
|
|
36
|
-
- **🔄 Unified Service** - Single `TemporalService` for all operations
|
|
37
|
-
- **⚙️ Flexible Setup** - Client-only, worker-only, or unified deployments
|
|
38
|
-
- **🏥 Health Monitoring** - Comprehensive status monitoring and health checks
|
|
39
|
-
- **🔧 Production Ready** - TLS, connection management, graceful shutdowns
|
|
40
|
-
- **📊 Modular Architecture** - Individual modules for specific needs
|
|
41
|
-
- **📝 Configurable Logging** - Fine-grained control with `TemporalLogger`
|
|
42
|
-
- **🔐 Enterprise Ready** - Temporal Cloud support with TLS and API keys
|
|
43
|
-
- **🛠️ Developer Experience** - Rich TypeScript support with comprehensive utilities
|
|
44
|
-
- **⚡ Performance Optimized** - Efficient metadata handling and caching
|
|
58
|
+
### Peer Dependencies
|
|
45
59
|
|
|
46
|
-
|
|
60
|
+
The package requires the following peer dependencies:
|
|
47
61
|
|
|
48
62
|
```bash
|
|
49
|
-
npm install nestjs
|
|
63
|
+
npm install @nestjs/common @nestjs/core reflect-metadata rxjs
|
|
50
64
|
```
|
|
51
65
|
|
|
52
|
-
##
|
|
66
|
+
## Quick Start
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
### 1. Enable Shutdown Hooks
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
│ ├── activity/ # Activity discovery and execution
|
|
63
|
-
│ ├── schedules/ # Schedule management
|
|
64
|
-
│ ├── discovery/ # Auto-discovery services
|
|
65
|
-
│ ├── utils/ # Utilities (validation, metadata, logging)
|
|
66
|
-
│ ├── constants.ts # Predefined constants and expressions
|
|
67
|
-
│ ├── interfaces.ts # TypeScript interfaces and types
|
|
68
|
-
│ ├── temporal.module.ts # Main module
|
|
69
|
-
│ └── temporal.service.ts # Unified service
|
|
70
|
-
```
|
|
70
|
+
First, enable shutdown hooks in your `main.ts` for proper Temporal resource cleanup:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// main.ts
|
|
74
|
+
import { NestFactory } from '@nestjs/core';
|
|
75
|
+
import { AppModule } from './app.module';
|
|
71
76
|
|
|
72
|
-
|
|
77
|
+
async function bootstrap() {
|
|
78
|
+
const app = await NestFactory.create(AppModule);
|
|
79
|
+
|
|
80
|
+
// Required for graceful Temporal connection cleanup
|
|
81
|
+
app.enableShutdownHooks();
|
|
82
|
+
|
|
83
|
+
await app.listen(3000);
|
|
84
|
+
}
|
|
85
|
+
bootstrap();
|
|
86
|
+
```
|
|
73
87
|
|
|
74
|
-
###
|
|
88
|
+
### 2. Configure the Module
|
|
75
89
|
|
|
76
|
-
|
|
90
|
+
Import and configure `TemporalModule` in your app module:
|
|
77
91
|
|
|
78
92
|
```typescript
|
|
79
93
|
// app.module.ts
|
|
80
94
|
import { Module } from '@nestjs/common';
|
|
81
95
|
import { TemporalModule } from 'nestjs-temporal-core';
|
|
82
|
-
import {
|
|
96
|
+
import { PaymentActivity } from './activities/payment.activity';
|
|
97
|
+
import { EmailActivity } from './activities/email.activity';
|
|
83
98
|
|
|
84
99
|
@Module({
|
|
85
100
|
imports: [
|
|
@@ -88,888 +103,1603 @@ import { EmailActivities } from './activities/email.activities';
|
|
|
88
103
|
address: 'localhost:7233',
|
|
89
104
|
namespace: 'default',
|
|
90
105
|
},
|
|
91
|
-
taskQueue: '
|
|
106
|
+
taskQueue: 'my-task-queue',
|
|
92
107
|
worker: {
|
|
93
|
-
workflowsPath: './
|
|
94
|
-
activityClasses: [
|
|
95
|
-
autoStart: true
|
|
96
|
-
}
|
|
97
|
-
})
|
|
108
|
+
workflowsPath: require.resolve('./workflows'),
|
|
109
|
+
activityClasses: [PaymentActivity, EmailActivity],
|
|
110
|
+
autoStart: true,
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
98
113
|
],
|
|
99
|
-
providers: [
|
|
114
|
+
providers: [PaymentActivity, EmailActivity],
|
|
100
115
|
})
|
|
101
116
|
export class AppModule {}
|
|
102
117
|
```
|
|
103
118
|
|
|
104
|
-
###
|
|
119
|
+
### 3. Define Activities
|
|
120
|
+
|
|
121
|
+
Create activities using `@Activity()` and `@ActivityMethod()` decorators:
|
|
105
122
|
|
|
106
123
|
```typescript
|
|
107
|
-
//
|
|
124
|
+
// payment.activity.ts
|
|
108
125
|
import { Injectable } from '@nestjs/common';
|
|
109
126
|
import { Activity, ActivityMethod } from 'nestjs-temporal-core';
|
|
110
127
|
|
|
111
|
-
|
|
128
|
+
export interface PaymentData {
|
|
129
|
+
amount: number;
|
|
130
|
+
currency: string;
|
|
131
|
+
customerId: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
@Injectable()
|
|
113
|
-
|
|
135
|
+
@Activity({ name: 'payment-activities' })
|
|
136
|
+
export class PaymentActivity {
|
|
114
137
|
|
|
115
|
-
@ActivityMethod(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
@ActivityMethod('processPayment')
|
|
139
|
+
async processPayment(data: PaymentData): Promise<{ transactionId: string }> {
|
|
140
|
+
// Payment processing logic with full NestJS DI support
|
|
141
|
+
console.log(`Processing payment: $${data.amount} ${data.currency}`);
|
|
142
|
+
|
|
143
|
+
// Simulate payment processing
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
145
|
+
|
|
146
|
+
return { transactionId: `txn_${Date.now()}` };
|
|
123
147
|
}
|
|
124
148
|
|
|
125
|
-
@ActivityMethod('
|
|
126
|
-
async
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
@ActivityMethod('refundPayment')
|
|
150
|
+
async refundPayment(transactionId: string): Promise<{ refundId: string }> {
|
|
151
|
+
// Refund logic
|
|
152
|
+
console.log(`Refunding transaction: ${transactionId}`);
|
|
153
|
+
return { refundId: `ref_${Date.now()}` };
|
|
129
154
|
}
|
|
130
155
|
}
|
|
131
156
|
```
|
|
132
157
|
|
|
133
|
-
###
|
|
134
|
-
|
|
135
|
-
```typescript
|
|
136
|
-
// workflows/email.workflow.ts
|
|
137
|
-
import { proxyActivities } from '@temporalio/workflow';
|
|
138
|
-
import type { EmailActivities } from '../activities/email.activities';
|
|
139
|
-
|
|
140
|
-
const { sendEmail, sendNotification } = proxyActivities<EmailActivities>({
|
|
141
|
-
startToCloseTimeout: '1 minute',
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
export async function processEmailWorkflow(
|
|
145
|
-
userId: string,
|
|
146
|
-
emailData: { to: string; subject: string; body: string }
|
|
147
|
-
): Promise<void> {
|
|
148
|
-
// Send email
|
|
149
|
-
await sendEmail(emailData.to, emailData.subject, emailData.body);
|
|
150
|
-
|
|
151
|
-
// Send notification
|
|
152
|
-
await sendNotification(userId, 'Email sent successfully');
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### 4. Schedule Workflows
|
|
158
|
+
### 4. Define Workflows
|
|
157
159
|
|
|
158
|
-
|
|
160
|
+
Create workflows as pure Temporal functions (NOT NestJS services):
|
|
159
161
|
|
|
160
162
|
```typescript
|
|
161
|
-
//
|
|
162
|
-
import {
|
|
163
|
-
import {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
description: 'Generate daily sales report',
|
|
174
|
-
taskQueue: 'reports',
|
|
175
|
-
workflowType: 'generateReportWorkflow', // Name of the workflow function to run
|
|
176
|
-
workflowArgs: [{ reportType: 'sales' }], // Arguments passed to the workflow
|
|
177
|
-
})
|
|
178
|
-
async generateDailyReport(): Promise<void> {
|
|
179
|
-
// This method is NOT executed directly. Instead, the schedule triggers the workflow specified above.
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
- The `@Scheduled` decorator registers a schedule with Temporal.
|
|
185
|
-
- The `workflowType` property specifies the workflow function to run (must be exported and available to the worker).
|
|
186
|
-
- The `workflowArgs` property allows you to pass arguments to the workflow when the schedule triggers.
|
|
187
|
-
|
|
188
|
-
> **Best Practice:** Keep your scheduled workflow logic in a dedicated workflow file, and use the schedule only to trigger it with the desired arguments.
|
|
163
|
+
// payment.workflow.ts
|
|
164
|
+
import { proxyActivities, defineSignal, defineQuery, setHandler } from '@temporalio/workflow';
|
|
165
|
+
import type { PaymentActivity } from './payment.activity';
|
|
166
|
+
|
|
167
|
+
// Create activity proxies
|
|
168
|
+
const { processPayment, refundPayment } = proxyActivities<typeof PaymentActivity.prototype>({
|
|
169
|
+
startToCloseTimeout: '5m',
|
|
170
|
+
retry: {
|
|
171
|
+
maximumAttempts: 3,
|
|
172
|
+
initialInterval: '1s',
|
|
173
|
+
},
|
|
174
|
+
});
|
|
189
175
|
|
|
190
|
-
|
|
176
|
+
// Define signals and queries
|
|
177
|
+
export const cancelPaymentSignal = defineSignal<[string]>('cancelPayment');
|
|
178
|
+
export const getPaymentStatusQuery = defineQuery<string>('getPaymentStatus');
|
|
191
179
|
|
|
192
|
-
|
|
180
|
+
export async function processPaymentWorkflow(data: PaymentData): Promise<any> {
|
|
181
|
+
let status = 'processing';
|
|
182
|
+
let transactionId: string | undefined;
|
|
193
183
|
|
|
194
|
-
|
|
184
|
+
// Set up signal and query handlers
|
|
185
|
+
setHandler(cancelPaymentSignal, (reason: string) => {
|
|
186
|
+
status = 'cancelled';
|
|
187
|
+
});
|
|
195
188
|
|
|
196
|
-
|
|
197
|
-
// workflows/order.workflow.ts
|
|
198
|
-
import { Injectable } from '@nestjs/common';
|
|
199
|
-
import {
|
|
200
|
-
WorkflowParam,
|
|
201
|
-
WorkflowContext,
|
|
202
|
-
WorkflowId,
|
|
203
|
-
RunId,
|
|
204
|
-
TaskQueue,
|
|
205
|
-
Signal,
|
|
206
|
-
Query
|
|
207
|
-
} from 'nestjs-temporal-core';
|
|
189
|
+
setHandler(getPaymentStatusQuery, () => status);
|
|
208
190
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
191
|
+
try {
|
|
192
|
+
// Execute payment activity
|
|
193
|
+
const result = await processPayment(data);
|
|
194
|
+
transactionId = result.transactionId;
|
|
195
|
+
status = 'completed';
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
success: true,
|
|
199
|
+
transactionId,
|
|
200
|
+
status,
|
|
201
|
+
};
|
|
202
|
+
} catch (error) {
|
|
203
|
+
status = 'failed';
|
|
204
|
+
|
|
205
|
+
// Compensate if needed
|
|
206
|
+
if (transactionId) {
|
|
207
|
+
await refundPayment(transactionId);
|
|
225
208
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.status = 'completed';
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
@Signal('updateOrder')
|
|
232
|
-
async updateOrder(@WorkflowParam() updateData: any): Promise<void> {
|
|
233
|
-
// Handle order update signal
|
|
234
|
-
this.updateData = updateData; // Store for use in processOrder
|
|
235
|
-
this.status = 'updated';
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
@Query('getOrderStatus')
|
|
239
|
-
getOrderStatus(@RunId() runId: string): string {
|
|
240
|
-
// Return current order status
|
|
241
|
-
return this.status;
|
|
209
|
+
|
|
210
|
+
throw error;
|
|
242
211
|
}
|
|
243
212
|
}
|
|
244
213
|
```
|
|
245
214
|
|
|
246
|
-
|
|
247
|
-
- **Best Practice:** Always store signal data in workflow state (class property or closure variable) to ensure it is available after workflow replay.
|
|
248
|
-
- **Forwarding Data:** The main workflow function (`processOrder`) can access and use the updated data as needed.
|
|
249
|
-
- **Status Tracking:** Use a property like `status` to track and query workflow progress.
|
|
250
|
-
|
|
251
|
-
> **Tip:** In Temporal workflows, use `condition()` or similar mechanisms to wait for signals in a non-blocking, replay-safe way.
|
|
215
|
+
### 5. Use in Services
|
|
252
216
|
|
|
253
|
-
|
|
217
|
+
Inject `TemporalService` to start and manage workflows:
|
|
254
218
|
|
|
255
219
|
```typescript
|
|
256
|
-
//
|
|
220
|
+
// payment.service.ts
|
|
257
221
|
import { Injectable } from '@nestjs/common';
|
|
258
222
|
import { TemporalService } from 'nestjs-temporal-core';
|
|
259
223
|
|
|
260
224
|
@Injectable()
|
|
261
|
-
export class
|
|
225
|
+
export class PaymentService {
|
|
262
226
|
constructor(private readonly temporal: TemporalService) {}
|
|
263
227
|
|
|
264
|
-
async
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
workflowId: `
|
|
271
|
-
|
|
272
|
-
'customer-id': orderData.customerId
|
|
273
|
-
}
|
|
228
|
+
async processPayment(paymentData: any) {
|
|
229
|
+
// Start workflow
|
|
230
|
+
const result = await this.temporal.startWorkflow(
|
|
231
|
+
'processPaymentWorkflow',
|
|
232
|
+
[paymentData],
|
|
233
|
+
{
|
|
234
|
+
workflowId: `payment-${Date.now()}`,
|
|
235
|
+
taskQueue: 'my-task-queue',
|
|
274
236
|
}
|
|
275
237
|
);
|
|
276
238
|
|
|
277
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
workflowId: result.result.workflowId,
|
|
241
|
+
runId: result.result.runId,
|
|
242
|
+
};
|
|
278
243
|
}
|
|
279
244
|
|
|
280
|
-
async
|
|
281
|
-
|
|
282
|
-
|
|
245
|
+
async checkPaymentStatus(workflowId: string) {
|
|
246
|
+
// Query workflow
|
|
247
|
+
const statusResult = await this.temporal.queryWorkflow(
|
|
248
|
+
workflowId,
|
|
249
|
+
'getPaymentStatus'
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return { status: statusResult.result };
|
|
283
253
|
}
|
|
284
254
|
|
|
285
|
-
async
|
|
286
|
-
|
|
287
|
-
|
|
255
|
+
async cancelPayment(workflowId: string, reason: string) {
|
|
256
|
+
// Send signal
|
|
257
|
+
await this.temporal.signalWorkflow(
|
|
258
|
+
workflowId,
|
|
259
|
+
'cancelPayment',
|
|
260
|
+
[reason]
|
|
261
|
+
);
|
|
288
262
|
}
|
|
289
263
|
}
|
|
290
264
|
```
|
|
291
265
|
|
|
292
|
-
##
|
|
293
|
-
|
|
294
|
-
NestJS Temporal Core provides comprehensive utilities and predefined constants for common use cases:
|
|
295
|
-
|
|
296
|
-
### Predefined Constants
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
import {
|
|
300
|
-
CRON_EXPRESSIONS,
|
|
301
|
-
INTERVAL_EXPRESSIONS,
|
|
302
|
-
TIMEOUTS,
|
|
303
|
-
RETRY_POLICIES
|
|
304
|
-
} from 'nestjs-temporal-core';
|
|
305
|
-
|
|
306
|
-
// Cron expressions
|
|
307
|
-
console.log(CRON_EXPRESSIONS.DAILY_8AM); // '0 8 * * *'
|
|
308
|
-
console.log(CRON_EXPRESSIONS.WEEKLY_MONDAY_9AM); // '0 9 * * 1'
|
|
309
|
-
console.log(CRON_EXPRESSIONS.MONTHLY_FIRST); // '0 0 1 * *'
|
|
310
|
-
|
|
311
|
-
// Interval expressions
|
|
312
|
-
console.log(INTERVAL_EXPRESSIONS.EVERY_5_MINUTES); // '5m'
|
|
313
|
-
console.log(INTERVAL_EXPRESSIONS.EVERY_HOUR); // '1h'
|
|
314
|
-
console.log(INTERVAL_EXPRESSIONS.DAILY); // '24h'
|
|
315
|
-
|
|
316
|
-
// Timeout values
|
|
317
|
-
console.log(TIMEOUTS.ACTIVITY_SHORT); // '1m'
|
|
318
|
-
console.log(TIMEOUTS.WORKFLOW_MEDIUM); // '24h'
|
|
319
|
-
console.log(TIMEOUTS.CONNECTION_TIMEOUT); // '10s'
|
|
320
|
-
|
|
321
|
-
// Retry policies
|
|
322
|
-
console.log(RETRY_POLICIES.QUICK.maximumAttempts); // 3
|
|
323
|
-
console.log(RETRY_POLICIES.STANDARD.backoffCoefficient); // 2.0
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### Validation Utilities
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
import {
|
|
330
|
-
isValidCronExpression,
|
|
331
|
-
isValidIntervalExpression
|
|
332
|
-
} from 'nestjs-temporal-core';
|
|
333
|
-
|
|
334
|
-
// Validate cron expressions
|
|
335
|
-
console.log(isValidCronExpression('0 8 * * *')); // true
|
|
336
|
-
console.log(isValidCronExpression('invalid')); // false
|
|
337
|
-
|
|
338
|
-
// Validate interval expressions
|
|
339
|
-
console.log(isValidIntervalExpression('5m')); // true
|
|
340
|
-
console.log(isValidIntervalExpression('2h')); // true
|
|
341
|
-
console.log(isValidIntervalExpression('bad')); // false
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
### Metadata Utilities
|
|
345
|
-
|
|
346
|
-
```typescript
|
|
347
|
-
import {
|
|
348
|
-
isActivity,
|
|
349
|
-
getActivityMetadata,
|
|
350
|
-
isActivityMethod,
|
|
351
|
-
getActivityMethodMetadata,
|
|
352
|
-
getParameterMetadata
|
|
353
|
-
} from 'nestjs-temporal-core';
|
|
354
|
-
|
|
355
|
-
// Check if a class is marked as an Activity
|
|
356
|
-
@Activity({ taskQueue: 'my-queue' })
|
|
357
|
-
class MyActivity {}
|
|
266
|
+
## Module Variants
|
|
358
267
|
|
|
359
|
-
|
|
360
|
-
const metadata = getActivityMetadata(MyActivity);
|
|
361
|
-
console.log(metadata.taskQueue); // 'my-queue'
|
|
268
|
+
The package provides modular architecture with separate modules for different use cases:
|
|
362
269
|
|
|
363
|
-
|
|
364
|
-
const methodMetadata = getActivityMethodMetadata(MyActivity.prototype.myMethod);
|
|
365
|
-
```
|
|
270
|
+
### 1. Unified Module (Recommended)
|
|
366
271
|
|
|
367
|
-
|
|
272
|
+
Complete integration with both client and worker capabilities:
|
|
368
273
|
|
|
369
274
|
```typescript
|
|
370
|
-
import {
|
|
371
|
-
|
|
372
|
-
// Configure logging
|
|
373
|
-
const logger = TemporalLoggerManager.getInstance();
|
|
374
|
-
logger.configure({
|
|
375
|
-
enableLogger: true,
|
|
376
|
-
logLevel: 'info',
|
|
377
|
-
appName: 'My Temporal App'
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
// Use in your services
|
|
381
|
-
@Injectable()
|
|
382
|
-
export class MyService {
|
|
383
|
-
private readonly logger = new TemporalLogger(MyService.name);
|
|
275
|
+
import { TemporalModule } from 'nestjs-temporal-core';
|
|
384
276
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
277
|
+
TemporalModule.register({
|
|
278
|
+
connection: { address: 'localhost:7233' },
|
|
279
|
+
taskQueue: 'my-queue',
|
|
280
|
+
worker: {
|
|
281
|
+
workflowsPath: require.resolve('./workflows'),
|
|
282
|
+
activityClasses: [PaymentActivity, EmailActivity],
|
|
283
|
+
},
|
|
284
|
+
})
|
|
390
285
|
```
|
|
391
286
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
### Client-Only Integration
|
|
287
|
+
### 2. Client-Only Module
|
|
395
288
|
|
|
396
|
-
For
|
|
289
|
+
For services that only need to start/query workflows:
|
|
397
290
|
|
|
398
291
|
```typescript
|
|
399
|
-
import { TemporalClientModule } from 'nestjs-temporal-core';
|
|
292
|
+
import { TemporalClientModule } from 'nestjs-temporal-core/client';
|
|
400
293
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
connection: {
|
|
405
|
-
address: 'localhost:7233',
|
|
406
|
-
namespace: 'production'
|
|
407
|
-
}
|
|
408
|
-
})
|
|
409
|
-
],
|
|
410
|
-
providers: [ApiService],
|
|
294
|
+
TemporalClientModule.register({
|
|
295
|
+
connection: { address: 'localhost:7233' },
|
|
296
|
+
namespace: 'default',
|
|
411
297
|
})
|
|
412
|
-
export class ClientOnlyModule {}
|
|
413
298
|
```
|
|
414
299
|
|
|
415
|
-
### Worker-Only
|
|
300
|
+
### 3. Worker-Only Module
|
|
416
301
|
|
|
417
302
|
For dedicated worker processes:
|
|
418
303
|
|
|
419
304
|
```typescript
|
|
420
|
-
import { TemporalWorkerModule
|
|
305
|
+
import { TemporalWorkerModule } from 'nestjs-temporal-core/worker';
|
|
421
306
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
taskQueue: 'worker-queue',
|
|
430
|
-
workflowsPath: './dist/workflows',
|
|
431
|
-
activityClasses: [ProcessingActivities],
|
|
432
|
-
workerOptions: WORKER_PRESETS.PRODUCTION_HIGH_THROUGHPUT
|
|
433
|
-
})
|
|
434
|
-
],
|
|
435
|
-
providers: [ProcessingActivities],
|
|
307
|
+
TemporalWorkerModule.register({
|
|
308
|
+
connection: { address: 'localhost:7233' },
|
|
309
|
+
taskQueue: 'worker-queue',
|
|
310
|
+
worker: {
|
|
311
|
+
workflowsPath: require.resolve('./workflows'),
|
|
312
|
+
activityClasses: [BackgroundActivity],
|
|
313
|
+
},
|
|
436
314
|
})
|
|
437
|
-
export class WorkerOnlyModule {}
|
|
438
315
|
```
|
|
439
316
|
|
|
440
|
-
###
|
|
317
|
+
### 4. Activity-Only Module
|
|
441
318
|
|
|
442
|
-
|
|
319
|
+
For standalone activity management:
|
|
443
320
|
|
|
444
321
|
```typescript
|
|
445
|
-
import {
|
|
446
|
-
TemporalClientModule,
|
|
447
|
-
TemporalActivityModule,
|
|
448
|
-
TemporalSchedulesModule
|
|
449
|
-
} from 'nestjs-temporal-core';
|
|
322
|
+
import { TemporalActivityModule } from 'nestjs-temporal-core/activity';
|
|
450
323
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
// Client for workflow operations
|
|
454
|
-
TemporalClientModule.forRoot({
|
|
455
|
-
connection: { address: 'localhost:7233' }
|
|
456
|
-
}),
|
|
457
|
-
|
|
458
|
-
// Activities management
|
|
459
|
-
TemporalActivityModule.forRoot({
|
|
460
|
-
activityClasses: [EmailActivities, PaymentActivities]
|
|
461
|
-
}),
|
|
462
|
-
|
|
463
|
-
// Schedule management
|
|
464
|
-
TemporalSchedulesModule.forRoot({
|
|
465
|
-
autoStart: true,
|
|
466
|
-
defaultTimezone: 'UTC'
|
|
467
|
-
}),
|
|
468
|
-
],
|
|
469
|
-
providers: [EmailActivities, PaymentActivities, ScheduledService],
|
|
324
|
+
TemporalActivityModule.register({
|
|
325
|
+
activityClasses: [DataProcessingActivity],
|
|
470
326
|
})
|
|
471
|
-
export class ModularIntegrationModule {}
|
|
472
327
|
```
|
|
473
328
|
|
|
474
|
-
|
|
329
|
+
### 5. Schedules-Only Module
|
|
475
330
|
|
|
476
|
-
|
|
331
|
+
For managing Temporal schedules:
|
|
477
332
|
|
|
478
333
|
```typescript
|
|
479
|
-
import {
|
|
334
|
+
import { TemporalSchedulesModule } from 'nestjs-temporal-core/schedules';
|
|
480
335
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
ConfigModule.forRoot(),
|
|
484
|
-
TemporalModule.registerAsync({
|
|
485
|
-
imports: [ConfigModule],
|
|
486
|
-
useFactory: async (config: ConfigService) => ({
|
|
487
|
-
connection: {
|
|
488
|
-
address: config.get('TEMPORAL_ADDRESS'),
|
|
489
|
-
namespace: config.get('TEMPORAL_NAMESPACE'),
|
|
490
|
-
tls: config.get('TEMPORAL_TLS_ENABLED') === 'true',
|
|
491
|
-
apiKey: config.get('TEMPORAL_API_KEY'),
|
|
492
|
-
},
|
|
493
|
-
taskQueue: config.get('TEMPORAL_TASK_QUEUE'),
|
|
494
|
-
worker: {
|
|
495
|
-
workflowsPath: config.get('WORKFLOWS_PATH'),
|
|
496
|
-
activityClasses: [EmailActivities, PaymentActivities],
|
|
497
|
-
autoStart: config.get('WORKER_AUTO_START') !== 'false',
|
|
498
|
-
}
|
|
499
|
-
}),
|
|
500
|
-
inject: [ConfigService],
|
|
501
|
-
})
|
|
502
|
-
],
|
|
336
|
+
TemporalSchedulesModule.register({
|
|
337
|
+
connection: { address: 'localhost:7233' },
|
|
503
338
|
})
|
|
504
|
-
export class AppModule {}
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
### Environment-Specific Configurations
|
|
508
|
-
|
|
509
|
-
```typescript
|
|
510
|
-
// Development
|
|
511
|
-
const developmentConfig = {
|
|
512
|
-
connection: {
|
|
513
|
-
address: 'localhost:7233',
|
|
514
|
-
namespace: 'development'
|
|
515
|
-
},
|
|
516
|
-
taskQueue: 'dev-queue',
|
|
517
|
-
worker: {
|
|
518
|
-
workflowsPath: './dist/workflows',
|
|
519
|
-
workerOptions: WORKER_PRESETS.DEVELOPMENT
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
// Production
|
|
524
|
-
const productionConfig = {
|
|
525
|
-
connection: {
|
|
526
|
-
address: process.env.TEMPORAL_ADDRESS!,
|
|
527
|
-
namespace: process.env.TEMPORAL_NAMESPACE!,
|
|
528
|
-
tls: true,
|
|
529
|
-
apiKey: process.env.TEMPORAL_API_KEY
|
|
530
|
-
},
|
|
531
|
-
taskQueue: process.env.TEMPORAL_TASK_QUEUE!,
|
|
532
|
-
worker: {
|
|
533
|
-
workflowBundle: require('../workflows/bundle'), // Pre-bundled
|
|
534
|
-
workerOptions: WORKER_PRESETS.PRODUCTION_BALANCED
|
|
535
|
-
}
|
|
536
|
-
};
|
|
537
339
|
```
|
|
538
340
|
|
|
539
|
-
##
|
|
341
|
+
## Configuration
|
|
540
342
|
|
|
541
|
-
|
|
343
|
+
## Configuration
|
|
542
344
|
|
|
543
|
-
### Basic
|
|
345
|
+
### Basic Configuration
|
|
544
346
|
|
|
545
347
|
```typescript
|
|
546
|
-
// Enable/disable logging and set log levels
|
|
547
348
|
TemporalModule.register({
|
|
548
349
|
connection: {
|
|
549
350
|
address: 'localhost:7233',
|
|
550
|
-
namespace: 'default'
|
|
351
|
+
namespace: 'default',
|
|
551
352
|
},
|
|
552
|
-
taskQueue: '
|
|
553
|
-
// Logger configuration
|
|
554
|
-
enableLogger: true, // Enable/disable all logging
|
|
555
|
-
logLevel: 'info', // Set log level: 'error' | 'warn' | 'info' | 'debug' | 'verbose'
|
|
353
|
+
taskQueue: 'my-task-queue',
|
|
556
354
|
worker: {
|
|
557
|
-
workflowsPath: './
|
|
558
|
-
activityClasses: [
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
### Environment-Based Logger Configuration
|
|
564
|
-
|
|
565
|
-
```typescript
|
|
566
|
-
// Different log levels for different environments
|
|
567
|
-
const loggerConfig = {
|
|
568
|
-
development: {
|
|
569
|
-
enableLogger: true,
|
|
570
|
-
logLevel: 'debug' as const // Show all logs in development
|
|
355
|
+
workflowsPath: require.resolve('./workflows'),
|
|
356
|
+
activityClasses: [PaymentActivity, EmailActivity],
|
|
357
|
+
autoStart: true,
|
|
358
|
+
maxConcurrentActivityExecutions: 100,
|
|
571
359
|
},
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
logLevel: 'warn' as const // Only warnings and errors in production
|
|
575
|
-
},
|
|
576
|
-
testing: {
|
|
577
|
-
enableLogger: false // Disable logging during tests
|
|
578
|
-
}
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
TemporalModule.register({
|
|
582
|
-
connection: { address: 'localhost:7233' },
|
|
583
|
-
taskQueue: 'main-queue',
|
|
584
|
-
...loggerConfig[process.env.NODE_ENV || 'development'],
|
|
585
|
-
worker: {
|
|
586
|
-
workflowsPath: './dist/workflows'
|
|
587
|
-
}
|
|
360
|
+
logLevel: 'info',
|
|
361
|
+
enableLogger: true,
|
|
588
362
|
})
|
|
589
363
|
```
|
|
590
364
|
|
|
591
|
-
###
|
|
365
|
+
### Async Configuration
|
|
592
366
|
|
|
593
|
-
|
|
367
|
+
For dynamic configuration using environment variables or config services:
|
|
594
368
|
|
|
595
369
|
```typescript
|
|
596
|
-
//
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
logLevel: 'debug'
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
// Schedules Module with minimal logging
|
|
604
|
-
TemporalSchedulesModule.forRoot({
|
|
605
|
-
autoStart: true,
|
|
606
|
-
enableLogger: true,
|
|
607
|
-
logLevel: 'error' // Only show errors
|
|
608
|
-
})
|
|
370
|
+
// config/temporal.config.ts
|
|
371
|
+
import { Injectable } from '@nestjs/common';
|
|
372
|
+
import { ConfigService } from '@nestjs/config';
|
|
373
|
+
import { TemporalOptionsFactory, TemporalOptions } from 'nestjs-temporal-core';
|
|
609
374
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
enableLogger: false
|
|
614
|
-
})
|
|
615
|
-
```
|
|
375
|
+
@Injectable()
|
|
376
|
+
export class TemporalConfigService implements TemporalOptionsFactory {
|
|
377
|
+
constructor(private configService: ConfigService) {}
|
|
616
378
|
|
|
617
|
-
|
|
379
|
+
createTemporalOptions(): TemporalOptions {
|
|
380
|
+
return {
|
|
381
|
+
connection: {
|
|
382
|
+
address: this.configService.get('TEMPORAL_ADDRESS', 'localhost:7233'),
|
|
383
|
+
namespace: this.configService.get('TEMPORAL_NAMESPACE', 'default'),
|
|
384
|
+
},
|
|
385
|
+
taskQueue: this.configService.get('TEMPORAL_TASK_QUEUE', 'default'),
|
|
386
|
+
worker: {
|
|
387
|
+
workflowsPath: require.resolve('../workflows'),
|
|
388
|
+
activityClasses: [], // Populated by module
|
|
389
|
+
maxConcurrentActivityExecutions: 100,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
618
394
|
|
|
619
|
-
|
|
395
|
+
// app.module.ts
|
|
396
|
+
import { ConfigModule } from '@nestjs/config';
|
|
620
397
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
398
|
+
@Module({
|
|
399
|
+
imports: [
|
|
400
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
401
|
+
TemporalModule.registerAsync({
|
|
402
|
+
imports: [ConfigModule],
|
|
403
|
+
useClass: TemporalConfigService,
|
|
404
|
+
}),
|
|
405
|
+
],
|
|
406
|
+
})
|
|
407
|
+
export class AppModule {}
|
|
408
|
+
```
|
|
626
409
|
|
|
627
|
-
### Async
|
|
410
|
+
### Alternative Async Pattern (useFactory)
|
|
628
411
|
|
|
629
412
|
```typescript
|
|
630
413
|
TemporalModule.registerAsync({
|
|
631
414
|
imports: [ConfigModule],
|
|
632
|
-
useFactory: (
|
|
415
|
+
useFactory: (configService: ConfigService) => ({
|
|
633
416
|
connection: {
|
|
634
|
-
address:
|
|
635
|
-
namespace:
|
|
417
|
+
address: configService.get('TEMPORAL_ADDRESS', 'localhost:7233'),
|
|
418
|
+
namespace: configService.get('TEMPORAL_NAMESPACE', 'default'),
|
|
636
419
|
},
|
|
637
|
-
taskQueue:
|
|
638
|
-
// Dynamic logger configuration
|
|
639
|
-
enableLogger: config.get('TEMPORAL_LOGGING_ENABLED', 'true') === 'true',
|
|
640
|
-
logLevel: config.get('TEMPORAL_LOG_LEVEL', 'info'),
|
|
420
|
+
taskQueue: configService.get('TEMPORAL_TASK_QUEUE', 'default'),
|
|
641
421
|
worker: {
|
|
642
|
-
workflowsPath: './
|
|
643
|
-
|
|
422
|
+
workflowsPath: require.resolve('./workflows'),
|
|
423
|
+
activityClasses: [PaymentActivity, EmailActivity],
|
|
424
|
+
},
|
|
644
425
|
}),
|
|
645
|
-
inject: [ConfigService]
|
|
426
|
+
inject: [ConfigService],
|
|
646
427
|
})
|
|
647
428
|
```
|
|
648
429
|
|
|
649
|
-
###
|
|
430
|
+
### TLS Configuration (Temporal Cloud)
|
|
431
|
+
|
|
432
|
+
For secure connections to Temporal Cloud:
|
|
650
433
|
|
|
651
434
|
```typescript
|
|
652
|
-
|
|
653
|
-
{
|
|
654
|
-
enableLogger: false
|
|
655
|
-
}
|
|
435
|
+
import * as fs from 'fs';
|
|
656
436
|
|
|
657
|
-
|
|
658
|
-
{
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
437
|
+
TemporalModule.register({
|
|
438
|
+
connection: {
|
|
439
|
+
address: 'your-namespace.your-account.tmprl.cloud:7233',
|
|
440
|
+
namespace: 'your-namespace.your-account',
|
|
441
|
+
tls: {
|
|
442
|
+
clientCertPair: {
|
|
443
|
+
crt: fs.readFileSync('/path/to/client.crt'),
|
|
444
|
+
key: fs.readFileSync('/path/to/client.key'),
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
taskQueue: 'my-task-queue',
|
|
449
|
+
worker: {
|
|
450
|
+
workflowsPath: require.resolve('./workflows'),
|
|
451
|
+
activityClasses: [PaymentActivity],
|
|
452
|
+
},
|
|
453
|
+
})
|
|
454
|
+
```
|
|
662
455
|
|
|
663
|
-
|
|
664
|
-
{
|
|
665
|
-
enableLogger: true,
|
|
666
|
-
logLevel: 'debug'
|
|
667
|
-
}
|
|
456
|
+
### Configuration Options Reference
|
|
668
457
|
|
|
669
|
-
|
|
670
|
-
{
|
|
671
|
-
|
|
672
|
-
|
|
458
|
+
```typescript
|
|
459
|
+
interface TemporalOptions {
|
|
460
|
+
// Connection settings
|
|
461
|
+
connection: {
|
|
462
|
+
address: string; // Temporal server address (default: 'localhost:7233')
|
|
463
|
+
namespace?: string; // Temporal namespace (default: 'default')
|
|
464
|
+
tls?: TLSConfig; // TLS configuration for secure connections
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// Task queue name
|
|
468
|
+
taskQueue?: string; // Default task queue (default: 'default')
|
|
469
|
+
|
|
470
|
+
// Worker configuration
|
|
471
|
+
worker?: {
|
|
472
|
+
workflowsPath?: string; // Path to workflow definitions (use require.resolve)
|
|
473
|
+
activityClasses?: any[]; // Array of activity classes to register
|
|
474
|
+
autoStart?: boolean; // Auto-start worker on module init (default: true)
|
|
475
|
+
maxConcurrentActivityExecutions?: number; // Max concurrent activities (default: 100)
|
|
476
|
+
maxActivitiesPerSecond?: number; // Rate limit for activities
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// Logging
|
|
480
|
+
logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error'; // Log level (default: 'info')
|
|
481
|
+
enableLogger?: boolean; // Enable logging (default: true)
|
|
482
|
+
|
|
483
|
+
// Advanced
|
|
484
|
+
isGlobal?: boolean; // Make module global (default: false)
|
|
485
|
+
autoRestart?: boolean; // Auto-restart worker on failure (default: true)
|
|
673
486
|
}
|
|
674
487
|
```
|
|
675
488
|
|
|
676
|
-
##
|
|
489
|
+
## Core Concepts
|
|
677
490
|
|
|
678
|
-
|
|
491
|
+
### Activities
|
|
679
492
|
|
|
680
|
-
|
|
681
|
-
@Controller('health')
|
|
682
|
-
export class HealthController {
|
|
683
|
-
constructor(private readonly temporal: TemporalService) {}
|
|
493
|
+
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.
|
|
684
494
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
495
|
+
**Key Points:**
|
|
496
|
+
- Activities are NestJS services (`@Injectable()`)
|
|
497
|
+
- Use `@Activity()` decorator at class level
|
|
498
|
+
- Use `@ActivityMethod()` decorator for methods to be registered
|
|
499
|
+
- Activities should be idempotent and handle retries gracefully
|
|
500
|
+
- Full access to NestJS DI (inject services, repositories, etc.)
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
@Injectable()
|
|
504
|
+
@Activity({ name: 'order-activities' })
|
|
505
|
+
export class OrderActivity {
|
|
506
|
+
constructor(
|
|
507
|
+
private readonly orderRepository: OrderRepository,
|
|
508
|
+
private readonly emailService: EmailService,
|
|
509
|
+
) {}
|
|
510
|
+
|
|
511
|
+
@ActivityMethod('createOrder')
|
|
512
|
+
async createOrder(orderData: CreateOrderData): Promise<Order> {
|
|
513
|
+
// Database operations with full DI support
|
|
514
|
+
const order = await this.orderRepository.create(orderData);
|
|
515
|
+
await this.emailService.sendConfirmation(order);
|
|
516
|
+
return order;
|
|
693
517
|
}
|
|
694
518
|
|
|
695
|
-
@
|
|
696
|
-
async
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
return {
|
|
701
|
-
system: systemStatus,
|
|
702
|
-
discovery: stats,
|
|
703
|
-
schedules: this.temporal.getScheduleStats()
|
|
704
|
-
};
|
|
519
|
+
@ActivityMethod('validateInventory')
|
|
520
|
+
async validateInventory(items: OrderItem[]): Promise<boolean> {
|
|
521
|
+
// Business logic with injected services
|
|
522
|
+
return await this.orderRepository.checkInventory(items);
|
|
705
523
|
}
|
|
706
524
|
}
|
|
707
525
|
```
|
|
708
526
|
|
|
709
|
-
|
|
527
|
+
### Workflows
|
|
528
|
+
|
|
529
|
+
Workflows are **pure Temporal functions** (NOT NestJS services) that orchestrate activities. They must be deterministic and use Temporal's workflow APIs.
|
|
710
530
|
|
|
711
|
-
|
|
531
|
+
**Important:** Workflows are NOT decorated with `@Injectable()` and should NOT use NestJS dependency injection.
|
|
712
532
|
|
|
713
533
|
```typescript
|
|
714
|
-
|
|
715
|
-
@
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
})
|
|
729
|
-
async processPayment(orderId: string, amount: number) {
|
|
730
|
-
// Complex payment processing with retries
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
```
|
|
534
|
+
// order.workflow.ts
|
|
535
|
+
import { proxyActivities, defineSignal, defineQuery, setHandler } from '@temporalio/workflow';
|
|
536
|
+
import type { OrderActivity } from './order.activity';
|
|
537
|
+
|
|
538
|
+
// Create activity proxies with proper typing
|
|
539
|
+
const { createOrder, validateInventory } = proxyActivities<typeof OrderActivity.prototype>({
|
|
540
|
+
startToCloseTimeout: '5m',
|
|
541
|
+
retry: {
|
|
542
|
+
maximumAttempts: 3,
|
|
543
|
+
initialInterval: '1s',
|
|
544
|
+
maximumInterval: '30s',
|
|
545
|
+
},
|
|
546
|
+
});
|
|
734
547
|
|
|
735
|
-
|
|
548
|
+
// Define signals and queries at module level
|
|
549
|
+
export const cancelOrderSignal = defineSignal<[string]>('cancelOrder');
|
|
550
|
+
export const getOrderStatusQuery = defineQuery<string>('getOrderStatus');
|
|
736
551
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
constructor(private readonly temporal: TemporalService) {}
|
|
552
|
+
// Workflow function (exported, not a class)
|
|
553
|
+
export async function processOrderWorkflow(orderData: CreateOrderData): Promise<OrderResult> {
|
|
554
|
+
let status = 'pending';
|
|
741
555
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
556
|
+
// Set up signal handler
|
|
557
|
+
setHandler(cancelOrderSignal, (reason: string) => {
|
|
558
|
+
status = 'cancelled';
|
|
559
|
+
});
|
|
745
560
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}
|
|
561
|
+
// Set up query handler
|
|
562
|
+
setHandler(getOrderStatusQuery, () => status);
|
|
749
563
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
564
|
+
try {
|
|
565
|
+
// Validate inventory
|
|
566
|
+
const isValid = await validateInventory(orderData.items);
|
|
567
|
+
if (!isValid) {
|
|
568
|
+
throw new Error('Insufficient inventory');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Create order
|
|
572
|
+
status = 'processing';
|
|
573
|
+
const order = await createOrder(orderData);
|
|
574
|
+
status = 'completed';
|
|
753
575
|
|
|
754
|
-
|
|
755
|
-
|
|
576
|
+
return {
|
|
577
|
+
orderId: order.id,
|
|
578
|
+
status,
|
|
579
|
+
};
|
|
580
|
+
} catch (error) {
|
|
581
|
+
status = 'failed';
|
|
582
|
+
throw error;
|
|
756
583
|
}
|
|
757
584
|
}
|
|
758
585
|
```
|
|
759
586
|
|
|
760
|
-
###
|
|
587
|
+
### Signals and Queries
|
|
588
|
+
|
|
589
|
+
Signals allow external systems to send events to workflows, while queries provide read-only access to workflow state.
|
|
761
590
|
|
|
762
591
|
```typescript
|
|
763
|
-
|
|
764
|
-
import { defineSignal, defineQuery, setHandler } from '@temporalio/workflow';
|
|
592
|
+
import { defineSignal, defineQuery, setHandler, condition } from '@temporalio/workflow';
|
|
765
593
|
|
|
766
|
-
|
|
594
|
+
// Define at module level
|
|
595
|
+
export const updateStatusSignal = defineSignal<[string]>('updateStatus');
|
|
596
|
+
export const addItemSignal = defineSignal<[Item]>('addItem');
|
|
597
|
+
export const getItemsQuery = defineQuery<Item[]>('getItems');
|
|
767
598
|
export const getStatusQuery = defineQuery<string>('getStatus');
|
|
768
599
|
|
|
769
|
-
export async function
|
|
770
|
-
let status = '
|
|
771
|
-
|
|
600
|
+
export async function myWorkflow(): Promise<void> {
|
|
601
|
+
let status = 'pending';
|
|
602
|
+
const items: Item[] = [];
|
|
772
603
|
|
|
773
|
-
//
|
|
774
|
-
setHandler(
|
|
775
|
-
|
|
776
|
-
|
|
604
|
+
// Set up handlers
|
|
605
|
+
setHandler(updateStatusSignal, (newStatus: string) => {
|
|
606
|
+
status = newStatus;
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
setHandler(addItemSignal, (item: Item) => {
|
|
610
|
+
items.push(item);
|
|
777
611
|
});
|
|
778
612
|
|
|
779
|
-
|
|
613
|
+
setHandler(getItemsQuery, () => items);
|
|
780
614
|
setHandler(getStatusQuery, () => status);
|
|
781
615
|
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
// Process order...
|
|
786
|
-
status = 'completed';
|
|
616
|
+
// Wait for completion signal
|
|
617
|
+
await condition(() => status === 'completed');
|
|
787
618
|
}
|
|
788
619
|
```
|
|
789
620
|
|
|
790
|
-
|
|
621
|
+
### Using Workflows in Services
|
|
791
622
|
|
|
792
|
-
|
|
623
|
+
Inject `TemporalService` in your NestJS services to interact with workflows:
|
|
793
624
|
|
|
794
625
|
```typescript
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
'
|
|
626
|
+
@Injectable()
|
|
627
|
+
export class OrderService {
|
|
628
|
+
constructor(private readonly temporal: TemporalService) {}
|
|
629
|
+
|
|
630
|
+
async createOrder(orderData: CreateOrderData) {
|
|
631
|
+
// Start workflow - note the method signature
|
|
632
|
+
const result = await this.temporal.startWorkflow(
|
|
633
|
+
'processOrderWorkflow', // Workflow function name
|
|
634
|
+
[orderData], // Arguments array
|
|
635
|
+
{ // Options
|
|
636
|
+
workflowId: `order-${Date.now()}`,
|
|
637
|
+
taskQueue: 'order-queue',
|
|
638
|
+
}
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
return {
|
|
642
|
+
workflowId: result.result.workflowId,
|
|
643
|
+
runId: result.result.runId,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async queryOrderStatus(workflowId: string) {
|
|
648
|
+
const result = await this.temporal.queryWorkflow(
|
|
649
|
+
workflowId,
|
|
650
|
+
'getOrderStatus'
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
return result.result;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async cancelOrder(workflowId: string, reason: string) {
|
|
657
|
+
await this.temporal.signalWorkflow(
|
|
658
|
+
workflowId,
|
|
659
|
+
'cancelOrder',
|
|
660
|
+
[reason]
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
## API Reference
|
|
667
|
+
|
|
668
|
+
### TemporalService
|
|
669
|
+
|
|
670
|
+
The main unified service providing access to all Temporal functionality:
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
class TemporalService {
|
|
674
|
+
/**
|
|
675
|
+
* Start a workflow execution
|
|
676
|
+
* @param workflowType - Name of the workflow function
|
|
677
|
+
* @param args - Array of arguments to pass to the workflow
|
|
678
|
+
* @param options - Workflow execution options
|
|
679
|
+
*/
|
|
680
|
+
async startWorkflow<T>(
|
|
681
|
+
workflowType: string,
|
|
682
|
+
args?: unknown[],
|
|
683
|
+
options?: WorkflowStartOptions
|
|
684
|
+
): Promise<WorkflowExecutionResult<T>>
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Send a signal to a running workflow
|
|
688
|
+
* @param workflowId - The workflow ID
|
|
689
|
+
* @param signalName - Name of the signal
|
|
690
|
+
* @param args - Arguments for the signal
|
|
691
|
+
*/
|
|
692
|
+
async signalWorkflow(
|
|
693
|
+
workflowId: string,
|
|
694
|
+
signalName: string,
|
|
695
|
+
args?: unknown[]
|
|
696
|
+
): Promise<WorkflowSignalResult>
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Query a running workflow
|
|
700
|
+
* @param workflowId - The workflow ID
|
|
701
|
+
* @param queryName - Name of the query
|
|
702
|
+
* @param args - Arguments for the query
|
|
703
|
+
*/
|
|
704
|
+
async queryWorkflow<T>(
|
|
705
|
+
workflowId: string,
|
|
706
|
+
queryName: string,
|
|
707
|
+
args?: unknown[]
|
|
708
|
+
): Promise<WorkflowQueryResult<T>>
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Get a workflow handle to interact with it
|
|
712
|
+
* @param workflowId - The workflow ID
|
|
713
|
+
* @param runId - Optional run ID for specific execution
|
|
714
|
+
*/
|
|
715
|
+
async getWorkflowHandle<T>(
|
|
716
|
+
workflowId: string,
|
|
717
|
+
runId?: string
|
|
718
|
+
): Promise<T>
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Terminate a workflow execution
|
|
722
|
+
* @param workflowId - The workflow ID
|
|
723
|
+
* @param reason - Termination reason
|
|
724
|
+
*/
|
|
725
|
+
async terminateWorkflow(
|
|
726
|
+
workflowId: string,
|
|
727
|
+
reason?: string
|
|
728
|
+
): Promise<WorkflowTerminationResult>
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Cancel a workflow execution
|
|
732
|
+
* @param workflowId - The workflow ID
|
|
733
|
+
*/
|
|
734
|
+
async cancelWorkflow(
|
|
735
|
+
workflowId: string
|
|
736
|
+
): Promise<WorkflowCancellationResult>
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Get service health status
|
|
740
|
+
*/
|
|
741
|
+
getHealth(): ServiceHealth
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Create a schedule
|
|
745
|
+
*/
|
|
746
|
+
async createSchedule(options: ScheduleCreateOptions): Promise<ScheduleHandle>
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* List all schedules
|
|
750
|
+
*/
|
|
751
|
+
async listSchedules(): Promise<ScheduleListDescription[]>
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Delete a schedule
|
|
755
|
+
*/
|
|
756
|
+
async deleteSchedule(scheduleId: string): Promise<void>
|
|
757
|
+
}
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### WorkflowStartOptions
|
|
761
|
+
|
|
762
|
+
Options for starting workflows:
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
interface WorkflowStartOptions {
|
|
766
|
+
workflowId?: string; // Unique workflow ID
|
|
767
|
+
taskQueue?: string; // Task queue name
|
|
768
|
+
workflowExecutionTimeout?: Duration; // Total workflow timeout
|
|
769
|
+
workflowRunTimeout?: Duration; // Single run timeout
|
|
770
|
+
workflowTaskTimeout?: Duration; // Decision task timeout
|
|
771
|
+
memo?: Record<string, unknown>; // Workflow memo
|
|
772
|
+
searchAttributes?: SearchAttributes; // Search attributes for filtering
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
### Result Types
|
|
777
|
+
|
|
778
|
+
```typescript
|
|
779
|
+
interface WorkflowExecutionResult<T> {
|
|
780
|
+
success: boolean;
|
|
781
|
+
result: T; // Contains workflowId, runId, etc.
|
|
782
|
+
executionTime: number;
|
|
783
|
+
error?: Error;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
interface WorkflowQueryResult<T> {
|
|
787
|
+
success: boolean;
|
|
788
|
+
result: T;
|
|
789
|
+
workflowId: string;
|
|
790
|
+
queryName: string;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
interface WorkflowSignalResult {
|
|
794
|
+
success: boolean;
|
|
795
|
+
workflowId: string;
|
|
796
|
+
signalName: string;
|
|
797
|
+
}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
## Examples
|
|
801
|
+
|
|
802
|
+
## Examples
|
|
803
|
+
|
|
804
|
+
### Example 1: E-commerce Order Processing
|
|
805
|
+
|
|
806
|
+
Complete example with compensation logic:
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
// order.activity.ts
|
|
810
|
+
@Injectable()
|
|
811
|
+
@Activity({ name: 'order-activities' })
|
|
812
|
+
export class OrderActivity {
|
|
813
|
+
constructor(
|
|
814
|
+
private readonly paymentService: PaymentService,
|
|
815
|
+
private readonly inventoryService: InventoryService,
|
|
816
|
+
private readonly emailService: EmailService,
|
|
817
|
+
) {}
|
|
818
|
+
|
|
819
|
+
@ActivityMethod('validatePayment')
|
|
820
|
+
async validatePayment(paymentData: PaymentData): Promise<PaymentResult> {
|
|
821
|
+
return await this.paymentService.validate(paymentData);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
@ActivityMethod('chargePayment')
|
|
825
|
+
async chargePayment(paymentData: PaymentData): Promise<{ transactionId: string }> {
|
|
826
|
+
return await this.paymentService.charge(paymentData);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
@ActivityMethod('refundPayment')
|
|
830
|
+
async refundPayment(transactionId: string): Promise<void> {
|
|
831
|
+
await this.paymentService.refund(transactionId);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
@ActivityMethod('reserveInventory')
|
|
835
|
+
async reserveInventory(items: OrderItem[]): Promise<{ reservationId: string }> {
|
|
836
|
+
return await this.inventoryService.reserve(items);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
@ActivityMethod('releaseInventory')
|
|
840
|
+
async releaseInventory(reservationId: string): Promise<void> {
|
|
841
|
+
await this.inventoryService.release(reservationId);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
@ActivityMethod('sendConfirmationEmail')
|
|
845
|
+
async sendConfirmationEmail(order: Order): Promise<void> {
|
|
846
|
+
await this.emailService.sendConfirmation(order);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// order.workflow.ts
|
|
851
|
+
import { proxyActivities, defineSignal, defineQuery, setHandler } from '@temporalio/workflow';
|
|
852
|
+
import type { OrderActivity } from './order.activity';
|
|
853
|
+
|
|
854
|
+
const {
|
|
855
|
+
validatePayment,
|
|
856
|
+
chargePayment,
|
|
857
|
+
refundPayment,
|
|
858
|
+
reserveInventory,
|
|
859
|
+
releaseInventory,
|
|
860
|
+
sendConfirmationEmail,
|
|
861
|
+
} = proxyActivities<typeof OrderActivity.prototype>({
|
|
862
|
+
startToCloseTimeout: '5m',
|
|
863
|
+
retry: { maximumAttempts: 3 },
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
export const cancelOrderSignal = defineSignal<[string]>('cancelOrder');
|
|
867
|
+
export const getOrderStatusQuery = defineQuery<OrderStatus>('getOrderStatus');
|
|
868
|
+
|
|
869
|
+
export async function processOrderWorkflow(orderData: OrderData): Promise<OrderResult> {
|
|
870
|
+
let status: OrderStatus = 'pending';
|
|
871
|
+
let transactionId: string | undefined;
|
|
872
|
+
let reservationId: string | undefined;
|
|
873
|
+
let cancelled = false;
|
|
874
|
+
|
|
875
|
+
setHandler(cancelOrderSignal, (reason: string) => {
|
|
876
|
+
cancelled = true;
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
setHandler(getOrderStatusQuery, () => status);
|
|
880
|
+
|
|
881
|
+
try {
|
|
882
|
+
// Step 1: Validate payment
|
|
883
|
+
status = 'validating_payment';
|
|
884
|
+
const paymentValid = await validatePayment(orderData.payment);
|
|
885
|
+
if (!paymentValid.valid) {
|
|
886
|
+
throw new Error('Invalid payment method');
|
|
803
887
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
888
|
+
|
|
889
|
+
// Check cancellation
|
|
890
|
+
if (cancelled) {
|
|
891
|
+
status = 'cancelled';
|
|
892
|
+
return { orderId: orderData.orderId, status };
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Step 2: Reserve inventory
|
|
896
|
+
status = 'reserving_inventory';
|
|
897
|
+
const reservation = await reserveInventory(orderData.items);
|
|
898
|
+
reservationId = reservation.reservationId;
|
|
899
|
+
|
|
900
|
+
// Step 3: Charge payment
|
|
901
|
+
status = 'charging_payment';
|
|
902
|
+
const payment = await chargePayment(orderData.payment);
|
|
903
|
+
transactionId = payment.transactionId;
|
|
904
|
+
|
|
905
|
+
// Step 4: Send confirmation
|
|
906
|
+
status = 'sending_confirmation';
|
|
907
|
+
await sendConfirmationEmail({
|
|
908
|
+
orderId: orderData.orderId,
|
|
909
|
+
items: orderData.items,
|
|
910
|
+
total: orderData.totalAmount,
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
status = 'completed';
|
|
914
|
+
return {
|
|
915
|
+
orderId: orderData.orderId,
|
|
916
|
+
status,
|
|
917
|
+
transactionId,
|
|
918
|
+
reservationId,
|
|
919
|
+
};
|
|
920
|
+
} catch (error) {
|
|
921
|
+
// Compensation logic
|
|
922
|
+
status = 'compensating';
|
|
923
|
+
|
|
924
|
+
if (reservationId) {
|
|
925
|
+
await releaseInventory(reservationId);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (transactionId) {
|
|
929
|
+
await refundPayment(transactionId);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
status = 'failed';
|
|
933
|
+
throw error;
|
|
809
934
|
}
|
|
810
|
-
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// order.service.ts
|
|
938
|
+
@Injectable()
|
|
939
|
+
export class OrderService {
|
|
940
|
+
constructor(private readonly temporal: TemporalService) {}
|
|
941
|
+
|
|
942
|
+
async createOrder(orderData: OrderData) {
|
|
943
|
+
const result = await this.temporal.startWorkflow(
|
|
944
|
+
'processOrderWorkflow',
|
|
945
|
+
[orderData],
|
|
946
|
+
{
|
|
947
|
+
workflowId: `order-${orderData.orderId}`,
|
|
948
|
+
taskQueue: 'order-queue',
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
return result.result;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async getOrderStatus(orderId: string) {
|
|
956
|
+
const result = await this.temporal.queryWorkflow(
|
|
957
|
+
`order-${orderId}`,
|
|
958
|
+
'getOrderStatus'
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
return result.result;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async cancelOrder(orderId: string, reason: string) {
|
|
965
|
+
await this.temporal.signalWorkflow(
|
|
966
|
+
`order-${orderId}`,
|
|
967
|
+
'cancelOrder',
|
|
968
|
+
[reason]
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
811
972
|
```
|
|
812
973
|
|
|
813
|
-
|
|
974
|
+
### Example 2: Scheduled Reports
|
|
814
975
|
|
|
815
|
-
|
|
816
|
-
- Keep activities idempotent
|
|
817
|
-
- Use proper timeouts and retry policies
|
|
818
|
-
- Handle errors gracefully
|
|
819
|
-
- Use dependency injection for testability
|
|
976
|
+
Creating and managing scheduled workflows:
|
|
820
977
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
978
|
+
```typescript
|
|
979
|
+
// report.activity.ts
|
|
980
|
+
@Injectable()
|
|
981
|
+
@Activity({ name: 'report-activities' })
|
|
982
|
+
export class ReportActivity {
|
|
983
|
+
constructor(
|
|
984
|
+
private readonly reportService: ReportService,
|
|
985
|
+
private readonly storageService: StorageService,
|
|
986
|
+
private readonly notificationService: NotificationService,
|
|
987
|
+
) {}
|
|
988
|
+
|
|
989
|
+
@ActivityMethod('generateSalesReport')
|
|
990
|
+
async generateSalesReport(period: ReportPeriod): Promise<ReportData> {
|
|
991
|
+
return await this.reportService.generateSales(period);
|
|
992
|
+
}
|
|
826
993
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
- Validate configuration at startup
|
|
994
|
+
@ActivityMethod('uploadReport')
|
|
995
|
+
async uploadReport(reportData: ReportData): Promise<string> {
|
|
996
|
+
return await this.storageService.upload(reportData);
|
|
997
|
+
}
|
|
832
998
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
999
|
+
@ActivityMethod('notifyStakeholders')
|
|
1000
|
+
async notifyStakeholders(reportUrl: string, recipients: string[]): Promise<void> {
|
|
1001
|
+
await this.notificationService.send(recipients, reportUrl);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
838
1004
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
- Enable TLS for security
|
|
843
|
-
- Implement graceful shutdowns
|
|
1005
|
+
// report.workflow.ts
|
|
1006
|
+
import { proxyActivities } from '@temporalio/workflow';
|
|
1007
|
+
import type { ReportActivity } from './report.activity';
|
|
844
1008
|
|
|
845
|
-
|
|
1009
|
+
const { generateSalesReport, uploadReport, notifyStakeholders } =
|
|
1010
|
+
proxyActivities<typeof ReportActivity.prototype>({
|
|
1011
|
+
startToCloseTimeout: '10m',
|
|
1012
|
+
});
|
|
846
1013
|
|
|
847
|
-
|
|
1014
|
+
export async function weeklyReportWorkflow(): Promise<ReportResult> {
|
|
1015
|
+
const endDate = new Date();
|
|
1016
|
+
const startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
848
1017
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1018
|
+
// Generate report
|
|
1019
|
+
const reportData = await generateSalesReport({
|
|
1020
|
+
startDate,
|
|
1021
|
+
endDate,
|
|
1022
|
+
type: 'weekly',
|
|
1023
|
+
});
|
|
852
1024
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
- `@Cron(expression, options?)` - Schedule using cron expression
|
|
856
|
-
- `@Interval(interval, options?)` - Schedule using interval expression
|
|
1025
|
+
// Upload to storage
|
|
1026
|
+
const reportUrl = await uploadReport(reportData);
|
|
857
1027
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
- `@Query(nameOrOptions?)` - Mark a method as a query handler
|
|
1028
|
+
// Notify stakeholders
|
|
1029
|
+
await notifyStakeholders(reportUrl, ['management@company.com']);
|
|
861
1030
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1031
|
+
return {
|
|
1032
|
+
reportUrl,
|
|
1033
|
+
generatedAt: new Date(),
|
|
1034
|
+
period: { startDate, endDate },
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
868
1037
|
|
|
869
|
-
|
|
1038
|
+
// schedule.service.ts
|
|
1039
|
+
@Injectable()
|
|
1040
|
+
export class ReportScheduleService {
|
|
1041
|
+
constructor(private readonly temporal: TemporalService) {}
|
|
870
1042
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1043
|
+
async setupWeeklyReports() {
|
|
1044
|
+
await this.temporal.createSchedule({
|
|
1045
|
+
scheduleId: 'weekly-sales-report',
|
|
1046
|
+
spec: {
|
|
1047
|
+
cronExpressions: ['0 9 * * MON'], // Every Monday at 9 AM
|
|
1048
|
+
},
|
|
1049
|
+
action: {
|
|
1050
|
+
type: 'startWorkflow',
|
|
1051
|
+
workflowType: 'weeklyReportWorkflow',
|
|
1052
|
+
taskQueue: 'reports-queue',
|
|
1053
|
+
},
|
|
1054
|
+
policies: {
|
|
1055
|
+
overlap: 'SKIP',
|
|
1056
|
+
catchupWindow: '1 hour',
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
876
1060
|
|
|
877
|
-
|
|
1061
|
+
async deleteSchedule(scheduleId: string) {
|
|
1062
|
+
await this.temporal.deleteSchedule(scheduleId);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async listAllSchedules() {
|
|
1066
|
+
return await this.temporal.listSchedules();
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
```
|
|
878
1070
|
|
|
879
|
-
|
|
880
|
-
- `isValidCronExpression(cron: string): boolean` - Validate cron format
|
|
881
|
-
- `isValidIntervalExpression(interval: string): boolean` - Validate interval format
|
|
1071
|
+
## Advanced Usage
|
|
882
1072
|
|
|
883
|
-
|
|
884
|
-
- `isActivity(target: object): boolean` - Check if class is an activity
|
|
885
|
-
- `getActivityMetadata(target: object)` - Get activity metadata
|
|
886
|
-
- `isActivityMethod(target: object): boolean` - Check if method is activity method
|
|
887
|
-
- `getActivityMethodMetadata(target: object)` - Get activity method metadata
|
|
888
|
-
- `getParameterMetadata(target: object, propertyKey: string | symbol)` - Get parameter metadata
|
|
1073
|
+
### Activity Retry Configuration
|
|
889
1074
|
|
|
890
|
-
|
|
891
|
-
- `TemporalLogger` - Enhanced logger with context support
|
|
892
|
-
- `TemporalLoggerManager` - Global logger configuration
|
|
1075
|
+
Configure custom retry policies for different activity types:
|
|
893
1076
|
|
|
894
|
-
|
|
1077
|
+
```typescript
|
|
1078
|
+
// workflow.ts
|
|
1079
|
+
const paymentActivities = proxyActivities<typeof PaymentActivity.prototype>({
|
|
1080
|
+
startToCloseTimeout: '5m',
|
|
1081
|
+
retry: {
|
|
1082
|
+
maximumAttempts: 5,
|
|
1083
|
+
initialInterval: '1s',
|
|
1084
|
+
maximumInterval: '1m',
|
|
1085
|
+
backoffCoefficient: 2,
|
|
1086
|
+
nonRetryableErrorTypes: ['InvalidPaymentMethod', 'InsufficientFunds'],
|
|
1087
|
+
},
|
|
1088
|
+
});
|
|
895
1089
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1090
|
+
const emailActivities = proxyActivities<typeof EmailActivity.prototype>({
|
|
1091
|
+
startToCloseTimeout: '2m',
|
|
1092
|
+
retry: {
|
|
1093
|
+
maximumAttempts: 3,
|
|
1094
|
+
initialInterval: '500ms',
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
```
|
|
899
1098
|
|
|
900
|
-
|
|
901
|
-
- `TIMEOUTS` - Common timeout values for different operation types
|
|
902
|
-
- `RETRY_POLICIES` - Predefined retry policies (QUICK, STANDARD, AGGRESSIVE)
|
|
1099
|
+
### Workflow Testing
|
|
903
1100
|
|
|
904
|
-
|
|
905
|
-
- `TEMPORAL_MODULE_OPTIONS` - Main module configuration token
|
|
906
|
-
- `TEMPORAL_CLIENT` - Client instance injection token
|
|
907
|
-
- `TEMPORAL_CONNECTION` - Connection instance injection token
|
|
1101
|
+
Test workflows using Temporal's testing framework:
|
|
908
1102
|
|
|
909
|
-
|
|
1103
|
+
```typescript
|
|
1104
|
+
import { TestWorkflowEnvironment } from '@temporalio/testing';
|
|
1105
|
+
import { Worker } from '@temporalio/worker';
|
|
1106
|
+
import { processOrderWorkflow } from './order.workflow';
|
|
1107
|
+
import { OrderActivity } from './order.activity';
|
|
910
1108
|
|
|
911
|
-
|
|
1109
|
+
describe('Order Workflow', () => {
|
|
1110
|
+
let testEnv: TestWorkflowEnvironment;
|
|
912
1111
|
|
|
913
|
-
|
|
1112
|
+
beforeAll(async () => {
|
|
1113
|
+
testEnv = await TestWorkflowEnvironment.createTimeSkipping();
|
|
1114
|
+
});
|
|
914
1115
|
|
|
915
|
-
|
|
1116
|
+
afterAll(async () => {
|
|
1117
|
+
await testEnv?.teardown();
|
|
1118
|
+
});
|
|
916
1119
|
|
|
917
|
-
|
|
1120
|
+
it('should process order successfully', async () => {
|
|
1121
|
+
const { client, nativeConnection } = testEnv;
|
|
918
1122
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1123
|
+
// Mock activities
|
|
1124
|
+
const mockOrderActivity = {
|
|
1125
|
+
validatePayment: async () => ({ valid: true }),
|
|
1126
|
+
reserveInventory: async () => ({ reservationId: 'res-123' }),
|
|
1127
|
+
chargePayment: async () => ({ transactionId: 'txn-123' }),
|
|
1128
|
+
sendConfirmationEmail: async () => {},
|
|
1129
|
+
};
|
|
922
1130
|
|
|
923
|
-
|
|
1131
|
+
const worker = await Worker.create({
|
|
1132
|
+
connection: nativeConnection,
|
|
1133
|
+
taskQueue: 'test',
|
|
1134
|
+
workflowsPath: require.resolve('./order.workflow'),
|
|
1135
|
+
activities: mockOrderActivity,
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
await worker.runUntil(async () => {
|
|
1139
|
+
const result = await client.workflow.execute(processOrderWorkflow, {
|
|
1140
|
+
workflowId: 'test-order-1',
|
|
1141
|
+
taskQueue: 'test',
|
|
1142
|
+
args: [{
|
|
1143
|
+
orderId: 'order-123',
|
|
1144
|
+
payment: { amount: 100, currency: 'USD' },
|
|
1145
|
+
items: [{ id: '1', quantity: 1 }],
|
|
1146
|
+
}],
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
expect(result.status).toBe('completed');
|
|
1150
|
+
expect(result.transactionId).toBe('txn-123');
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
```
|
|
924
1155
|
|
|
925
|
-
|
|
1156
|
+
### Child Workflows
|
|
926
1157
|
|
|
927
|
-
|
|
1158
|
+
Organize complex workflows using child workflows:
|
|
928
1159
|
|
|
929
|
-
|
|
1160
|
+
```typescript
|
|
1161
|
+
// parent.workflow.ts
|
|
1162
|
+
import { startChild } from '@temporalio/workflow';
|
|
1163
|
+
|
|
1164
|
+
export async function parentWorkflow(orderId: string) {
|
|
1165
|
+
// Start child workflows
|
|
1166
|
+
const paymentHandle = await startChild(processPaymentWorkflow, {
|
|
1167
|
+
workflowId: `payment-${orderId}`,
|
|
1168
|
+
args: [paymentData],
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
const shippingHandle = await startChild(processShippingWorkflow, {
|
|
1172
|
+
workflowId: `shipping-${orderId}`,
|
|
1173
|
+
args: [shippingData],
|
|
1174
|
+
});
|
|
930
1175
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1176
|
+
// Wait for both to complete
|
|
1177
|
+
const [paymentResult, shippingResult] = await Promise.all([
|
|
1178
|
+
paymentHandle.result(),
|
|
1179
|
+
shippingHandle.result(),
|
|
1180
|
+
]);
|
|
1181
|
+
|
|
1182
|
+
return {
|
|
1183
|
+
payment: paymentResult,
|
|
1184
|
+
shipping: shippingResult,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
### Continue-As-New for Long-Running Workflows
|
|
1190
|
+
|
|
1191
|
+
Use continue-as-new to prevent event history from growing too large:
|
|
939
1192
|
|
|
940
1193
|
```typescript
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1194
|
+
import { continueAsNew } from '@temporalio/workflow';
|
|
1195
|
+
|
|
1196
|
+
export async function processEventStreamWorkflow(cursor: number): Promise<void> {
|
|
1197
|
+
const events = await fetchEvents(cursor);
|
|
1198
|
+
|
|
1199
|
+
for (const event of events) {
|
|
1200
|
+
await processEvent(event);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Continue as new after processing 1000 events
|
|
1204
|
+
if (events.length >= 1000) {
|
|
1205
|
+
await continueAsNew<typeof processEventStreamWorkflow>(cursor + events.length);
|
|
1206
|
+
}
|
|
944
1207
|
}
|
|
945
1208
|
```
|
|
946
1209
|
|
|
947
|
-
###
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
- Enables parameter injection (workflowId, context, etc.)
|
|
951
|
-
- Useful for advanced scenarios (e.g., dynamic metadata, dependency injection)
|
|
952
|
-
- Can organize signals/queries as class methods
|
|
953
|
-
- **When to Use:**
|
|
954
|
-
- When you need to access workflow context, IDs, or inject dependencies
|
|
955
|
-
- When you want to group signals/queries with workflow logic
|
|
1210
|
+
### Custom Error Handling
|
|
1211
|
+
|
|
1212
|
+
Implement custom error types and handling:
|
|
956
1213
|
|
|
957
1214
|
```typescript
|
|
958
|
-
|
|
959
|
-
export class
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
@WorkflowContext() context: any
|
|
964
|
-
) {
|
|
965
|
-
// ...
|
|
1215
|
+
// activities
|
|
1216
|
+
export class RetryableError extends Error {
|
|
1217
|
+
constructor(message: string) {
|
|
1218
|
+
super(message);
|
|
1219
|
+
this.name = 'RetryableError';
|
|
966
1220
|
}
|
|
967
1221
|
}
|
|
1222
|
+
|
|
1223
|
+
export class NonRetryableError extends Error {
|
|
1224
|
+
constructor(message: string) {
|
|
1225
|
+
super(message);
|
|
1226
|
+
this.name = 'NonRetryableError';
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
@ActivityMethod('processData')
|
|
1231
|
+
async processData(data: any): Promise<any> {
|
|
1232
|
+
try {
|
|
1233
|
+
return await this.externalApi.process(data);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
if (error.code === 'RATE_LIMIT') {
|
|
1236
|
+
throw new RetryableError('Rate limit exceeded, will retry');
|
|
1237
|
+
} else if (error.code === 'INVALID_DATA') {
|
|
1238
|
+
throw new NonRetryableError('Invalid data format');
|
|
1239
|
+
}
|
|
1240
|
+
throw error;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// workflow configuration
|
|
1245
|
+
const activities = proxyActivities<typeof DataActivity.prototype>({
|
|
1246
|
+
startToCloseTimeout: '5m',
|
|
1247
|
+
retry: {
|
|
1248
|
+
nonRetryableErrorTypes: ['NonRetryableError'],
|
|
1249
|
+
},
|
|
1250
|
+
});
|
|
968
1251
|
```
|
|
969
1252
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1253
|
+
## Best Practices
|
|
1254
|
+
|
|
1255
|
+
### 1. Workflow Design
|
|
1256
|
+
|
|
1257
|
+
**✅ DO:**
|
|
1258
|
+
- Keep workflows deterministic (no random numbers, current time, network calls)
|
|
1259
|
+
- Use activities for any non-deterministic operations
|
|
1260
|
+
- Keep workflow history size manageable (use continue-as-new for long-running workflows)
|
|
1261
|
+
- Export workflow functions (not classes)
|
|
1262
|
+
- Use `defineSignal` and `defineQuery` at module level
|
|
1263
|
+
|
|
1264
|
+
**❌ DON'T:**
|
|
1265
|
+
- Don't use `@Injectable()` on workflow functions
|
|
1266
|
+
- Don't inject NestJS services in workflows
|
|
1267
|
+
- Don't use `Math.random()` or `Date.now()` directly in workflows
|
|
1268
|
+
- Don't make HTTP calls or database queries directly in workflows
|
|
1269
|
+
|
|
1270
|
+
### 2. Activity Design
|
|
1271
|
+
|
|
1272
|
+
**✅ DO:**
|
|
1273
|
+
- Make activities idempotent (safe to retry)
|
|
1274
|
+
- Use `@Injectable()` and leverage NestJS DI
|
|
1275
|
+
- Use `@Activity()` and `@ActivityMethod()` decorators
|
|
1276
|
+
- Handle errors appropriately
|
|
1277
|
+
- Log activity execution for debugging
|
|
1278
|
+
|
|
1279
|
+
**❌ DON'T:**
|
|
1280
|
+
- Don't make activities too granular (network overhead)
|
|
1281
|
+
- Don't rely on activity execution order guarantees
|
|
1282
|
+
- Don't share mutable state between activity invocations
|
|
1283
|
+
|
|
1284
|
+
### 3. Configuration
|
|
1285
|
+
|
|
1286
|
+
**✅ DO:**
|
|
1287
|
+
- Use async configuration for environment-based setup
|
|
1288
|
+
- Configure appropriate timeouts for your use case
|
|
1289
|
+
- Set up proper retry policies
|
|
1290
|
+
- Enable graceful shutdown hooks
|
|
1291
|
+
- Use task queues to organize work
|
|
1292
|
+
|
|
1293
|
+
**❌ DON'T:**
|
|
1294
|
+
- Don't hardcode connection strings
|
|
1295
|
+
- Don't use the same task queue for all workflows
|
|
1296
|
+
- Don't ignore timeout configurations
|
|
1297
|
+
|
|
1298
|
+
### 4. Error Handling
|
|
1299
|
+
|
|
1300
|
+
**✅ DO:**
|
|
1301
|
+
- Implement compensation logic in workflows
|
|
1302
|
+
- Use appropriate retry policies
|
|
1303
|
+
- Log errors with context
|
|
1304
|
+
- Define non-retryable error types
|
|
1305
|
+
- Handle activity failures gracefully
|
|
1306
|
+
|
|
1307
|
+
**❌ DON'T:**
|
|
1308
|
+
- Don't swallow errors silently
|
|
1309
|
+
- Don't retry indefinitely
|
|
1310
|
+
- Don't ignore business-level failures
|
|
1311
|
+
|
|
1312
|
+
### 5. Testing
|
|
1313
|
+
|
|
1314
|
+
**✅ DO:**
|
|
1315
|
+
- Write unit tests for activities
|
|
1316
|
+
- Use TestWorkflowEnvironment for integration tests
|
|
1317
|
+
- Mock external dependencies
|
|
1318
|
+
- Test failure scenarios
|
|
1319
|
+
- Test signal and query handlers
|
|
1320
|
+
|
|
1321
|
+
**❌ DON'T:**
|
|
1322
|
+
- Don't skip workflow testing
|
|
1323
|
+
- Don't test against production Temporal server
|
|
1324
|
+
- Don't assume workflows are correct without testing
|
|
1325
|
+
|
|
1326
|
+
## Health Monitoring
|
|
1327
|
+
|
|
1328
|
+
The package includes comprehensive health monitoring capabilities for production deployments.
|
|
1329
|
+
|
|
1330
|
+
### Using Built-in Health Module
|
|
1331
|
+
|
|
1332
|
+
```typescript
|
|
1333
|
+
// app.module.ts
|
|
1334
|
+
import { Module } from '@nestjs/common';
|
|
1335
|
+
import { TemporalModule } from 'nestjs-temporal-core';
|
|
1336
|
+
import { TemporalHealthModule } from 'nestjs-temporal-core/health';
|
|
1337
|
+
|
|
1338
|
+
@Module({
|
|
1339
|
+
imports: [
|
|
1340
|
+
TemporalModule.register({
|
|
1341
|
+
connection: { address: 'localhost:7233' },
|
|
1342
|
+
taskQueue: 'my-queue',
|
|
1343
|
+
worker: {
|
|
1344
|
+
workflowsPath: require.resolve('./workflows'),
|
|
1345
|
+
activityClasses: [MyActivity],
|
|
1346
|
+
},
|
|
1347
|
+
}),
|
|
1348
|
+
TemporalHealthModule, // Adds /health/temporal endpoint
|
|
1349
|
+
],
|
|
1350
|
+
})
|
|
1351
|
+
export class AppModule {}
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
### Custom Health Checks
|
|
1355
|
+
|
|
1356
|
+
```typescript
|
|
1357
|
+
@Controller('health')
|
|
1358
|
+
export class HealthController {
|
|
1359
|
+
constructor(private readonly temporal: TemporalService) {}
|
|
1360
|
+
|
|
1361
|
+
@Get('/status')
|
|
1362
|
+
async getHealthStatus() {
|
|
1363
|
+
const health = this.temporal.getHealth();
|
|
1364
|
+
|
|
1365
|
+
return {
|
|
1366
|
+
status: health.overallHealth,
|
|
1367
|
+
timestamp: new Date(),
|
|
1368
|
+
services: {
|
|
1369
|
+
client: {
|
|
1370
|
+
healthy: health.client.status === 'healthy',
|
|
1371
|
+
connection: health.client.connectionStatus,
|
|
1372
|
+
},
|
|
1373
|
+
worker: {
|
|
1374
|
+
healthy: health.worker.status === 'healthy',
|
|
1375
|
+
state: health.worker.state,
|
|
1376
|
+
activitiesRegistered: health.worker.activitiesCount,
|
|
1377
|
+
},
|
|
1378
|
+
discovery: {
|
|
1379
|
+
healthy: health.discovery.status === 'healthy',
|
|
1380
|
+
activitiesDiscovered: health.discovery.activitiesDiscovered,
|
|
1381
|
+
},
|
|
1382
|
+
},
|
|
1383
|
+
uptime: health.uptime,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
@Get('/detailed')
|
|
1388
|
+
async getDetailedHealth() {
|
|
1389
|
+
const health = this.temporal.getHealth();
|
|
1390
|
+
const stats = this.temporal.getStatistics();
|
|
1391
|
+
|
|
1392
|
+
return {
|
|
1393
|
+
health,
|
|
1394
|
+
statistics: stats,
|
|
1395
|
+
performance: {
|
|
1396
|
+
workflowStartLatency: stats.averageWorkflowStartTime,
|
|
1397
|
+
activityExecutionCount: stats.totalActivitiesExecuted,
|
|
1398
|
+
},
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
### Health Check Response
|
|
1405
|
+
|
|
1406
|
+
```typescript
|
|
1407
|
+
interface ServiceHealth {
|
|
1408
|
+
overallHealth: 'healthy' | 'degraded' | 'unhealthy';
|
|
1409
|
+
client: {
|
|
1410
|
+
status: 'healthy' | 'unhealthy';
|
|
1411
|
+
connectionStatus: 'connected' | 'disconnected';
|
|
1412
|
+
};
|
|
1413
|
+
worker: {
|
|
1414
|
+
status: 'healthy' | 'unhealthy';
|
|
1415
|
+
state: 'RUNNING' | 'STOPPED' | 'FAILED';
|
|
1416
|
+
activitiesCount: number;
|
|
1417
|
+
};
|
|
1418
|
+
discovery: {
|
|
1419
|
+
status: 'healthy' | 'unhealthy';
|
|
1420
|
+
activitiesDiscovered: number;
|
|
1421
|
+
};
|
|
1422
|
+
uptime: number;
|
|
1423
|
+
lastChecked: Date;
|
|
1424
|
+
}
|
|
1425
|
+
```
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
## Troubleshooting
|
|
1429
|
+
|
|
1430
|
+
### Common Issues and Solutions
|
|
1431
|
+
|
|
1432
|
+
#### 1. Connection Errors
|
|
1433
|
+
|
|
1434
|
+
**Problem:** Cannot connect to Temporal server
|
|
1435
|
+
|
|
1436
|
+
```
|
|
1437
|
+
Error: Failed to connect to localhost:7233
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1440
|
+
**Solutions:**
|
|
1441
|
+
```typescript
|
|
1442
|
+
// Check connection configuration
|
|
1443
|
+
const health = temporalService.getHealth();
|
|
1444
|
+
console.log('Connection status:', health.client.connectionStatus);
|
|
1445
|
+
|
|
1446
|
+
// Verify Temporal server is running
|
|
1447
|
+
// docker ps | grep temporal
|
|
1448
|
+
|
|
1449
|
+
// Check connection settings
|
|
1450
|
+
TemporalModule.register({
|
|
1451
|
+
connection: {
|
|
1452
|
+
address: process.env.TEMPORAL_ADDRESS || 'localhost:7233',
|
|
1453
|
+
namespace: 'default',
|
|
1454
|
+
},
|
|
1455
|
+
})
|
|
1456
|
+
```
|
|
1457
|
+
|
|
1458
|
+
#### 2. Activity Not Found
|
|
1459
|
+
|
|
1460
|
+
**Problem:** Workflow cannot find registered activities
|
|
1461
|
+
|
|
1462
|
+
```
|
|
1463
|
+
Error: Activity 'myActivity' not found
|
|
1464
|
+
```
|
|
1465
|
+
|
|
1466
|
+
**Solutions:**
|
|
1467
|
+
```typescript
|
|
1468
|
+
// 1. Ensure activity is in activityClasses array
|
|
1469
|
+
TemporalModule.register({
|
|
1470
|
+
worker: {
|
|
1471
|
+
activityClasses: [MyActivity], // Must include the activity class
|
|
1472
|
+
},
|
|
1473
|
+
})
|
|
1474
|
+
|
|
1475
|
+
// 2. Verify activity is registered as provider
|
|
1476
|
+
@Module({
|
|
1477
|
+
providers: [MyActivity], // Must be in providers array
|
|
1478
|
+
})
|
|
1479
|
+
|
|
1480
|
+
// 3. Check activity decorator
|
|
1481
|
+
@Activity({ name: 'my-activities' })
|
|
1482
|
+
export class MyActivity {
|
|
1483
|
+
@ActivityMethod('myActivity')
|
|
1484
|
+
async myActivity() { }
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// 4. Check discovery status
|
|
1488
|
+
const health = temporalService.getHealth();
|
|
1489
|
+
console.log('Activities discovered:', health.discovery.activitiesDiscovered);
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
#### 3. Workflow Registration Issues
|
|
1493
|
+
|
|
1494
|
+
**Problem:** Workflow not found or not executing
|
|
1495
|
+
|
|
1496
|
+
```
|
|
1497
|
+
Error: Workflow 'myWorkflow' not found
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
**Solutions:**
|
|
1501
|
+
```typescript
|
|
1502
|
+
// 1. Ensure workflowsPath is correct
|
|
1503
|
+
TemporalModule.register({
|
|
1504
|
+
worker: {
|
|
1505
|
+
workflowsPath: require.resolve('./workflows'), // Must resolve to workflows file/directory
|
|
1506
|
+
},
|
|
1507
|
+
})
|
|
1508
|
+
|
|
1509
|
+
// 2. Export workflow function properly
|
|
1510
|
+
// workflows/index.ts
|
|
1511
|
+
export { processOrderWorkflow } from './order.workflow';
|
|
1512
|
+
export { reportWorkflow } from './report.workflow';
|
|
1513
|
+
|
|
1514
|
+
// 3. Use correct workflow name when starting
|
|
1515
|
+
await temporal.startWorkflow(
|
|
1516
|
+
'processOrderWorkflow', // Must match exported function name
|
|
1517
|
+
[args],
|
|
1518
|
+
options
|
|
1519
|
+
);
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
#### 4. Timeout Issues
|
|
1523
|
+
|
|
1524
|
+
**Problem:** Activities or workflows timing out
|
|
1525
|
+
|
|
1526
|
+
```
|
|
1527
|
+
Error: Activity timed out after 10s
|
|
1528
|
+
```
|
|
1529
|
+
|
|
1530
|
+
**Solutions:**
|
|
1531
|
+
```typescript
|
|
1532
|
+
// Configure appropriate timeouts
|
|
1533
|
+
const activities = proxyActivities<typeof MyActivity.prototype>({
|
|
1534
|
+
startToCloseTimeout: '10m', // Increase for long-running activities
|
|
1535
|
+
scheduleToCloseTimeout: '15m', // Total time including queuing
|
|
1536
|
+
scheduleToStartTimeout: '5m', // Time waiting in queue
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
// For workflows
|
|
1540
|
+
await temporal.startWorkflow('myWorkflow', [args], {
|
|
1541
|
+
workflowExecutionTimeout: '24h', // Max total execution time
|
|
1542
|
+
workflowRunTimeout: '12h', // Max single run time
|
|
1543
|
+
workflowTaskTimeout: '10s', // Decision task timeout
|
|
1544
|
+
});
|
|
1545
|
+
```
|
|
1546
|
+
|
|
1547
|
+
#### 5. Worker Not Starting
|
|
1548
|
+
|
|
1549
|
+
**Problem:** Worker fails to start or crashes
|
|
1550
|
+
|
|
1551
|
+
```
|
|
1552
|
+
Error: Worker failed to start
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
**Solutions:**
|
|
1556
|
+
```typescript
|
|
1557
|
+
// 1. Check worker configuration
|
|
1558
|
+
TemporalModule.register({
|
|
1559
|
+
worker: {
|
|
1560
|
+
autoStart: true, // Ensure autoStart is true
|
|
1561
|
+
workflowsPath: require.resolve('./workflows'),
|
|
1562
|
+
activityClasses: [MyActivity],
|
|
1563
|
+
},
|
|
1564
|
+
})
|
|
1565
|
+
|
|
1566
|
+
// 2. Check logs
|
|
1567
|
+
// Enable debug logging
|
|
1568
|
+
TemporalModule.register({
|
|
1569
|
+
logLevel: 'debug',
|
|
1570
|
+
enableLogger: true,
|
|
1571
|
+
})
|
|
1572
|
+
|
|
1573
|
+
// 3. Verify worker health
|
|
1574
|
+
const health = temporalService.getHealth();
|
|
1575
|
+
console.log('Worker status:', health.worker.state);
|
|
1576
|
+
|
|
1577
|
+
// 4. Check for port conflicts or resource issues
|
|
1578
|
+
```
|
|
1579
|
+
|
|
1580
|
+
#### 6. Signal/Query Not Working
|
|
1581
|
+
|
|
1582
|
+
**Problem:** Signals or queries not being handled
|
|
1583
|
+
|
|
1584
|
+
**Solutions:**
|
|
1585
|
+
```typescript
|
|
1586
|
+
// 1. Define signals/queries at module level (not inside workflow)
|
|
1587
|
+
export const mySignal = defineSignal<[string]>('mySignal');
|
|
1588
|
+
export const myQuery = defineQuery<string>('myQuery');
|
|
1589
|
+
|
|
1590
|
+
// 2. Set up handlers in workflow
|
|
1591
|
+
export async function myWorkflow() {
|
|
1592
|
+
let value = 'initial';
|
|
1593
|
+
|
|
1594
|
+
setHandler(mySignal, (newValue: string) => {
|
|
1595
|
+
value = newValue;
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
setHandler(myQuery, () => value);
|
|
1599
|
+
|
|
1600
|
+
// ... workflow logic
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// 3. Use correct names when signaling/querying
|
|
1604
|
+
await temporal.signalWorkflow(workflowId, 'mySignal', ['newValue']);
|
|
1605
|
+
const result = await temporal.queryWorkflow(workflowId, 'myQuery');
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
### Debug Mode
|
|
1609
|
+
|
|
1610
|
+
Enable comprehensive debugging:
|
|
1611
|
+
|
|
1612
|
+
```typescript
|
|
1613
|
+
TemporalModule.register({
|
|
1614
|
+
logLevel: 'debug',
|
|
1615
|
+
enableLogger: true,
|
|
1616
|
+
connection: {
|
|
1617
|
+
address: 'localhost:7233',
|
|
1618
|
+
},
|
|
1619
|
+
worker: {
|
|
1620
|
+
debugMode: true, // If available
|
|
1621
|
+
},
|
|
1622
|
+
})
|
|
1623
|
+
|
|
1624
|
+
// Check detailed health and statistics
|
|
1625
|
+
const health = temporalService.getHealth();
|
|
1626
|
+
const stats = temporalService.getStatistics();
|
|
1627
|
+
console.log('Health:', JSON.stringify(health, null, 2));
|
|
1628
|
+
console.log('Stats:', JSON.stringify(stats, null, 2));
|
|
1629
|
+
```
|
|
1630
|
+
|
|
1631
|
+
### Getting Help
|
|
1632
|
+
|
|
1633
|
+
If you're still experiencing issues:
|
|
1634
|
+
|
|
1635
|
+
1. **Check the logs** - Enable debug logging to see detailed information
|
|
1636
|
+
2. **Verify configuration** - Double-check all connection and worker settings
|
|
1637
|
+
3. **Test connectivity** - Ensure Temporal server is accessible
|
|
1638
|
+
4. **Review health status** - Use `getHealth()` to identify failing components
|
|
1639
|
+
5. **Check GitHub Issues** - [Search existing issues](https://github.com/harsh-simform/nestjs-temporal-core/issues)
|
|
1640
|
+
6. **Create an issue** - Provide logs, configuration, and minimal reproduction
|
|
1641
|
+
|
|
1642
|
+
## Requirements
|
|
1643
|
+
|
|
1644
|
+
- **Node.js**: >= 16.0.0
|
|
1645
|
+
- **NestJS**: >= 9.0.0
|
|
1646
|
+
- **Temporal Server**: >= 1.20.0
|
|
1647
|
+
|
|
1648
|
+
## Contributing
|
|
1649
|
+
|
|
1650
|
+
We welcome contributions! To contribute:
|
|
1651
|
+
|
|
1652
|
+
1. Fork the repository
|
|
1653
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
1654
|
+
3. Make your changes
|
|
1655
|
+
4. Run tests (`npm test`)
|
|
1656
|
+
5. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
1657
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
1658
|
+
7. Open a Pull Request
|
|
1659
|
+
|
|
1660
|
+
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
|
|
1661
|
+
|
|
1662
|
+
### Development Setup
|
|
1663
|
+
|
|
1664
|
+
```bash
|
|
1665
|
+
# Clone the repository
|
|
1666
|
+
git clone https://github.com/harsh-simform/nestjs-temporal-core.git
|
|
1667
|
+
cd nestjs-temporal-core
|
|
1668
|
+
|
|
1669
|
+
# Install dependencies
|
|
1670
|
+
npm install
|
|
1671
|
+
|
|
1672
|
+
# Run tests
|
|
1673
|
+
npm test
|
|
1674
|
+
|
|
1675
|
+
# Run tests with coverage
|
|
1676
|
+
npm run test:cov
|
|
1677
|
+
|
|
1678
|
+
# Build the package
|
|
1679
|
+
npm run build
|
|
1680
|
+
|
|
1681
|
+
# Generate documentation
|
|
1682
|
+
npm run docs:generate
|
|
1683
|
+
```
|
|
1684
|
+
|
|
1685
|
+
## License
|
|
1686
|
+
|
|
1687
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
1688
|
+
|
|
1689
|
+
## Support and Resources
|
|
1690
|
+
|
|
1691
|
+
- 📚 **Documentation**: [Full API Documentation](https://harsh-simform.github.io/nestjs-temporal-core/)
|
|
1692
|
+
- 🐛 **Issues**: [GitHub Issues](https://github.com/harsh-simform/nestjs-temporal-core/issues)
|
|
1693
|
+
- 💬 **Discussions**: [GitHub Discussions](https://github.com/harsh-simform/nestjs-temporal-core/discussions)
|
|
1694
|
+
- 📦 **NPM**: [nestjs-temporal-core](https://www.npmjs.com/package/nestjs-temporal-core)
|
|
1695
|
+
- 🔄 **Changelog**: [Releases](https://github.com/harsh-simform/nestjs-temporal-core/releases)
|
|
1696
|
+
- 📖 **Example Project**: [nestjs-temporal-core-example](https://github.com/harsh-simform/nestjs-temporal-core-example)
|
|
1697
|
+
|
|
1698
|
+
## Related Projects
|
|
1699
|
+
|
|
1700
|
+
- [Temporal.io](https://temporal.io/) - The underlying workflow orchestration platform
|
|
1701
|
+
- [NestJS](https://nestjs.com/) - Progressive Node.js framework
|
|
1702
|
+
- [@temporalio/sdk](https://www.npmjs.com/package/@temporalio/client) - Official Temporal TypeScript SDK
|
|
974
1703
|
|
|
975
1704
|
---
|
|
1705
|
+
|