nestjs-temporal-core 3.0.10 → 3.0.12
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/CHANGELOG.md +80 -0
- package/README.md +1755 -693
- 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 +773 -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 +67 -0
- package/dist/services/temporal-worker.service.js +845 -0
- package/dist/services/temporal-worker.service.js.map +1 -0
- package/dist/services/temporal.service.d.ts +92 -0
- package/dist/services/temporal.service.js +621 -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,1935 @@ 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.
|
|
215
|
+
### 5. Use in Services
|
|
250
216
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
### 6. Use in Services
|
|
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
|
-
|
|
287
|
+
### 2. Client-Only Module
|
|
393
288
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
For applications that only start workflows (e.g., web APIs):
|
|
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
339
|
```
|
|
506
340
|
|
|
507
|
-
|
|
341
|
+
## Configuration
|
|
342
|
+
|
|
343
|
+
## Configuration
|
|
344
|
+
|
|
345
|
+
### Basic Configuration
|
|
508
346
|
|
|
509
347
|
```typescript
|
|
510
|
-
|
|
511
|
-
const developmentConfig = {
|
|
348
|
+
TemporalModule.register({
|
|
512
349
|
connection: {
|
|
513
350
|
address: 'localhost:7233',
|
|
514
|
-
namespace: '
|
|
351
|
+
namespace: 'default',
|
|
515
352
|
},
|
|
516
|
-
taskQueue: '
|
|
353
|
+
taskQueue: 'my-task-queue',
|
|
517
354
|
worker: {
|
|
518
|
-
workflowsPath: './
|
|
519
|
-
|
|
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
|
|
355
|
+
workflowsPath: require.resolve('./workflows'),
|
|
356
|
+
activityClasses: [PaymentActivity, EmailActivity],
|
|
357
|
+
autoStart: true,
|
|
358
|
+
maxConcurrentActivityExecutions: 100,
|
|
530
359
|
},
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
workerOptions: WORKER_PRESETS.PRODUCTION_BALANCED
|
|
535
|
-
}
|
|
536
|
-
};
|
|
360
|
+
logLevel: 'info',
|
|
361
|
+
enableLogger: true,
|
|
362
|
+
})
|
|
537
363
|
```
|
|
538
364
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
Control logging behavior across all Temporal modules with configurable logger settings:
|
|
365
|
+
### Multiple Workers Configuration
|
|
542
366
|
|
|
543
|
-
|
|
367
|
+
**New in 3.0.12**: Support for multiple workers with different task queues in the same process.
|
|
544
368
|
|
|
545
369
|
```typescript
|
|
546
|
-
// Enable/disable logging and set log levels
|
|
547
370
|
TemporalModule.register({
|
|
548
371
|
connection: {
|
|
549
372
|
address: 'localhost:7233',
|
|
550
|
-
namespace: 'default'
|
|
373
|
+
namespace: 'default',
|
|
551
374
|
},
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
375
|
+
workers: [
|
|
376
|
+
{
|
|
377
|
+
taskQueue: 'payments-queue',
|
|
378
|
+
workflowsPath: require.resolve('./workflows/payments'),
|
|
379
|
+
activityClasses: [PaymentActivity, RefundActivity],
|
|
380
|
+
autoStart: true,
|
|
381
|
+
workerOptions: {
|
|
382
|
+
maxConcurrentActivityTaskExecutions: 100,
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
taskQueue: 'notifications-queue',
|
|
387
|
+
workflowsPath: require.resolve('./workflows/notifications'),
|
|
388
|
+
activityClasses: [EmailActivity, SmsActivity],
|
|
389
|
+
autoStart: true,
|
|
390
|
+
workerOptions: {
|
|
391
|
+
maxConcurrentActivityTaskExecutions: 50,
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
taskQueue: 'background-jobs',
|
|
396
|
+
workflowsPath: require.resolve('./workflows/jobs'),
|
|
397
|
+
activityClasses: [DataProcessingActivity],
|
|
398
|
+
autoStart: false, // Start manually later
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
logLevel: 'info',
|
|
402
|
+
enableLogger: true,
|
|
560
403
|
})
|
|
561
404
|
```
|
|
562
405
|
|
|
563
|
-
|
|
406
|
+
#### Accessing Multiple Workers
|
|
564
407
|
|
|
565
408
|
```typescript
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
409
|
+
import { Injectable } from '@nestjs/common';
|
|
410
|
+
import { TemporalService } from 'nestjs-temporal-core';
|
|
411
|
+
|
|
412
|
+
@Injectable()
|
|
413
|
+
export class WorkerManagementService {
|
|
414
|
+
constructor(private readonly temporal: TemporalService) {}
|
|
415
|
+
|
|
416
|
+
async checkWorkerStatus() {
|
|
417
|
+
// Get all workers info
|
|
418
|
+
const workersInfo = this.temporal.getAllWorkers();
|
|
419
|
+
console.log(`Total workers: ${workersInfo.totalWorkers}`);
|
|
420
|
+
console.log(`Running workers: ${workersInfo.runningWorkers}`);
|
|
421
|
+
|
|
422
|
+
// Get specific worker status
|
|
423
|
+
const paymentWorkerStatus = this.temporal.getWorkerStatusByTaskQueue('payments-queue');
|
|
424
|
+
if (paymentWorkerStatus?.isHealthy) {
|
|
425
|
+
console.log('Payment worker is healthy');
|
|
426
|
+
}
|
|
578
427
|
}
|
|
579
|
-
};
|
|
580
428
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
429
|
+
async controlWorkers() {
|
|
430
|
+
// Start a specific worker
|
|
431
|
+
await this.temporal.startWorkerByTaskQueue('background-jobs');
|
|
432
|
+
|
|
433
|
+
// Stop a specific worker
|
|
434
|
+
await this.temporal.stopWorkerByTaskQueue('notifications-queue');
|
|
587
435
|
}
|
|
588
|
-
|
|
436
|
+
|
|
437
|
+
async registerNewWorker() {
|
|
438
|
+
// Dynamically register a new worker at runtime
|
|
439
|
+
const result = await this.temporal.registerWorker({
|
|
440
|
+
taskQueue: 'new-queue',
|
|
441
|
+
workflowsPath: require.resolve('./workflows/new'),
|
|
442
|
+
activityClasses: [NewActivity],
|
|
443
|
+
autoStart: true,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (result.success) {
|
|
447
|
+
console.log(`Worker registered for queue: ${result.taskQueue}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
589
451
|
```
|
|
590
452
|
|
|
591
|
-
###
|
|
453
|
+
### Manual Worker Creation (Advanced)
|
|
592
454
|
|
|
593
|
-
|
|
455
|
+
For users who need full control, you can access the native Temporal connection to create custom workers:
|
|
594
456
|
|
|
595
457
|
```typescript
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
enableLogger: true,
|
|
600
|
-
logLevel: 'debug'
|
|
601
|
-
})
|
|
458
|
+
import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
459
|
+
import { TemporalService } from 'nestjs-temporal-core';
|
|
460
|
+
import { Worker } from '@temporalio/worker';
|
|
602
461
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
enableLogger: true,
|
|
607
|
-
logLevel: 'error' // Only show errors
|
|
608
|
-
})
|
|
462
|
+
@Injectable()
|
|
463
|
+
export class CustomWorkerService implements OnModuleInit {
|
|
464
|
+
private customWorker: Worker;
|
|
609
465
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
466
|
+
constructor(private readonly temporal: TemporalService) {}
|
|
467
|
+
|
|
468
|
+
async onModuleInit() {
|
|
469
|
+
const workerManager = this.temporal.getWorkerManager();
|
|
470
|
+
const connection = workerManager.getConnection();
|
|
471
|
+
|
|
472
|
+
if (!connection) {
|
|
473
|
+
throw new Error('No connection available');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Create your custom worker using the native Temporal SDK
|
|
477
|
+
this.customWorker = await Worker.create({
|
|
478
|
+
connection,
|
|
479
|
+
taskQueue: 'custom-task-queue',
|
|
480
|
+
namespace: 'default',
|
|
481
|
+
workflowsPath: require.resolve('./workflows/custom'),
|
|
482
|
+
activities: {
|
|
483
|
+
myCustomActivity: async (data: string) => {
|
|
484
|
+
return `Processed: ${data}`;
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Start the worker
|
|
490
|
+
await this.customWorker.run();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
615
493
|
```
|
|
616
494
|
|
|
617
|
-
###
|
|
495
|
+
### Async Configuration
|
|
496
|
+
|
|
497
|
+
For dynamic configuration using environment variables or config services:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
// config/temporal.config.ts
|
|
501
|
+
import { Injectable } from '@nestjs/common';
|
|
502
|
+
import { ConfigService } from '@nestjs/config';
|
|
503
|
+
import { TemporalOptionsFactory, TemporalOptions } from 'nestjs-temporal-core';
|
|
504
|
+
|
|
505
|
+
@Injectable()
|
|
506
|
+
export class TemporalConfigService implements TemporalOptionsFactory {
|
|
507
|
+
constructor(private configService: ConfigService) {}
|
|
508
|
+
|
|
509
|
+
createTemporalOptions(): TemporalOptions {
|
|
510
|
+
return {
|
|
511
|
+
connection: {
|
|
512
|
+
address: this.configService.get('TEMPORAL_ADDRESS', 'localhost:7233'),
|
|
513
|
+
namespace: this.configService.get('TEMPORAL_NAMESPACE', 'default'),
|
|
514
|
+
},
|
|
515
|
+
taskQueue: this.configService.get('TEMPORAL_TASK_QUEUE', 'default'),
|
|
516
|
+
worker: {
|
|
517
|
+
workflowsPath: require.resolve('../workflows'),
|
|
518
|
+
activityClasses: [], // Populated by module
|
|
519
|
+
maxConcurrentActivityExecutions: 100,
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
618
524
|
|
|
619
|
-
|
|
525
|
+
// app.module.ts
|
|
526
|
+
import { ConfigModule } from '@nestjs/config';
|
|
620
527
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
528
|
+
@Module({
|
|
529
|
+
imports: [
|
|
530
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
531
|
+
TemporalModule.registerAsync({
|
|
532
|
+
imports: [ConfigModule],
|
|
533
|
+
useClass: TemporalConfigService,
|
|
534
|
+
}),
|
|
535
|
+
],
|
|
536
|
+
})
|
|
537
|
+
export class AppModule {}
|
|
538
|
+
```
|
|
626
539
|
|
|
627
|
-
### Async
|
|
540
|
+
### Alternative Async Pattern (useFactory)
|
|
628
541
|
|
|
629
542
|
```typescript
|
|
630
543
|
TemporalModule.registerAsync({
|
|
631
544
|
imports: [ConfigModule],
|
|
632
|
-
useFactory: (
|
|
545
|
+
useFactory: (configService: ConfigService) => ({
|
|
633
546
|
connection: {
|
|
634
|
-
address:
|
|
635
|
-
namespace:
|
|
547
|
+
address: configService.get('TEMPORAL_ADDRESS', 'localhost:7233'),
|
|
548
|
+
namespace: configService.get('TEMPORAL_NAMESPACE', 'default'),
|
|
636
549
|
},
|
|
637
|
-
taskQueue:
|
|
638
|
-
// Dynamic logger configuration
|
|
639
|
-
enableLogger: config.get('TEMPORAL_LOGGING_ENABLED', 'true') === 'true',
|
|
640
|
-
logLevel: config.get('TEMPORAL_LOG_LEVEL', 'info'),
|
|
550
|
+
taskQueue: configService.get('TEMPORAL_TASK_QUEUE', 'default'),
|
|
641
551
|
worker: {
|
|
642
|
-
workflowsPath: './
|
|
643
|
-
|
|
552
|
+
workflowsPath: require.resolve('./workflows'),
|
|
553
|
+
activityClasses: [PaymentActivity, EmailActivity],
|
|
554
|
+
},
|
|
644
555
|
}),
|
|
645
|
-
inject: [ConfigService]
|
|
556
|
+
inject: [ConfigService],
|
|
646
557
|
})
|
|
647
558
|
```
|
|
648
559
|
|
|
649
|
-
###
|
|
560
|
+
### TLS Configuration (Temporal Cloud)
|
|
561
|
+
|
|
562
|
+
For secure connections to Temporal Cloud:
|
|
650
563
|
|
|
651
564
|
```typescript
|
|
652
|
-
|
|
653
|
-
{
|
|
654
|
-
enableLogger: false
|
|
655
|
-
}
|
|
565
|
+
import * as fs from 'fs';
|
|
656
566
|
|
|
657
|
-
|
|
658
|
-
{
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
567
|
+
TemporalModule.register({
|
|
568
|
+
connection: {
|
|
569
|
+
address: 'your-namespace.your-account.tmprl.cloud:7233',
|
|
570
|
+
namespace: 'your-namespace.your-account',
|
|
571
|
+
tls: {
|
|
572
|
+
clientCertPair: {
|
|
573
|
+
crt: fs.readFileSync('/path/to/client.crt'),
|
|
574
|
+
key: fs.readFileSync('/path/to/client.key'),
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
taskQueue: 'my-task-queue',
|
|
579
|
+
worker: {
|
|
580
|
+
workflowsPath: require.resolve('./workflows'),
|
|
581
|
+
activityClasses: [PaymentActivity],
|
|
582
|
+
},
|
|
583
|
+
})
|
|
584
|
+
```
|
|
662
585
|
|
|
663
|
-
|
|
664
|
-
{
|
|
665
|
-
enableLogger: true,
|
|
666
|
-
logLevel: 'debug'
|
|
667
|
-
}
|
|
586
|
+
### Configuration Options Reference
|
|
668
587
|
|
|
669
|
-
|
|
670
|
-
{
|
|
671
|
-
|
|
672
|
-
|
|
588
|
+
```typescript
|
|
589
|
+
interface TemporalOptions {
|
|
590
|
+
// Connection settings
|
|
591
|
+
connection: {
|
|
592
|
+
address: string; // Temporal server address (default: 'localhost:7233')
|
|
593
|
+
namespace?: string; // Temporal namespace (default: 'default')
|
|
594
|
+
tls?: TLSConfig; // TLS configuration for secure connections
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// Task queue name
|
|
598
|
+
taskQueue?: string; // Default task queue (default: 'default')
|
|
599
|
+
|
|
600
|
+
// Worker configuration
|
|
601
|
+
worker?: {
|
|
602
|
+
workflowsPath?: string; // Path to workflow definitions (use require.resolve)
|
|
603
|
+
activityClasses?: any[]; // Array of activity classes to register
|
|
604
|
+
autoStart?: boolean; // Auto-start worker on module init (default: true)
|
|
605
|
+
maxConcurrentActivityExecutions?: number; // Max concurrent activities (default: 100)
|
|
606
|
+
maxActivitiesPerSecond?: number; // Rate limit for activities
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// Logging
|
|
610
|
+
logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error'; // Log level (default: 'info')
|
|
611
|
+
enableLogger?: boolean; // Enable logging (default: true)
|
|
612
|
+
|
|
613
|
+
// Advanced
|
|
614
|
+
isGlobal?: boolean; // Make module global (default: false)
|
|
615
|
+
autoRestart?: boolean; // Auto-restart worker on failure (default: true)
|
|
673
616
|
}
|
|
674
617
|
```
|
|
675
618
|
|
|
676
|
-
##
|
|
619
|
+
## Core Concepts
|
|
677
620
|
|
|
678
|
-
|
|
621
|
+
### Activities
|
|
679
622
|
|
|
680
|
-
|
|
681
|
-
@Controller('health')
|
|
682
|
-
export class HealthController {
|
|
683
|
-
constructor(private readonly temporal: TemporalService) {}
|
|
623
|
+
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
624
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
625
|
+
**Key Points:**
|
|
626
|
+
- Activities are NestJS services (`@Injectable()`)
|
|
627
|
+
- Use `@Activity()` decorator at class level
|
|
628
|
+
- Use `@ActivityMethod()` decorator for methods to be registered
|
|
629
|
+
- Activities should be idempotent and handle retries gracefully
|
|
630
|
+
- Full access to NestJS DI (inject services, repositories, etc.)
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
@Injectable()
|
|
634
|
+
@Activity({ name: 'order-activities' })
|
|
635
|
+
export class OrderActivity {
|
|
636
|
+
constructor(
|
|
637
|
+
private readonly orderRepository: OrderRepository,
|
|
638
|
+
private readonly emailService: EmailService,
|
|
639
|
+
) {}
|
|
640
|
+
|
|
641
|
+
@ActivityMethod('createOrder')
|
|
642
|
+
async createOrder(orderData: CreateOrderData): Promise<Order> {
|
|
643
|
+
// Database operations with full DI support
|
|
644
|
+
const order = await this.orderRepository.create(orderData);
|
|
645
|
+
await this.emailService.sendConfirmation(order);
|
|
646
|
+
return order;
|
|
693
647
|
}
|
|
694
648
|
|
|
695
|
-
@
|
|
696
|
-
async
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
649
|
+
@ActivityMethod('validateInventory')
|
|
650
|
+
async validateInventory(items: OrderItem[]): Promise<boolean> {
|
|
651
|
+
// Business logic with injected services
|
|
652
|
+
return await this.orderRepository.checkInventory(items);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Workflows
|
|
658
|
+
|
|
659
|
+
Workflows are **pure Temporal functions** (NOT NestJS services) that orchestrate activities. They must be deterministic and use Temporal's workflow APIs.
|
|
660
|
+
|
|
661
|
+
**Important:** Workflows are NOT decorated with `@Injectable()` and should NOT use NestJS dependency injection.
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
// order.workflow.ts
|
|
665
|
+
import { proxyActivities, defineSignal, defineQuery, setHandler } from '@temporalio/workflow';
|
|
666
|
+
import type { OrderActivity } from './order.activity';
|
|
667
|
+
|
|
668
|
+
// Create activity proxies with proper typing
|
|
669
|
+
const { createOrder, validateInventory } = proxyActivities<typeof OrderActivity.prototype>({
|
|
670
|
+
startToCloseTimeout: '5m',
|
|
671
|
+
retry: {
|
|
672
|
+
maximumAttempts: 3,
|
|
673
|
+
initialInterval: '1s',
|
|
674
|
+
maximumInterval: '30s',
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Define signals and queries at module level
|
|
679
|
+
export const cancelOrderSignal = defineSignal<[string]>('cancelOrder');
|
|
680
|
+
export const getOrderStatusQuery = defineQuery<string>('getOrderStatus');
|
|
681
|
+
|
|
682
|
+
// Workflow function (exported, not a class)
|
|
683
|
+
export async function processOrderWorkflow(orderData: CreateOrderData): Promise<OrderResult> {
|
|
684
|
+
let status = 'pending';
|
|
685
|
+
|
|
686
|
+
// Set up signal handler
|
|
687
|
+
setHandler(cancelOrderSignal, (reason: string) => {
|
|
688
|
+
status = 'cancelled';
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Set up query handler
|
|
692
|
+
setHandler(getOrderStatusQuery, () => status);
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
// Validate inventory
|
|
696
|
+
const isValid = await validateInventory(orderData.items);
|
|
697
|
+
if (!isValid) {
|
|
698
|
+
throw new Error('Insufficient inventory');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Create order
|
|
702
|
+
status = 'processing';
|
|
703
|
+
const order = await createOrder(orderData);
|
|
704
|
+
status = 'completed';
|
|
705
|
+
|
|
700
706
|
return {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
schedules: this.temporal.getScheduleStats()
|
|
707
|
+
orderId: order.id,
|
|
708
|
+
status,
|
|
704
709
|
};
|
|
710
|
+
} catch (error) {
|
|
711
|
+
status = 'failed';
|
|
712
|
+
throw error;
|
|
705
713
|
}
|
|
706
714
|
}
|
|
707
715
|
```
|
|
708
716
|
|
|
709
|
-
|
|
717
|
+
### Signals and Queries
|
|
718
|
+
|
|
719
|
+
Signals allow external systems to send events to workflows, while queries provide read-only access to workflow state.
|
|
720
|
+
|
|
721
|
+
```typescript
|
|
722
|
+
import { defineSignal, defineQuery, setHandler, condition } from '@temporalio/workflow';
|
|
723
|
+
|
|
724
|
+
// Define at module level
|
|
725
|
+
export const updateStatusSignal = defineSignal<[string]>('updateStatus');
|
|
726
|
+
export const addItemSignal = defineSignal<[Item]>('addItem');
|
|
727
|
+
export const getItemsQuery = defineQuery<Item[]>('getItems');
|
|
728
|
+
export const getStatusQuery = defineQuery<string>('getStatus');
|
|
729
|
+
|
|
730
|
+
export async function myWorkflow(): Promise<void> {
|
|
731
|
+
let status = 'pending';
|
|
732
|
+
const items: Item[] = [];
|
|
733
|
+
|
|
734
|
+
// Set up handlers
|
|
735
|
+
setHandler(updateStatusSignal, (newStatus: string) => {
|
|
736
|
+
status = newStatus;
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
setHandler(addItemSignal, (item: Item) => {
|
|
740
|
+
items.push(item);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
setHandler(getItemsQuery, () => items);
|
|
744
|
+
setHandler(getStatusQuery, () => status);
|
|
745
|
+
|
|
746
|
+
// Wait for completion signal
|
|
747
|
+
await condition(() => status === 'completed');
|
|
748
|
+
}
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### Using Workflows in Services
|
|
710
752
|
|
|
711
|
-
|
|
753
|
+
Inject `TemporalService` in your NestJS services to interact with workflows:
|
|
712
754
|
|
|
713
755
|
```typescript
|
|
714
|
-
@Activity()
|
|
715
756
|
@Injectable()
|
|
716
|
-
export class
|
|
757
|
+
export class OrderService {
|
|
758
|
+
constructor(private readonly temporal: TemporalService) {}
|
|
759
|
+
|
|
760
|
+
async createOrder(orderData: CreateOrderData) {
|
|
761
|
+
// Start workflow - note the method signature
|
|
762
|
+
const result = await this.temporal.startWorkflow(
|
|
763
|
+
'processOrderWorkflow', // Workflow function name
|
|
764
|
+
[orderData], // Arguments array
|
|
765
|
+
{ // Options
|
|
766
|
+
workflowId: `order-${Date.now()}`,
|
|
767
|
+
taskQueue: 'order-queue',
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
workflowId: result.result.workflowId,
|
|
773
|
+
runId: result.result.runId,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async queryOrderStatus(workflowId: string) {
|
|
778
|
+
const result = await this.temporal.queryWorkflow(
|
|
779
|
+
workflowId,
|
|
780
|
+
'getOrderStatus'
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
return result.result;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async cancelOrder(workflowId: string, reason: string) {
|
|
787
|
+
await this.temporal.signalWorkflow(
|
|
788
|
+
workflowId,
|
|
789
|
+
'cancelOrder',
|
|
790
|
+
[reason]
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
## API Reference
|
|
797
|
+
|
|
798
|
+
### TemporalService
|
|
799
|
+
|
|
800
|
+
The main unified service providing access to all Temporal functionality:
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
class TemporalService {
|
|
804
|
+
/**
|
|
805
|
+
* Start a workflow execution
|
|
806
|
+
* @param workflowType - Name of the workflow function
|
|
807
|
+
* @param args - Array of arguments to pass to the workflow
|
|
808
|
+
* @param options - Workflow execution options
|
|
809
|
+
*/
|
|
810
|
+
async startWorkflow<T>(
|
|
811
|
+
workflowType: string,
|
|
812
|
+
args?: unknown[],
|
|
813
|
+
options?: WorkflowStartOptions
|
|
814
|
+
): Promise<WorkflowExecutionResult<T>>
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Send a signal to a running workflow
|
|
818
|
+
* @param workflowId - The workflow ID
|
|
819
|
+
* @param signalName - Name of the signal
|
|
820
|
+
* @param args - Arguments for the signal
|
|
821
|
+
*/
|
|
822
|
+
async signalWorkflow(
|
|
823
|
+
workflowId: string,
|
|
824
|
+
signalName: string,
|
|
825
|
+
args?: unknown[]
|
|
826
|
+
): Promise<WorkflowSignalResult>
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Query a running workflow
|
|
830
|
+
* @param workflowId - The workflow ID
|
|
831
|
+
* @param queryName - Name of the query
|
|
832
|
+
* @param args - Arguments for the query
|
|
833
|
+
*/
|
|
834
|
+
async queryWorkflow<T>(
|
|
835
|
+
workflowId: string,
|
|
836
|
+
queryName: string,
|
|
837
|
+
args?: unknown[]
|
|
838
|
+
): Promise<WorkflowQueryResult<T>>
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Get a workflow handle to interact with it
|
|
842
|
+
* @param workflowId - The workflow ID
|
|
843
|
+
* @param runId - Optional run ID for specific execution
|
|
844
|
+
*/
|
|
845
|
+
async getWorkflowHandle<T>(
|
|
846
|
+
workflowId: string,
|
|
847
|
+
runId?: string
|
|
848
|
+
): Promise<T>
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Terminate a workflow execution
|
|
852
|
+
* @param workflowId - The workflow ID
|
|
853
|
+
* @param reason - Termination reason
|
|
854
|
+
*/
|
|
855
|
+
async terminateWorkflow(
|
|
856
|
+
workflowId: string,
|
|
857
|
+
reason?: string
|
|
858
|
+
): Promise<WorkflowTerminationResult>
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Cancel a workflow execution
|
|
862
|
+
* @param workflowId - The workflow ID
|
|
863
|
+
*/
|
|
864
|
+
async cancelWorkflow(
|
|
865
|
+
workflowId: string
|
|
866
|
+
): Promise<WorkflowCancellationResult>
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Get service health status
|
|
870
|
+
*/
|
|
871
|
+
getHealth(): ServiceHealth
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Create a schedule
|
|
875
|
+
*/
|
|
876
|
+
async createSchedule(options: ScheduleCreateOptions): Promise<ScheduleHandle>
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* List all schedules
|
|
880
|
+
*/
|
|
881
|
+
async listSchedules(): Promise<ScheduleListDescription[]>
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Delete a schedule
|
|
885
|
+
*/
|
|
886
|
+
async deleteSchedule(scheduleId: string): Promise<void>
|
|
887
|
+
}
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
### WorkflowStartOptions
|
|
891
|
+
|
|
892
|
+
Options for starting workflows:
|
|
893
|
+
|
|
894
|
+
```typescript
|
|
895
|
+
interface WorkflowStartOptions {
|
|
896
|
+
workflowId?: string; // Unique workflow ID
|
|
897
|
+
taskQueue?: string; // Task queue name
|
|
898
|
+
workflowExecutionTimeout?: Duration; // Total workflow timeout
|
|
899
|
+
workflowRunTimeout?: Duration; // Single run timeout
|
|
900
|
+
workflowTaskTimeout?: Duration; // Decision task timeout
|
|
901
|
+
memo?: Record<string, unknown>; // Workflow memo
|
|
902
|
+
searchAttributes?: SearchAttributes; // Search attributes for filtering
|
|
903
|
+
}
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
### Result Types
|
|
907
|
+
|
|
908
|
+
```typescript
|
|
909
|
+
interface WorkflowExecutionResult<T> {
|
|
910
|
+
success: boolean;
|
|
911
|
+
result: T; // Contains workflowId, runId, etc.
|
|
912
|
+
executionTime: number;
|
|
913
|
+
error?: Error;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
interface WorkflowQueryResult<T> {
|
|
917
|
+
success: boolean;
|
|
918
|
+
result: T;
|
|
919
|
+
workflowId: string;
|
|
920
|
+
queryName: string;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
interface WorkflowSignalResult {
|
|
924
|
+
success: boolean;
|
|
925
|
+
workflowId: string;
|
|
926
|
+
signalName: string;
|
|
927
|
+
}
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
## Examples
|
|
931
|
+
|
|
932
|
+
## Examples
|
|
933
|
+
|
|
934
|
+
### Example 1: E-commerce Order Processing
|
|
935
|
+
|
|
936
|
+
Complete example with compensation logic:
|
|
937
|
+
|
|
938
|
+
```typescript
|
|
939
|
+
// order.activity.ts
|
|
940
|
+
@Injectable()
|
|
941
|
+
@Activity({ name: 'order-activities' })
|
|
942
|
+
export class OrderActivity {
|
|
943
|
+
constructor(
|
|
944
|
+
private readonly paymentService: PaymentService,
|
|
945
|
+
private readonly inventoryService: InventoryService,
|
|
946
|
+
private readonly emailService: EmailService,
|
|
947
|
+
) {}
|
|
717
948
|
|
|
718
|
-
@ActivityMethod(
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
949
|
+
@ActivityMethod('validatePayment')
|
|
950
|
+
async validatePayment(paymentData: PaymentData): Promise<PaymentResult> {
|
|
951
|
+
return await this.paymentService.validate(paymentData);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
@ActivityMethod('chargePayment')
|
|
955
|
+
async chargePayment(paymentData: PaymentData): Promise<{ transactionId: string }> {
|
|
956
|
+
return await this.paymentService.charge(paymentData);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
@ActivityMethod('refundPayment')
|
|
960
|
+
async refundPayment(transactionId: string): Promise<void> {
|
|
961
|
+
await this.paymentService.refund(transactionId);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
@ActivityMethod('reserveInventory')
|
|
965
|
+
async reserveInventory(items: OrderItem[]): Promise<{ reservationId: string }> {
|
|
966
|
+
return await this.inventoryService.reserve(items);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
@ActivityMethod('releaseInventory')
|
|
970
|
+
async releaseInventory(reservationId: string): Promise<void> {
|
|
971
|
+
await this.inventoryService.release(reservationId);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
@ActivityMethod('sendConfirmationEmail')
|
|
975
|
+
async sendConfirmationEmail(order: Order): Promise<void> {
|
|
976
|
+
await this.emailService.sendConfirmation(order);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// order.workflow.ts
|
|
981
|
+
import { proxyActivities, defineSignal, defineQuery, setHandler } from '@temporalio/workflow';
|
|
982
|
+
import type { OrderActivity } from './order.activity';
|
|
983
|
+
|
|
984
|
+
const {
|
|
985
|
+
validatePayment,
|
|
986
|
+
chargePayment,
|
|
987
|
+
refundPayment,
|
|
988
|
+
reserveInventory,
|
|
989
|
+
releaseInventory,
|
|
990
|
+
sendConfirmationEmail,
|
|
991
|
+
} = proxyActivities<typeof OrderActivity.prototype>({
|
|
992
|
+
startToCloseTimeout: '5m',
|
|
993
|
+
retry: { maximumAttempts: 3 },
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
export const cancelOrderSignal = defineSignal<[string]>('cancelOrder');
|
|
997
|
+
export const getOrderStatusQuery = defineQuery<OrderStatus>('getOrderStatus');
|
|
998
|
+
|
|
999
|
+
export async function processOrderWorkflow(orderData: OrderData): Promise<OrderResult> {
|
|
1000
|
+
let status: OrderStatus = 'pending';
|
|
1001
|
+
let transactionId: string | undefined;
|
|
1002
|
+
let reservationId: string | undefined;
|
|
1003
|
+
let cancelled = false;
|
|
1004
|
+
|
|
1005
|
+
setHandler(cancelOrderSignal, (reason: string) => {
|
|
1006
|
+
cancelled = true;
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
setHandler(getOrderStatusQuery, () => status);
|
|
1010
|
+
|
|
1011
|
+
try {
|
|
1012
|
+
// Step 1: Validate payment
|
|
1013
|
+
status = 'validating_payment';
|
|
1014
|
+
const paymentValid = await validatePayment(orderData.payment);
|
|
1015
|
+
if (!paymentValid.valid) {
|
|
1016
|
+
throw new Error('Invalid payment method');
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Check cancellation
|
|
1020
|
+
if (cancelled) {
|
|
1021
|
+
status = 'cancelled';
|
|
1022
|
+
return { orderId: orderData.orderId, status };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Step 2: Reserve inventory
|
|
1026
|
+
status = 'reserving_inventory';
|
|
1027
|
+
const reservation = await reserveInventory(orderData.items);
|
|
1028
|
+
reservationId = reservation.reservationId;
|
|
1029
|
+
|
|
1030
|
+
// Step 3: Charge payment
|
|
1031
|
+
status = 'charging_payment';
|
|
1032
|
+
const payment = await chargePayment(orderData.payment);
|
|
1033
|
+
transactionId = payment.transactionId;
|
|
1034
|
+
|
|
1035
|
+
// Step 4: Send confirmation
|
|
1036
|
+
status = 'sending_confirmation';
|
|
1037
|
+
await sendConfirmationEmail({
|
|
1038
|
+
orderId: orderData.orderId,
|
|
1039
|
+
items: orderData.items,
|
|
1040
|
+
total: orderData.totalAmount,
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
status = 'completed';
|
|
1044
|
+
return {
|
|
1045
|
+
orderId: orderData.orderId,
|
|
1046
|
+
status,
|
|
1047
|
+
transactionId,
|
|
1048
|
+
reservationId,
|
|
1049
|
+
};
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
// Compensation logic
|
|
1052
|
+
status = 'compensating';
|
|
1053
|
+
|
|
1054
|
+
if (reservationId) {
|
|
1055
|
+
await releaseInventory(reservationId);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (transactionId) {
|
|
1059
|
+
await refundPayment(transactionId);
|
|
727
1060
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1061
|
+
|
|
1062
|
+
status = 'failed';
|
|
1063
|
+
throw error;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// order.service.ts
|
|
1068
|
+
@Injectable()
|
|
1069
|
+
export class OrderService {
|
|
1070
|
+
constructor(private readonly temporal: TemporalService) {}
|
|
1071
|
+
|
|
1072
|
+
async createOrder(orderData: OrderData) {
|
|
1073
|
+
const result = await this.temporal.startWorkflow(
|
|
1074
|
+
'processOrderWorkflow',
|
|
1075
|
+
[orderData],
|
|
1076
|
+
{
|
|
1077
|
+
workflowId: `order-${orderData.orderId}`,
|
|
1078
|
+
taskQueue: 'order-queue',
|
|
1079
|
+
}
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
return result.result;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
async getOrderStatus(orderId: string) {
|
|
1086
|
+
const result = await this.temporal.queryWorkflow(
|
|
1087
|
+
`order-${orderId}`,
|
|
1088
|
+
'getOrderStatus'
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
return result.result;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
async cancelOrder(orderId: string, reason: string) {
|
|
1095
|
+
await this.temporal.signalWorkflow(
|
|
1096
|
+
`order-${orderId}`,
|
|
1097
|
+
'cancelOrder',
|
|
1098
|
+
[reason]
|
|
1099
|
+
);
|
|
731
1100
|
}
|
|
732
1101
|
}
|
|
733
1102
|
```
|
|
734
1103
|
|
|
735
|
-
###
|
|
1104
|
+
### Example 2: Scheduled Reports
|
|
1105
|
+
|
|
1106
|
+
Creating and managing scheduled workflows:
|
|
736
1107
|
|
|
737
1108
|
```typescript
|
|
1109
|
+
// report.activity.ts
|
|
738
1110
|
@Injectable()
|
|
739
|
-
|
|
740
|
-
|
|
1111
|
+
@Activity({ name: 'report-activities' })
|
|
1112
|
+
export class ReportActivity {
|
|
1113
|
+
constructor(
|
|
1114
|
+
private readonly reportService: ReportService,
|
|
1115
|
+
private readonly storageService: StorageService,
|
|
1116
|
+
private readonly notificationService: NotificationService,
|
|
1117
|
+
) {}
|
|
1118
|
+
|
|
1119
|
+
@ActivityMethod('generateSalesReport')
|
|
1120
|
+
async generateSalesReport(period: ReportPeriod): Promise<ReportData> {
|
|
1121
|
+
return await this.reportService.generateSales(period);
|
|
1122
|
+
}
|
|
741
1123
|
|
|
742
|
-
|
|
743
|
-
|
|
1124
|
+
@ActivityMethod('uploadReport')
|
|
1125
|
+
async uploadReport(reportData: ReportData): Promise<string> {
|
|
1126
|
+
return await this.storageService.upload(reportData);
|
|
744
1127
|
}
|
|
745
1128
|
|
|
746
|
-
|
|
747
|
-
|
|
1129
|
+
@ActivityMethod('notifyStakeholders')
|
|
1130
|
+
async notifyStakeholders(reportUrl: string, recipients: string[]): Promise<void> {
|
|
1131
|
+
await this.notificationService.send(recipients, reportUrl);
|
|
748
1132
|
}
|
|
1133
|
+
}
|
|
749
1134
|
|
|
750
|
-
|
|
751
|
-
|
|
1135
|
+
// report.workflow.ts
|
|
1136
|
+
import { proxyActivities } from '@temporalio/workflow';
|
|
1137
|
+
import type { ReportActivity } from './report.activity';
|
|
1138
|
+
|
|
1139
|
+
const { generateSalesReport, uploadReport, notifyStakeholders } =
|
|
1140
|
+
proxyActivities<typeof ReportActivity.prototype>({
|
|
1141
|
+
startToCloseTimeout: '10m',
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
export async function weeklyReportWorkflow(): Promise<ReportResult> {
|
|
1145
|
+
const endDate = new Date();
|
|
1146
|
+
const startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
1147
|
+
|
|
1148
|
+
// Generate report
|
|
1149
|
+
const reportData = await generateSalesReport({
|
|
1150
|
+
startDate,
|
|
1151
|
+
endDate,
|
|
1152
|
+
type: 'weekly',
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
// Upload to storage
|
|
1156
|
+
const reportUrl = await uploadReport(reportData);
|
|
1157
|
+
|
|
1158
|
+
// Notify stakeholders
|
|
1159
|
+
await notifyStakeholders(reportUrl, ['management@company.com']);
|
|
1160
|
+
|
|
1161
|
+
return {
|
|
1162
|
+
reportUrl,
|
|
1163
|
+
generatedAt: new Date(),
|
|
1164
|
+
period: { startDate, endDate },
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// schedule.service.ts
|
|
1169
|
+
@Injectable()
|
|
1170
|
+
export class ReportScheduleService {
|
|
1171
|
+
constructor(private readonly temporal: TemporalService) {}
|
|
1172
|
+
|
|
1173
|
+
async setupWeeklyReports() {
|
|
1174
|
+
await this.temporal.createSchedule({
|
|
1175
|
+
scheduleId: 'weekly-sales-report',
|
|
1176
|
+
spec: {
|
|
1177
|
+
cronExpressions: ['0 9 * * MON'], // Every Monday at 9 AM
|
|
1178
|
+
},
|
|
1179
|
+
action: {
|
|
1180
|
+
type: 'startWorkflow',
|
|
1181
|
+
workflowType: 'weeklyReportWorkflow',
|
|
1182
|
+
taskQueue: 'reports-queue',
|
|
1183
|
+
},
|
|
1184
|
+
policies: {
|
|
1185
|
+
overlap: 'SKIP',
|
|
1186
|
+
catchupWindow: '1 hour',
|
|
1187
|
+
},
|
|
1188
|
+
});
|
|
752
1189
|
}
|
|
753
1190
|
|
|
754
|
-
async
|
|
755
|
-
|
|
1191
|
+
async deleteSchedule(scheduleId: string) {
|
|
1192
|
+
await this.temporal.deleteSchedule(scheduleId);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
async listAllSchedules() {
|
|
1196
|
+
return await this.temporal.listSchedules();
|
|
756
1197
|
}
|
|
757
1198
|
}
|
|
758
1199
|
```
|
|
759
1200
|
|
|
760
|
-
|
|
1201
|
+
## Advanced Usage
|
|
1202
|
+
|
|
1203
|
+
### Activity Retry Configuration
|
|
1204
|
+
|
|
1205
|
+
Configure custom retry policies for different activity types:
|
|
761
1206
|
|
|
762
1207
|
```typescript
|
|
763
|
-
//
|
|
764
|
-
|
|
1208
|
+
// workflow.ts
|
|
1209
|
+
const paymentActivities = proxyActivities<typeof PaymentActivity.prototype>({
|
|
1210
|
+
startToCloseTimeout: '5m',
|
|
1211
|
+
retry: {
|
|
1212
|
+
maximumAttempts: 5,
|
|
1213
|
+
initialInterval: '1s',
|
|
1214
|
+
maximumInterval: '1m',
|
|
1215
|
+
backoffCoefficient: 2,
|
|
1216
|
+
nonRetryableErrorTypes: ['InvalidPaymentMethod', 'InsufficientFunds'],
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
765
1219
|
|
|
766
|
-
|
|
767
|
-
|
|
1220
|
+
const emailActivities = proxyActivities<typeof EmailActivity.prototype>({
|
|
1221
|
+
startToCloseTimeout: '2m',
|
|
1222
|
+
retry: {
|
|
1223
|
+
maximumAttempts: 3,
|
|
1224
|
+
initialInterval: '500ms',
|
|
1225
|
+
},
|
|
1226
|
+
});
|
|
1227
|
+
```
|
|
768
1228
|
|
|
769
|
-
|
|
770
|
-
let status = 'processing';
|
|
771
|
-
let cancelled = false;
|
|
1229
|
+
### Workflow Testing
|
|
772
1230
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1231
|
+
Test workflows using Temporal's testing framework:
|
|
1232
|
+
|
|
1233
|
+
```typescript
|
|
1234
|
+
import { TestWorkflowEnvironment } from '@temporalio/testing';
|
|
1235
|
+
import { Worker } from '@temporalio/worker';
|
|
1236
|
+
import { processOrderWorkflow } from './order.workflow';
|
|
1237
|
+
import { OrderActivity } from './order.activity';
|
|
1238
|
+
|
|
1239
|
+
describe('Order Workflow', () => {
|
|
1240
|
+
let testEnv: TestWorkflowEnvironment;
|
|
1241
|
+
|
|
1242
|
+
beforeAll(async () => {
|
|
1243
|
+
testEnv = await TestWorkflowEnvironment.createTimeSkipping();
|
|
777
1244
|
});
|
|
778
1245
|
|
|
779
|
-
|
|
780
|
-
|
|
1246
|
+
afterAll(async () => {
|
|
1247
|
+
await testEnv?.teardown();
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
it('should process order successfully', async () => {
|
|
1251
|
+
const { client, nativeConnection } = testEnv;
|
|
1252
|
+
|
|
1253
|
+
// Mock activities
|
|
1254
|
+
const mockOrderActivity = {
|
|
1255
|
+
validatePayment: async () => ({ valid: true }),
|
|
1256
|
+
reserveInventory: async () => ({ reservationId: 'res-123' }),
|
|
1257
|
+
chargePayment: async () => ({ transactionId: 'txn-123' }),
|
|
1258
|
+
sendConfirmationEmail: async () => {},
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
const worker = await Worker.create({
|
|
1262
|
+
connection: nativeConnection,
|
|
1263
|
+
taskQueue: 'test',
|
|
1264
|
+
workflowsPath: require.resolve('./order.workflow'),
|
|
1265
|
+
activities: mockOrderActivity,
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
await worker.runUntil(async () => {
|
|
1269
|
+
const result = await client.workflow.execute(processOrderWorkflow, {
|
|
1270
|
+
workflowId: 'test-order-1',
|
|
1271
|
+
taskQueue: 'test',
|
|
1272
|
+
args: [{
|
|
1273
|
+
orderId: 'order-123',
|
|
1274
|
+
payment: { amount: 100, currency: 'USD' },
|
|
1275
|
+
items: [{ id: '1', quantity: 1 }],
|
|
1276
|
+
}],
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
expect(result.status).toBe('completed');
|
|
1280
|
+
expect(result.transactionId).toBe('txn-123');
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
### Child Workflows
|
|
1287
|
+
|
|
1288
|
+
Organize complex workflows using child workflows:
|
|
781
1289
|
|
|
782
|
-
|
|
783
|
-
|
|
1290
|
+
```typescript
|
|
1291
|
+
// parent.workflow.ts
|
|
1292
|
+
import { startChild } from '@temporalio/workflow';
|
|
1293
|
+
|
|
1294
|
+
export async function parentWorkflow(orderId: string) {
|
|
1295
|
+
// Start child workflows
|
|
1296
|
+
const paymentHandle = await startChild(processPaymentWorkflow, {
|
|
1297
|
+
workflowId: `payment-${orderId}`,
|
|
1298
|
+
args: [paymentData],
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
const shippingHandle = await startChild(processShippingWorkflow, {
|
|
1302
|
+
workflowId: `shipping-${orderId}`,
|
|
1303
|
+
args: [shippingData],
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
// Wait for both to complete
|
|
1307
|
+
const [paymentResult, shippingResult] = await Promise.all([
|
|
1308
|
+
paymentHandle.result(),
|
|
1309
|
+
shippingHandle.result(),
|
|
1310
|
+
]);
|
|
1311
|
+
|
|
1312
|
+
return {
|
|
1313
|
+
payment: paymentResult,
|
|
1314
|
+
shipping: shippingResult,
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1319
|
+
### Continue-As-New for Long-Running Workflows
|
|
1320
|
+
|
|
1321
|
+
Use continue-as-new to prevent event history from growing too large:
|
|
1322
|
+
|
|
1323
|
+
```typescript
|
|
1324
|
+
import { continueAsNew } from '@temporalio/workflow';
|
|
1325
|
+
|
|
1326
|
+
export async function processEventStreamWorkflow(cursor: number): Promise<void> {
|
|
1327
|
+
const events = await fetchEvents(cursor);
|
|
784
1328
|
|
|
785
|
-
|
|
786
|
-
|
|
1329
|
+
for (const event of events) {
|
|
1330
|
+
await processEvent(event);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Continue as new after processing 1000 events
|
|
1334
|
+
if (events.length >= 1000) {
|
|
1335
|
+
await continueAsNew<typeof processEventStreamWorkflow>(cursor + events.length);
|
|
1336
|
+
}
|
|
787
1337
|
}
|
|
788
1338
|
```
|
|
789
1339
|
|
|
790
|
-
|
|
1340
|
+
### Custom Error Handling
|
|
791
1341
|
|
|
792
|
-
|
|
1342
|
+
Implement custom error types and handling:
|
|
793
1343
|
|
|
794
1344
|
```typescript
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1345
|
+
// activities
|
|
1346
|
+
export class RetryableError extends Error {
|
|
1347
|
+
constructor(message: string) {
|
|
1348
|
+
super(message);
|
|
1349
|
+
this.name = 'RetryableError';
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
export class NonRetryableError extends Error {
|
|
1354
|
+
constructor(message: string) {
|
|
1355
|
+
super(message);
|
|
1356
|
+
this.name = 'NonRetryableError';
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
@ActivityMethod('processData')
|
|
1361
|
+
async processData(data: any): Promise<any> {
|
|
1362
|
+
try {
|
|
1363
|
+
return await this.externalApi.process(data);
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
if (error.code === 'RATE_LIMIT') {
|
|
1366
|
+
throw new RetryableError('Rate limit exceeded, will retry');
|
|
1367
|
+
} else if (error.code === 'INVALID_DATA') {
|
|
1368
|
+
throw new NonRetryableError('Invalid data format');
|
|
803
1369
|
}
|
|
804
|
-
|
|
805
|
-
taskQueue: 'production-queue',
|
|
806
|
-
worker: {
|
|
807
|
-
workflowBundle: require('../workflows/bundle'),
|
|
808
|
-
workerOptions: WORKER_PRESETS.PRODUCTION_BALANCED
|
|
1370
|
+
throw error;
|
|
809
1371
|
}
|
|
810
|
-
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// workflow configuration
|
|
1375
|
+
const activities = proxyActivities<typeof DataActivity.prototype>({
|
|
1376
|
+
startToCloseTimeout: '5m',
|
|
1377
|
+
retry: {
|
|
1378
|
+
nonRetryableErrorTypes: ['NonRetryableError'],
|
|
1379
|
+
},
|
|
1380
|
+
});
|
|
811
1381
|
```
|
|
812
1382
|
|
|
813
|
-
##
|
|
1383
|
+
## Best Practices
|
|
814
1384
|
|
|
815
|
-
### 1.
|
|
816
|
-
- Keep activities idempotent
|
|
817
|
-
- Use proper timeouts and retry policies
|
|
818
|
-
- Handle errors gracefully
|
|
819
|
-
- Use dependency injection for testability
|
|
1385
|
+
### 1. Workflow Design
|
|
820
1386
|
|
|
821
|
-
|
|
822
|
-
-
|
|
823
|
-
- Use
|
|
824
|
-
- Keep workflows
|
|
825
|
-
-
|
|
1387
|
+
**✅ DO:**
|
|
1388
|
+
- Keep workflows deterministic (no random numbers, current time, network calls)
|
|
1389
|
+
- Use activities for any non-deterministic operations
|
|
1390
|
+
- Keep workflow history size manageable (use continue-as-new for long-running workflows)
|
|
1391
|
+
- Export workflow functions (not classes)
|
|
1392
|
+
- Use `defineSignal` and `defineQuery` at module level
|
|
826
1393
|
|
|
827
|
-
|
|
828
|
-
-
|
|
829
|
-
-
|
|
830
|
-
-
|
|
831
|
-
-
|
|
1394
|
+
**❌ DON'T:**
|
|
1395
|
+
- Don't use `@Injectable()` on workflow functions
|
|
1396
|
+
- Don't inject NestJS services in workflows
|
|
1397
|
+
- Don't use `Math.random()` or `Date.now()` directly in workflows
|
|
1398
|
+
- Don't make HTTP calls or database queries directly in workflows
|
|
832
1399
|
|
|
833
|
-
###
|
|
834
|
-
- Implement health checks
|
|
835
|
-
- Monitor worker status
|
|
836
|
-
- Track schedule execution
|
|
837
|
-
- Use structured logging
|
|
1400
|
+
### 2. Activity Design
|
|
838
1401
|
|
|
839
|
-
|
|
840
|
-
-
|
|
841
|
-
-
|
|
842
|
-
-
|
|
843
|
-
-
|
|
1402
|
+
**✅ DO:**
|
|
1403
|
+
- Make activities idempotent (safe to retry)
|
|
1404
|
+
- Use `@Injectable()` and leverage NestJS DI
|
|
1405
|
+
- Use `@Activity()` and `@ActivityMethod()` decorators
|
|
1406
|
+
- Handle errors appropriately
|
|
1407
|
+
- Log activity execution for debugging
|
|
844
1408
|
|
|
845
|
-
|
|
1409
|
+
**❌ DON'T:**
|
|
1410
|
+
- Don't make activities too granular (network overhead)
|
|
1411
|
+
- Don't rely on activity execution order guarantees
|
|
1412
|
+
- Don't share mutable state between activity invocations
|
|
846
1413
|
|
|
847
|
-
###
|
|
1414
|
+
### 3. Configuration
|
|
848
1415
|
|
|
849
|
-
|
|
850
|
-
-
|
|
851
|
-
-
|
|
1416
|
+
**✅ DO:**
|
|
1417
|
+
- Use async configuration for environment-based setup
|
|
1418
|
+
- Configure appropriate timeouts for your use case
|
|
1419
|
+
- Set up proper retry policies
|
|
1420
|
+
- Enable graceful shutdown hooks
|
|
1421
|
+
- Use task queues to organize work
|
|
852
1422
|
|
|
853
|
-
|
|
854
|
-
-
|
|
855
|
-
-
|
|
856
|
-
-
|
|
1423
|
+
**❌ DON'T:**
|
|
1424
|
+
- Don't hardcode connection strings
|
|
1425
|
+
- Don't use the same task queue for all workflows
|
|
1426
|
+
- Don't ignore timeout configurations
|
|
857
1427
|
|
|
858
|
-
|
|
859
|
-
- `@Signal(nameOrOptions?)` - Mark a method as a signal handler
|
|
860
|
-
- `@Query(nameOrOptions?)` - Mark a method as a query handler
|
|
1428
|
+
### 4. Error Handling
|
|
861
1429
|
|
|
862
|
-
|
|
863
|
-
-
|
|
864
|
-
-
|
|
865
|
-
-
|
|
866
|
-
-
|
|
867
|
-
-
|
|
1430
|
+
**✅ DO:**
|
|
1431
|
+
- Implement compensation logic in workflows
|
|
1432
|
+
- Use appropriate retry policies
|
|
1433
|
+
- Log errors with context
|
|
1434
|
+
- Define non-retryable error types
|
|
1435
|
+
- Handle activity failures gracefully
|
|
868
1436
|
|
|
869
|
-
|
|
1437
|
+
**❌ DON'T:**
|
|
1438
|
+
- Don't swallow errors silently
|
|
1439
|
+
- Don't retry indefinitely
|
|
1440
|
+
- Don't ignore business-level failures
|
|
870
1441
|
|
|
871
|
-
|
|
872
|
-
- `TemporalClientService` - Client-only operations (starting workflows, signals, queries)
|
|
873
|
-
- `TemporalActivityService` - Activity discovery and management
|
|
874
|
-
- `TemporalSchedulesService` - Schedule creation and management
|
|
875
|
-
- `TemporalWorkerManagerService` - Worker lifecycle and health monitoring
|
|
1442
|
+
### 5. Testing
|
|
876
1443
|
|
|
877
|
-
|
|
1444
|
+
**✅ DO:**
|
|
1445
|
+
- Write unit tests for activities
|
|
1446
|
+
- Use TestWorkflowEnvironment for integration tests
|
|
1447
|
+
- Mock external dependencies
|
|
1448
|
+
- Test failure scenarios
|
|
1449
|
+
- Test signal and query handlers
|
|
878
1450
|
|
|
879
|
-
|
|
880
|
-
-
|
|
881
|
-
-
|
|
1451
|
+
**❌ DON'T:**
|
|
1452
|
+
- Don't skip workflow testing
|
|
1453
|
+
- Don't test against production Temporal server
|
|
1454
|
+
- Don't assume workflows are correct without testing
|
|
882
1455
|
|
|
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
|
|
1456
|
+
## Health Monitoring
|
|
889
1457
|
|
|
890
|
-
|
|
891
|
-
- `TemporalLogger` - Enhanced logger with context support
|
|
892
|
-
- `TemporalLoggerManager` - Global logger configuration
|
|
1458
|
+
The package includes comprehensive health monitoring capabilities for production deployments.
|
|
893
1459
|
|
|
894
|
-
###
|
|
1460
|
+
### Using Built-in Health Module
|
|
895
1461
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1462
|
+
```typescript
|
|
1463
|
+
// app.module.ts
|
|
1464
|
+
import { Module } from '@nestjs/common';
|
|
1465
|
+
import { TemporalModule } from 'nestjs-temporal-core';
|
|
1466
|
+
import { TemporalHealthModule } from 'nestjs-temporal-core/health';
|
|
899
1467
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1468
|
+
@Module({
|
|
1469
|
+
imports: [
|
|
1470
|
+
TemporalModule.register({
|
|
1471
|
+
connection: { address: 'localhost:7233' },
|
|
1472
|
+
taskQueue: 'my-queue',
|
|
1473
|
+
worker: {
|
|
1474
|
+
workflowsPath: require.resolve('./workflows'),
|
|
1475
|
+
activityClasses: [MyActivity],
|
|
1476
|
+
},
|
|
1477
|
+
}),
|
|
1478
|
+
TemporalHealthModule, // Adds /health/temporal endpoint
|
|
1479
|
+
],
|
|
1480
|
+
})
|
|
1481
|
+
export class AppModule {}
|
|
1482
|
+
```
|
|
903
1483
|
|
|
904
|
-
|
|
905
|
-
- `TEMPORAL_MODULE_OPTIONS` - Main module configuration token
|
|
906
|
-
- `TEMPORAL_CLIENT` - Client instance injection token
|
|
907
|
-
- `TEMPORAL_CONNECTION` - Connection instance injection token
|
|
1484
|
+
### Custom Health Checks
|
|
908
1485
|
|
|
909
|
-
|
|
1486
|
+
```typescript
|
|
1487
|
+
@Controller('health')
|
|
1488
|
+
export class HealthController {
|
|
1489
|
+
constructor(private readonly temporal: TemporalService) {}
|
|
910
1490
|
|
|
911
|
-
|
|
1491
|
+
@Get('/status')
|
|
1492
|
+
async getHealthStatus() {
|
|
1493
|
+
const health = this.temporal.getHealth();
|
|
1494
|
+
|
|
1495
|
+
return {
|
|
1496
|
+
status: health.overallHealth,
|
|
1497
|
+
timestamp: new Date(),
|
|
1498
|
+
services: {
|
|
1499
|
+
client: {
|
|
1500
|
+
healthy: health.client.status === 'healthy',
|
|
1501
|
+
connection: health.client.connectionStatus,
|
|
1502
|
+
},
|
|
1503
|
+
worker: {
|
|
1504
|
+
healthy: health.worker.status === 'healthy',
|
|
1505
|
+
state: health.worker.state,
|
|
1506
|
+
activitiesRegistered: health.worker.activitiesCount,
|
|
1507
|
+
},
|
|
1508
|
+
discovery: {
|
|
1509
|
+
healthy: health.discovery.status === 'healthy',
|
|
1510
|
+
activitiesDiscovered: health.discovery.activitiesDiscovered,
|
|
1511
|
+
},
|
|
1512
|
+
},
|
|
1513
|
+
uptime: health.uptime,
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
912
1516
|
|
|
913
|
-
|
|
1517
|
+
@Get('/detailed')
|
|
1518
|
+
async getDetailedHealth() {
|
|
1519
|
+
const health = this.temporal.getHealth();
|
|
1520
|
+
const stats = this.temporal.getStatistics();
|
|
1521
|
+
|
|
1522
|
+
return {
|
|
1523
|
+
health,
|
|
1524
|
+
statistics: stats,
|
|
1525
|
+
performance: {
|
|
1526
|
+
workflowStartLatency: stats.averageWorkflowStartTime,
|
|
1527
|
+
activityExecutionCount: stats.totalActivitiesExecuted,
|
|
1528
|
+
},
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
```
|
|
914
1533
|
|
|
915
|
-
|
|
1534
|
+
### Health Check Response
|
|
916
1535
|
|
|
917
|
-
|
|
1536
|
+
```typescript
|
|
1537
|
+
interface ServiceHealth {
|
|
1538
|
+
overallHealth: 'healthy' | 'degraded' | 'unhealthy';
|
|
1539
|
+
client: {
|
|
1540
|
+
status: 'healthy' | 'unhealthy';
|
|
1541
|
+
connectionStatus: 'connected' | 'disconnected';
|
|
1542
|
+
};
|
|
1543
|
+
worker: {
|
|
1544
|
+
status: 'healthy' | 'unhealthy';
|
|
1545
|
+
state: 'RUNNING' | 'STOPPED' | 'FAILED';
|
|
1546
|
+
activitiesCount: number;
|
|
1547
|
+
};
|
|
1548
|
+
discovery: {
|
|
1549
|
+
status: 'healthy' | 'unhealthy';
|
|
1550
|
+
activitiesDiscovered: number;
|
|
1551
|
+
};
|
|
1552
|
+
uptime: number;
|
|
1553
|
+
lastChecked: Date;
|
|
1554
|
+
}
|
|
1555
|
+
```
|
|
1556
|
+
```
|
|
918
1557
|
|
|
919
|
-
|
|
920
|
-
- [NestJS](https://nestjs.com/) for the fantastic framework
|
|
921
|
-
- The TypeScript community for excellent tooling
|
|
1558
|
+
## Migration Guide
|
|
922
1559
|
|
|
923
|
-
|
|
1560
|
+
### Migrating to v3.0.12+ (Multiple Workers Support)
|
|
1561
|
+
|
|
1562
|
+
Version 3.0.12 introduces support for multiple workers without breaking existing single-worker configurations.
|
|
1563
|
+
|
|
1564
|
+
#### No Changes Required for Single Worker
|
|
1565
|
+
|
|
1566
|
+
If you're using a single worker, your existing configuration continues to work without any changes:
|
|
1567
|
+
|
|
1568
|
+
```typescript
|
|
1569
|
+
// ✅ This still works exactly as before
|
|
1570
|
+
TemporalModule.register({
|
|
1571
|
+
connection: { address: 'localhost:7233' },
|
|
1572
|
+
taskQueue: 'my-queue',
|
|
1573
|
+
worker: {
|
|
1574
|
+
workflowsPath: require.resolve('./workflows'),
|
|
1575
|
+
activityClasses: [MyActivity],
|
|
1576
|
+
},
|
|
1577
|
+
})
|
|
1578
|
+
```
|
|
1579
|
+
|
|
1580
|
+
#### Migrating to Multiple Workers
|
|
924
1581
|
|
|
925
|
-
|
|
1582
|
+
**Before (v3.0.10):**
|
|
1583
|
+
```typescript
|
|
1584
|
+
// You had to create custom workers manually
|
|
1585
|
+
@Injectable()
|
|
1586
|
+
export class ScheduleService implements OnModuleInit {
|
|
1587
|
+
private customWorker: Worker;
|
|
926
1588
|
|
|
927
|
-
|
|
1589
|
+
constructor(private temporal: TemporalService) {}
|
|
928
1590
|
|
|
929
|
-
|
|
1591
|
+
async onModuleInit() {
|
|
1592
|
+
// This pattern required accessing internal APIs
|
|
1593
|
+
const workerManager = this.temporal.getWorkerManager();
|
|
1594
|
+
const connection = workerManager?.getConnection(); // This wasn't available!
|
|
930
1595
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
- Fully compatible with Temporal's TypeScript SDK
|
|
936
|
-
- Easier to test and bundle
|
|
937
|
-
- **When to Use:**
|
|
938
|
-
- Most workflows, especially if you don't need dependency injection or advanced metadata
|
|
1596
|
+
// Manual worker creation...
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
```
|
|
939
1600
|
|
|
1601
|
+
**After (v3.0.12):**
|
|
940
1602
|
```typescript
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
|
|
1603
|
+
// Option 1: Configure multiple workers in module
|
|
1604
|
+
TemporalModule.register({
|
|
1605
|
+
connection: { address: 'localhost:7233' },
|
|
1606
|
+
workers: [
|
|
1607
|
+
{
|
|
1608
|
+
taskQueue: 'main-queue',
|
|
1609
|
+
workflowsPath: require.resolve('./workflows/main'),
|
|
1610
|
+
activityClasses: [MainActivity],
|
|
1611
|
+
},
|
|
1612
|
+
{
|
|
1613
|
+
taskQueue: 'schedule-queue',
|
|
1614
|
+
workflowsPath: require.resolve('./workflows/schedule'),
|
|
1615
|
+
activityClasses: [ScheduleActivity],
|
|
1616
|
+
},
|
|
1617
|
+
],
|
|
1618
|
+
})
|
|
1619
|
+
|
|
1620
|
+
// Option 2: Get native connection for manual worker creation
|
|
1621
|
+
@Injectable()
|
|
1622
|
+
export class CustomWorkerService implements OnModuleInit {
|
|
1623
|
+
constructor(private temporal: TemporalService) {}
|
|
1624
|
+
|
|
1625
|
+
async onModuleInit() {
|
|
1626
|
+
const workerManager = this.temporal.getWorkerManager();
|
|
1627
|
+
const connection = workerManager.getConnection(); // Now available!
|
|
1628
|
+
|
|
1629
|
+
if (!connection) return;
|
|
1630
|
+
|
|
1631
|
+
const customWorker = await Worker.create({
|
|
1632
|
+
connection, // Native NativeConnection object
|
|
1633
|
+
taskQueue: 'custom-queue',
|
|
1634
|
+
workflowsPath: require.resolve('./workflows/custom'),
|
|
1635
|
+
activities: {
|
|
1636
|
+
myActivity: async (data: string) => data,
|
|
1637
|
+
},
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
await customWorker.run();
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Option 3: Register workers dynamically at runtime
|
|
1645
|
+
@Injectable()
|
|
1646
|
+
export class DynamicWorkerService {
|
|
1647
|
+
constructor(private temporal: TemporalService) {}
|
|
1648
|
+
|
|
1649
|
+
async registerNewQueue(taskQueue: string) {
|
|
1650
|
+
const result = await this.temporal.registerWorker({
|
|
1651
|
+
taskQueue,
|
|
1652
|
+
workflowsPath: require.resolve('./workflows/dynamic'),
|
|
1653
|
+
activityClasses: [DynamicActivity],
|
|
1654
|
+
autoStart: true,
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
if (result.success) {
|
|
1658
|
+
console.log(`Worker registered for ${taskQueue}`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
944
1661
|
}
|
|
945
1662
|
```
|
|
946
1663
|
|
|
947
|
-
|
|
948
|
-
- **How:** Use an injectable class and parameter decorators like `@WorkflowParam`, `@WorkflowId`, etc.
|
|
949
|
-
- **Benefits:**
|
|
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
|
|
1664
|
+
#### New APIs in v3.0.12
|
|
956
1665
|
|
|
957
1666
|
```typescript
|
|
1667
|
+
// Get native connection for custom worker creation
|
|
1668
|
+
const workerManager = temporal.getWorkerManager();
|
|
1669
|
+
const connection: NativeConnection | null = workerManager.getConnection();
|
|
1670
|
+
|
|
1671
|
+
// Get specific worker by task queue
|
|
1672
|
+
const worker: Worker | null = temporal.getWorker('payments-queue');
|
|
1673
|
+
|
|
1674
|
+
// Get all workers information
|
|
1675
|
+
const workersInfo: MultipleWorkersInfo = temporal.getAllWorkers();
|
|
1676
|
+
console.log(`${workersInfo.runningWorkers}/${workersInfo.totalWorkers} workers running`);
|
|
1677
|
+
|
|
1678
|
+
// Get specific worker status
|
|
1679
|
+
const status: WorkerStatus | null = temporal.getWorkerStatusByTaskQueue('payments-queue');
|
|
1680
|
+
|
|
1681
|
+
// Control specific workers
|
|
1682
|
+
await temporal.startWorkerByTaskQueue('payments-queue');
|
|
1683
|
+
await temporal.stopWorkerByTaskQueue('notifications-queue');
|
|
1684
|
+
|
|
1685
|
+
// Register new worker dynamically
|
|
1686
|
+
const result = await temporal.registerWorker({
|
|
1687
|
+
taskQueue: 'new-queue',
|
|
1688
|
+
workflowsPath: require.resolve('./workflows/new'),
|
|
1689
|
+
activityClasses: [NewActivity],
|
|
1690
|
+
autoStart: true,
|
|
1691
|
+
});
|
|
1692
|
+
```
|
|
1693
|
+
|
|
1694
|
+
#### Breaking Changes from v3.0.10 to v3.0.11
|
|
1695
|
+
|
|
1696
|
+
If you're upgrading from v3.0.10, note these changes:
|
|
1697
|
+
|
|
1698
|
+
1. **Internal Architecture**: The internal connection management was refactored. If you were accessing private/internal APIs, those may have changed.
|
|
1699
|
+
|
|
1700
|
+
2. **getConnection() Now Available**: In v3.0.10, accessing the native connection wasn't possible. This is now officially supported via `getWorkerManager().getConnection()`.
|
|
1701
|
+
|
|
1702
|
+
3. **No API Removals**: All public APIs from v3.0.10 remain available in v3.0.11+.
|
|
1703
|
+
|
|
1704
|
+
#### Best Practices for Multiple Workers
|
|
1705
|
+
|
|
1706
|
+
```typescript
|
|
1707
|
+
// 1. Separate workers by domain/responsibility
|
|
1708
|
+
TemporalModule.register({
|
|
1709
|
+
connection: { address: 'localhost:7233' },
|
|
1710
|
+
workers: [
|
|
1711
|
+
{
|
|
1712
|
+
taskQueue: 'payments', // Financial transactions
|
|
1713
|
+
workflowsPath: require.resolve('./workflows/payments'),
|
|
1714
|
+
activityClasses: [PaymentActivity, RefundActivity],
|
|
1715
|
+
workerOptions: {
|
|
1716
|
+
maxConcurrentActivityTaskExecutions: 50,
|
|
1717
|
+
},
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
taskQueue: 'notifications', // User notifications
|
|
1721
|
+
workflowsPath: require.resolve('./workflows/notifications'),
|
|
1722
|
+
activityClasses: [EmailActivity, SmsActivity],
|
|
1723
|
+
workerOptions: {
|
|
1724
|
+
maxConcurrentActivityTaskExecutions: 100,
|
|
1725
|
+
},
|
|
1726
|
+
},
|
|
1727
|
+
{
|
|
1728
|
+
taskQueue: 'background-jobs', // Async background processing
|
|
1729
|
+
workflowsPath: require.resolve('./workflows/jobs'),
|
|
1730
|
+
activityClasses: [DataProcessingActivity],
|
|
1731
|
+
autoStart: false, // Start only when needed
|
|
1732
|
+
},
|
|
1733
|
+
],
|
|
1734
|
+
})
|
|
1735
|
+
|
|
1736
|
+
// 2. Monitor worker health individually
|
|
958
1737
|
@Injectable()
|
|
959
|
-
export class
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1738
|
+
export class WorkerHealthService {
|
|
1739
|
+
constructor(private temporal: TemporalService) {}
|
|
1740
|
+
|
|
1741
|
+
async checkAllWorkers() {
|
|
1742
|
+
const allWorkers = this.temporal.getAllWorkers();
|
|
1743
|
+
|
|
1744
|
+
for (const [taskQueue, status] of allWorkers.workers.entries()) {
|
|
1745
|
+
if (!status.isHealthy) {
|
|
1746
|
+
// Alert or restart unhealthy worker
|
|
1747
|
+
await this.restartWorker(taskQueue);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
private async restartWorker(taskQueue: string) {
|
|
1753
|
+
await this.temporal.stopWorkerByTaskQueue(taskQueue);
|
|
1754
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1755
|
+
await this.temporal.startWorkerByTaskQueue(taskQueue);
|
|
966
1756
|
}
|
|
967
1757
|
}
|
|
968
1758
|
```
|
|
969
1759
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1760
|
+
## Troubleshooting
|
|
1761
|
+
|
|
1762
|
+
### Common Issues and Solutions
|
|
1763
|
+
|
|
1764
|
+
#### 1. Connection Errors
|
|
1765
|
+
|
|
1766
|
+
**Problem:** Cannot connect to Temporal server
|
|
1767
|
+
|
|
1768
|
+
```
|
|
1769
|
+
Error: Failed to connect to localhost:7233
|
|
1770
|
+
```
|
|
1771
|
+
|
|
1772
|
+
**Solutions:**
|
|
1773
|
+
```typescript
|
|
1774
|
+
// Check connection configuration
|
|
1775
|
+
const health = temporalService.getHealth();
|
|
1776
|
+
console.log('Connection status:', health.client.connectionStatus);
|
|
1777
|
+
|
|
1778
|
+
// Verify Temporal server is running
|
|
1779
|
+
// docker ps | grep temporal
|
|
1780
|
+
|
|
1781
|
+
// Check connection settings
|
|
1782
|
+
TemporalModule.register({
|
|
1783
|
+
connection: {
|
|
1784
|
+
address: process.env.TEMPORAL_ADDRESS || 'localhost:7233',
|
|
1785
|
+
namespace: 'default',
|
|
1786
|
+
},
|
|
1787
|
+
})
|
|
1788
|
+
```
|
|
1789
|
+
|
|
1790
|
+
#### 2. Activity Not Found
|
|
1791
|
+
|
|
1792
|
+
**Problem:** Workflow cannot find registered activities
|
|
1793
|
+
|
|
1794
|
+
```
|
|
1795
|
+
Error: Activity 'myActivity' not found
|
|
1796
|
+
```
|
|
1797
|
+
|
|
1798
|
+
**Solutions:**
|
|
1799
|
+
```typescript
|
|
1800
|
+
// 1. Ensure activity is in activityClasses array
|
|
1801
|
+
TemporalModule.register({
|
|
1802
|
+
worker: {
|
|
1803
|
+
activityClasses: [MyActivity], // Must include the activity class
|
|
1804
|
+
},
|
|
1805
|
+
})
|
|
1806
|
+
|
|
1807
|
+
// 2. Verify activity is registered as provider
|
|
1808
|
+
@Module({
|
|
1809
|
+
providers: [MyActivity], // Must be in providers array
|
|
1810
|
+
})
|
|
1811
|
+
|
|
1812
|
+
// 3. Check activity decorator
|
|
1813
|
+
@Activity({ name: 'my-activities' })
|
|
1814
|
+
export class MyActivity {
|
|
1815
|
+
@ActivityMethod('myActivity')
|
|
1816
|
+
async myActivity() { }
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// 4. Check discovery status
|
|
1820
|
+
const health = temporalService.getHealth();
|
|
1821
|
+
console.log('Activities discovered:', health.discovery.activitiesDiscovered);
|
|
1822
|
+
```
|
|
1823
|
+
|
|
1824
|
+
#### 3. Workflow Registration Issues
|
|
1825
|
+
|
|
1826
|
+
**Problem:** Workflow not found or not executing
|
|
1827
|
+
|
|
1828
|
+
```
|
|
1829
|
+
Error: Workflow 'myWorkflow' not found
|
|
1830
|
+
```
|
|
1831
|
+
|
|
1832
|
+
**Solutions:**
|
|
1833
|
+
```typescript
|
|
1834
|
+
// 1. Ensure workflowsPath is correct
|
|
1835
|
+
TemporalModule.register({
|
|
1836
|
+
worker: {
|
|
1837
|
+
workflowsPath: require.resolve('./workflows'), // Must resolve to workflows file/directory
|
|
1838
|
+
},
|
|
1839
|
+
})
|
|
1840
|
+
|
|
1841
|
+
// 2. Export workflow function properly
|
|
1842
|
+
// workflows/index.ts
|
|
1843
|
+
export { processOrderWorkflow } from './order.workflow';
|
|
1844
|
+
export { reportWorkflow } from './report.workflow';
|
|
1845
|
+
|
|
1846
|
+
// 3. Use correct workflow name when starting
|
|
1847
|
+
await temporal.startWorkflow(
|
|
1848
|
+
'processOrderWorkflow', // Must match exported function name
|
|
1849
|
+
[args],
|
|
1850
|
+
options
|
|
1851
|
+
);
|
|
1852
|
+
```
|
|
1853
|
+
|
|
1854
|
+
#### 4. Timeout Issues
|
|
1855
|
+
|
|
1856
|
+
**Problem:** Activities or workflows timing out
|
|
1857
|
+
|
|
1858
|
+
```
|
|
1859
|
+
Error: Activity timed out after 10s
|
|
1860
|
+
```
|
|
1861
|
+
|
|
1862
|
+
**Solutions:**
|
|
1863
|
+
```typescript
|
|
1864
|
+
// Configure appropriate timeouts
|
|
1865
|
+
const activities = proxyActivities<typeof MyActivity.prototype>({
|
|
1866
|
+
startToCloseTimeout: '10m', // Increase for long-running activities
|
|
1867
|
+
scheduleToCloseTimeout: '15m', // Total time including queuing
|
|
1868
|
+
scheduleToStartTimeout: '5m', // Time waiting in queue
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
// For workflows
|
|
1872
|
+
await temporal.startWorkflow('myWorkflow', [args], {
|
|
1873
|
+
workflowExecutionTimeout: '24h', // Max total execution time
|
|
1874
|
+
workflowRunTimeout: '12h', // Max single run time
|
|
1875
|
+
workflowTaskTimeout: '10s', // Decision task timeout
|
|
1876
|
+
});
|
|
1877
|
+
```
|
|
1878
|
+
|
|
1879
|
+
#### 5. Worker Not Starting
|
|
1880
|
+
|
|
1881
|
+
**Problem:** Worker fails to start or crashes
|
|
1882
|
+
|
|
1883
|
+
```
|
|
1884
|
+
Error: Worker failed to start
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
**Solutions:**
|
|
1888
|
+
```typescript
|
|
1889
|
+
// 1. Check worker configuration
|
|
1890
|
+
TemporalModule.register({
|
|
1891
|
+
worker: {
|
|
1892
|
+
autoStart: true, // Ensure autoStart is true
|
|
1893
|
+
workflowsPath: require.resolve('./workflows'),
|
|
1894
|
+
activityClasses: [MyActivity],
|
|
1895
|
+
},
|
|
1896
|
+
})
|
|
1897
|
+
|
|
1898
|
+
// 2. Check logs
|
|
1899
|
+
// Enable debug logging
|
|
1900
|
+
TemporalModule.register({
|
|
1901
|
+
logLevel: 'debug',
|
|
1902
|
+
enableLogger: true,
|
|
1903
|
+
})
|
|
1904
|
+
|
|
1905
|
+
// 3. Verify worker health
|
|
1906
|
+
const health = temporalService.getHealth();
|
|
1907
|
+
console.log('Worker status:', health.worker.state);
|
|
1908
|
+
|
|
1909
|
+
// 4. Check for port conflicts or resource issues
|
|
1910
|
+
```
|
|
1911
|
+
|
|
1912
|
+
#### 6. Signal/Query Not Working
|
|
1913
|
+
|
|
1914
|
+
**Problem:** Signals or queries not being handled
|
|
1915
|
+
|
|
1916
|
+
**Solutions:**
|
|
1917
|
+
```typescript
|
|
1918
|
+
// 1. Define signals/queries at module level (not inside workflow)
|
|
1919
|
+
export const mySignal = defineSignal<[string]>('mySignal');
|
|
1920
|
+
export const myQuery = defineQuery<string>('myQuery');
|
|
1921
|
+
|
|
1922
|
+
// 2. Set up handlers in workflow
|
|
1923
|
+
export async function myWorkflow() {
|
|
1924
|
+
let value = 'initial';
|
|
1925
|
+
|
|
1926
|
+
setHandler(mySignal, (newValue: string) => {
|
|
1927
|
+
value = newValue;
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
setHandler(myQuery, () => value);
|
|
1931
|
+
|
|
1932
|
+
// ... workflow logic
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// 3. Use correct names when signaling/querying
|
|
1936
|
+
await temporal.signalWorkflow(workflowId, 'mySignal', ['newValue']);
|
|
1937
|
+
const result = await temporal.queryWorkflow(workflowId, 'myQuery');
|
|
1938
|
+
```
|
|
1939
|
+
|
|
1940
|
+
### Debug Mode
|
|
1941
|
+
|
|
1942
|
+
Enable comprehensive debugging:
|
|
1943
|
+
|
|
1944
|
+
```typescript
|
|
1945
|
+
TemporalModule.register({
|
|
1946
|
+
logLevel: 'debug',
|
|
1947
|
+
enableLogger: true,
|
|
1948
|
+
connection: {
|
|
1949
|
+
address: 'localhost:7233',
|
|
1950
|
+
},
|
|
1951
|
+
worker: {
|
|
1952
|
+
debugMode: true, // If available
|
|
1953
|
+
},
|
|
1954
|
+
})
|
|
1955
|
+
|
|
1956
|
+
// Check detailed health and statistics
|
|
1957
|
+
const health = temporalService.getHealth();
|
|
1958
|
+
const stats = temporalService.getStatistics();
|
|
1959
|
+
console.log('Health:', JSON.stringify(health, null, 2));
|
|
1960
|
+
console.log('Stats:', JSON.stringify(stats, null, 2));
|
|
1961
|
+
```
|
|
1962
|
+
|
|
1963
|
+
### Getting Help
|
|
1964
|
+
|
|
1965
|
+
If you're still experiencing issues:
|
|
1966
|
+
|
|
1967
|
+
1. **Check the logs** - Enable debug logging to see detailed information
|
|
1968
|
+
2. **Verify configuration** - Double-check all connection and worker settings
|
|
1969
|
+
3. **Test connectivity** - Ensure Temporal server is accessible
|
|
1970
|
+
4. **Review health status** - Use `getHealth()` to identify failing components
|
|
1971
|
+
5. **Check GitHub Issues** - [Search existing issues](https://github.com/harsh-simform/nestjs-temporal-core/issues)
|
|
1972
|
+
6. **Create an issue** - Provide logs, configuration, and minimal reproduction
|
|
1973
|
+
|
|
1974
|
+
## Requirements
|
|
1975
|
+
|
|
1976
|
+
- **Node.js**: >= 16.0.0
|
|
1977
|
+
- **NestJS**: >= 9.0.0
|
|
1978
|
+
- **Temporal Server**: >= 1.20.0
|
|
1979
|
+
|
|
1980
|
+
## Contributing
|
|
1981
|
+
|
|
1982
|
+
We welcome contributions! To contribute:
|
|
1983
|
+
|
|
1984
|
+
1. Fork the repository
|
|
1985
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
1986
|
+
3. Make your changes
|
|
1987
|
+
4. Run tests (`npm test`)
|
|
1988
|
+
5. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
1989
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
1990
|
+
7. Open a Pull Request
|
|
1991
|
+
|
|
1992
|
+
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
|
|
1993
|
+
|
|
1994
|
+
### Development Setup
|
|
1995
|
+
|
|
1996
|
+
```bash
|
|
1997
|
+
# Clone the repository
|
|
1998
|
+
git clone https://github.com/harsh-simform/nestjs-temporal-core.git
|
|
1999
|
+
cd nestjs-temporal-core
|
|
2000
|
+
|
|
2001
|
+
# Install dependencies
|
|
2002
|
+
npm install
|
|
2003
|
+
|
|
2004
|
+
# Run tests
|
|
2005
|
+
npm test
|
|
2006
|
+
|
|
2007
|
+
# Run tests with coverage
|
|
2008
|
+
npm run test:cov
|
|
2009
|
+
|
|
2010
|
+
# Build the package
|
|
2011
|
+
npm run build
|
|
2012
|
+
|
|
2013
|
+
# Generate documentation
|
|
2014
|
+
npm run docs:generate
|
|
2015
|
+
```
|
|
2016
|
+
|
|
2017
|
+
## License
|
|
2018
|
+
|
|
2019
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
2020
|
+
|
|
2021
|
+
## Support and Resources
|
|
2022
|
+
|
|
2023
|
+
- 📚 **Documentation**: [Full API Documentation](https://harsh-simform.github.io/nestjs-temporal-core/)
|
|
2024
|
+
- 🐛 **Issues**: [GitHub Issues](https://github.com/harsh-simform/nestjs-temporal-core/issues)
|
|
2025
|
+
- 💬 **Discussions**: [GitHub Discussions](https://github.com/harsh-simform/nestjs-temporal-core/discussions)
|
|
2026
|
+
- 📦 **NPM**: [nestjs-temporal-core](https://www.npmjs.com/package/nestjs-temporal-core)
|
|
2027
|
+
- 🔄 **Changelog**: [Releases](https://github.com/harsh-simform/nestjs-temporal-core/releases)
|
|
2028
|
+
- 📖 **Example Project**: [nestjs-temporal-core-example](https://github.com/harsh-simform/nestjs-temporal-core-example)
|
|
2029
|
+
|
|
2030
|
+
## Related Projects
|
|
2031
|
+
|
|
2032
|
+
- [Temporal.io](https://temporal.io/) - The underlying workflow orchestration platform
|
|
2033
|
+
- [NestJS](https://nestjs.com/) - Progressive Node.js framework
|
|
2034
|
+
- [@temporalio/sdk](https://www.npmjs.com/package/@temporalio/client) - Official Temporal TypeScript SDK
|
|
974
2035
|
|
|
975
2036
|
---
|
|
2037
|
+
|