nestjs-temporal-core 3.1.3 → 3.1.5
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 +12 -0
- package/README.md +169 -852
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
## [3.1.5] - 2025-11-12
|
|
3
|
+
|
|
4
|
+
### Changes
|
|
5
|
+
|
|
6
|
+
- Update documentation for TIMEOUTS and WORKFLOW_PARAMS_METADATA; remove WorkflowRun documentation (c293dc5)
|
|
7
|
+
|
|
8
|
+
## [3.1.4] - 2025-11-12
|
|
9
|
+
|
|
10
|
+
### Changes
|
|
11
|
+
|
|
12
|
+
- chore: add environment configuration for GitHub Pages deployment (2236d03)
|
|
13
|
+
|
|
2
14
|
## [3.1.3] - 2025-11-12
|
|
3
15
|
|
|
4
16
|
### Changes
|
package/README.md
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
# NestJS Temporal Core
|
|
2
2
|
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
3
5
|
A comprehensive NestJS integration framework for Temporal.io that provides enterprise-ready workflow orchestration with automatic discovery, declarative decorators, and robust monitoring capabilities.
|
|
4
6
|
|
|
5
7
|
[](https://badge.fury.io/js/nestjs-temporal-core)
|
|
6
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://www.npmjs.com/package/nestjs-temporal-core)
|
|
10
|
+
|
|
11
|
+
[Documentation](https://harsh-simform.github.io/nestjs-temporal-core/) • [NPM](https://www.npmjs.com/package/nestjs-temporal-core) • [GitHub](https://github.com/harsh-simform/nestjs-temporal-core) • [Examples](#examples)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
7
16
|
|
|
8
17
|
## Table of Contents
|
|
9
18
|
|
|
@@ -13,13 +22,21 @@ A comprehensive NestJS integration framework for Temporal.io that provides enter
|
|
|
13
22
|
- [Quick Start](#quick-start)
|
|
14
23
|
- [Module Variants](#module-variants)
|
|
15
24
|
- [Configuration](#configuration)
|
|
25
|
+
- [Basic Configuration](#basic-configuration)
|
|
26
|
+
- [Multiple Workers](#multiple-workers-configuration)
|
|
27
|
+
- [Async Configuration](#async-configuration)
|
|
28
|
+
- [TLS Configuration](#tls-configuration-temporal-cloud)
|
|
16
29
|
- [Core Concepts](#core-concepts)
|
|
30
|
+
- [Activities](#activities)
|
|
31
|
+
- [Workflows](#workflows)
|
|
32
|
+
- [Signals and Queries](#signals-and-queries)
|
|
17
33
|
- [API Reference](#api-reference)
|
|
18
34
|
- [Examples](#examples)
|
|
19
35
|
- [Advanced Usage](#advanced-usage)
|
|
20
|
-
- [Health Monitoring](#health-monitoring)
|
|
21
36
|
- [Best Practices](#best-practices)
|
|
37
|
+
- [Health Monitoring](#health-monitoring)
|
|
22
38
|
- [Troubleshooting](#troubleshooting)
|
|
39
|
+
- [Migration Guide](#migration-guide)
|
|
23
40
|
- [Contributing](#contributing)
|
|
24
41
|
- [License](#license)
|
|
25
42
|
|
|
@@ -37,6 +54,8 @@ NestJS Temporal Core bridges NestJS's powerful dependency injection system with
|
|
|
37
54
|
- 📦 **Modular Architecture**: Separate modules for client, worker, activities, and schedules
|
|
38
55
|
- 🔄 **Production Grade**: Connection pooling, graceful shutdown, and fault tolerance
|
|
39
56
|
|
|
57
|
+
[🔝 Back to top](#table-of-contents)
|
|
58
|
+
|
|
40
59
|
## Features
|
|
41
60
|
|
|
42
61
|
- ✨ **Declarative Decorators**: `@Activity()` and `@ActivityMethod()` for clean activity definitions
|
|
@@ -48,6 +67,9 @@ NestJS Temporal Core bridges NestJS's powerful dependency injection system with
|
|
|
48
67
|
- 📊 **Performance Monitoring**: Built-in metrics and performance tracking
|
|
49
68
|
- 🔚 **Graceful Shutdown**: Clean resource cleanup and connection termination
|
|
50
69
|
- 📦 **Modular Design**: Use only what you need (client-only, worker-only, etc.)
|
|
70
|
+
- 🔄 **Multiple Workers**: Support for multiple workers with different task queues
|
|
71
|
+
|
|
72
|
+
[🔝 Back to top](#table-of-contents)
|
|
51
73
|
|
|
52
74
|
## Installation
|
|
53
75
|
|
|
@@ -57,17 +79,17 @@ npm install nestjs-temporal-core @temporalio/client @temporalio/worker @temporal
|
|
|
57
79
|
|
|
58
80
|
### Peer Dependencies
|
|
59
81
|
|
|
60
|
-
The package requires the following peer dependencies:
|
|
61
|
-
|
|
62
82
|
```bash
|
|
63
83
|
npm install @nestjs/common @nestjs/core reflect-metadata rxjs
|
|
64
84
|
```
|
|
65
85
|
|
|
86
|
+
[🔝 Back to top](#table-of-contents)
|
|
87
|
+
|
|
66
88
|
## Quick Start
|
|
67
89
|
|
|
68
90
|
### 1. Enable Shutdown Hooks
|
|
69
91
|
|
|
70
|
-
|
|
92
|
+
Enable shutdown hooks in your `main.ts` for proper Temporal resource cleanup:
|
|
71
93
|
|
|
72
94
|
```typescript
|
|
73
95
|
// main.ts
|
|
@@ -76,10 +98,10 @@ import { AppModule } from './app.module';
|
|
|
76
98
|
|
|
77
99
|
async function bootstrap() {
|
|
78
100
|
const app = await NestFactory.create(AppModule);
|
|
79
|
-
|
|
101
|
+
|
|
80
102
|
// Required for graceful Temporal connection cleanup
|
|
81
103
|
app.enableShutdownHooks();
|
|
82
|
-
|
|
104
|
+
|
|
83
105
|
await app.listen(3000);
|
|
84
106
|
}
|
|
85
107
|
bootstrap();
|
|
@@ -134,15 +156,15 @@ export interface PaymentData {
|
|
|
134
156
|
@Injectable()
|
|
135
157
|
@Activity({ name: 'payment-activities' })
|
|
136
158
|
export class PaymentActivity {
|
|
137
|
-
|
|
159
|
+
|
|
138
160
|
@ActivityMethod('processPayment')
|
|
139
161
|
async processPayment(data: PaymentData): Promise<{ transactionId: string }> {
|
|
140
162
|
// Payment processing logic with full NestJS DI support
|
|
141
163
|
console.log(`Processing payment: $${data.amount} ${data.currency}`);
|
|
142
|
-
|
|
164
|
+
|
|
143
165
|
// Simulate payment processing
|
|
144
166
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
145
|
-
|
|
167
|
+
|
|
146
168
|
return { transactionId: `txn_${Date.now()}` };
|
|
147
169
|
}
|
|
148
170
|
|
|
@@ -193,7 +215,7 @@ export async function processPaymentWorkflow(data: PaymentData): Promise<any> {
|
|
|
193
215
|
const result = await processPayment(data);
|
|
194
216
|
transactionId = result.transactionId;
|
|
195
217
|
status = 'completed';
|
|
196
|
-
|
|
218
|
+
|
|
197
219
|
return {
|
|
198
220
|
success: true,
|
|
199
221
|
transactionId,
|
|
@@ -201,12 +223,12 @@ export async function processPaymentWorkflow(data: PaymentData): Promise<any> {
|
|
|
201
223
|
};
|
|
202
224
|
} catch (error) {
|
|
203
225
|
status = 'failed';
|
|
204
|
-
|
|
226
|
+
|
|
205
227
|
// Compensate if needed
|
|
206
228
|
if (transactionId) {
|
|
207
229
|
await refundPayment(transactionId);
|
|
208
230
|
}
|
|
209
|
-
|
|
231
|
+
|
|
210
232
|
throw error;
|
|
211
233
|
}
|
|
212
234
|
}
|
|
@@ -230,7 +252,7 @@ export class PaymentService {
|
|
|
230
252
|
const result = await this.temporal.startWorkflow(
|
|
231
253
|
'processPaymentWorkflow',
|
|
232
254
|
[paymentData],
|
|
233
|
-
{
|
|
255
|
+
{
|
|
234
256
|
workflowId: `payment-${Date.now()}`,
|
|
235
257
|
taskQueue: 'my-task-queue',
|
|
236
258
|
}
|
|
@@ -248,7 +270,7 @@ export class PaymentService {
|
|
|
248
270
|
workflowId,
|
|
249
271
|
'getPaymentStatus'
|
|
250
272
|
);
|
|
251
|
-
|
|
273
|
+
|
|
252
274
|
return { status: statusResult.result };
|
|
253
275
|
}
|
|
254
276
|
|
|
@@ -263,6 +285,8 @@ export class PaymentService {
|
|
|
263
285
|
}
|
|
264
286
|
```
|
|
265
287
|
|
|
288
|
+
[🔝 Back to top](#table-of-contents)
|
|
289
|
+
|
|
266
290
|
## Module Variants
|
|
267
291
|
|
|
268
292
|
The package provides modular architecture with separate modules for different use cases:
|
|
@@ -338,7 +362,7 @@ TemporalSchedulesModule.register({
|
|
|
338
362
|
})
|
|
339
363
|
```
|
|
340
364
|
|
|
341
|
-
|
|
365
|
+
[🔝 Back to top](#table-of-contents)
|
|
342
366
|
|
|
343
367
|
## Configuration
|
|
344
368
|
|
|
@@ -616,6 +640,8 @@ interface TemporalOptions {
|
|
|
616
640
|
}
|
|
617
641
|
```
|
|
618
642
|
|
|
643
|
+
[🔝 Back to top](#table-of-contents)
|
|
644
|
+
|
|
619
645
|
## Core Concepts
|
|
620
646
|
|
|
621
647
|
### Activities
|
|
@@ -637,7 +663,7 @@ export class OrderActivity {
|
|
|
637
663
|
private readonly orderRepository: OrderRepository,
|
|
638
664
|
private readonly emailService: EmailService,
|
|
639
665
|
) {}
|
|
640
|
-
|
|
666
|
+
|
|
641
667
|
@ActivityMethod('createOrder')
|
|
642
668
|
async createOrder(orderData: CreateOrderData): Promise<Order> {
|
|
643
669
|
// Database operations with full DI support
|
|
@@ -779,7 +805,7 @@ export class OrderService {
|
|
|
779
805
|
workflowId,
|
|
780
806
|
'getOrderStatus'
|
|
781
807
|
);
|
|
782
|
-
|
|
808
|
+
|
|
783
809
|
return result.result;
|
|
784
810
|
}
|
|
785
811
|
|
|
@@ -793,417 +819,47 @@ export class OrderService {
|
|
|
793
819
|
}
|
|
794
820
|
```
|
|
795
821
|
|
|
796
|
-
|
|
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:
|
|
822
|
+
[🔝 Back to top](#table-of-contents)
|
|
893
823
|
|
|
894
|
-
|
|
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
|
-
```
|
|
824
|
+
## API Reference
|
|
905
825
|
|
|
906
|
-
|
|
826
|
+
For detailed API documentation, visit the [Full API Documentation](https://harsh-simform.github.io/nestjs-temporal-core/).
|
|
907
827
|
|
|
908
|
-
|
|
909
|
-
interface WorkflowExecutionResult<T> {
|
|
910
|
-
success: boolean;
|
|
911
|
-
result: T; // Contains workflowId, runId, etc.
|
|
912
|
-
executionTime: number;
|
|
913
|
-
error?: Error;
|
|
914
|
-
}
|
|
828
|
+
### TemporalService
|
|
915
829
|
|
|
916
|
-
|
|
917
|
-
success: boolean;
|
|
918
|
-
result: T;
|
|
919
|
-
workflowId: string;
|
|
920
|
-
queryName: string;
|
|
921
|
-
}
|
|
830
|
+
The main unified service providing access to all Temporal functionality. See the [API Documentation](https://harsh-simform.github.io/nestjs-temporal-core/) for complete method signatures and examples.
|
|
922
831
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
832
|
+
Key methods:
|
|
833
|
+
- `startWorkflow()` - Start a workflow execution
|
|
834
|
+
- `signalWorkflow()` - Send a signal to a running workflow
|
|
835
|
+
- `queryWorkflow()` - Query a running workflow
|
|
836
|
+
- `getWorkflowHandle()` - Get a workflow handle to interact with it
|
|
837
|
+
- `terminateWorkflow()` - Terminate a workflow execution
|
|
838
|
+
- `cancelWorkflow()` - Cancel a workflow execution
|
|
839
|
+
- `getHealth()` - Get service health status
|
|
840
|
+
- `createSchedule()` - Create a schedule
|
|
841
|
+
- `listSchedules()` - List all schedules
|
|
842
|
+
- `deleteSchedule()` - Delete a schedule
|
|
929
843
|
|
|
930
|
-
|
|
844
|
+
[🔝 Back to top](#table-of-contents)
|
|
931
845
|
|
|
932
846
|
## Examples
|
|
933
847
|
|
|
934
|
-
|
|
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
|
-
) {}
|
|
948
|
-
|
|
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);
|
|
1060
|
-
}
|
|
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
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
```
|
|
1103
|
-
|
|
1104
|
-
### Example 2: Scheduled Reports
|
|
1105
|
-
|
|
1106
|
-
Creating and managing scheduled workflows:
|
|
1107
|
-
|
|
1108
|
-
```typescript
|
|
1109
|
-
// report.activity.ts
|
|
1110
|
-
@Injectable()
|
|
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
|
-
}
|
|
1123
|
-
|
|
1124
|
-
@ActivityMethod('uploadReport')
|
|
1125
|
-
async uploadReport(reportData: ReportData): Promise<string> {
|
|
1126
|
-
return await this.storageService.upload(reportData);
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
@ActivityMethod('notifyStakeholders')
|
|
1130
|
-
async notifyStakeholders(reportUrl: string, recipients: string[]): Promise<void> {
|
|
1131
|
-
await this.notificationService.send(recipients, reportUrl);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
|
|
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
|
-
});
|
|
1189
|
-
}
|
|
848
|
+
For complete working examples, visit our [documentation](https://harsh-simform.github.io/nestjs-temporal-core/). The README includes example scenarios for:
|
|
1190
849
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
850
|
+
1. **E-commerce Order Processing** - Complete example with compensation logic
|
|
851
|
+
2. **Scheduled Reports** - Creating and managing scheduled workflows
|
|
852
|
+
3. **Activity Retry Configuration** - Custom retry policies
|
|
853
|
+
4. **Child Workflows** - Organizing complex workflows
|
|
854
|
+
5. **Continue-As-New** - For long-running workflows
|
|
855
|
+
6. **Custom Error Handling** - Implementing custom error types
|
|
1194
856
|
|
|
1195
|
-
|
|
1196
|
-
return await this.temporal.listSchedules();
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
```
|
|
857
|
+
[🔝 Back to top](#table-of-contents)
|
|
1200
858
|
|
|
1201
859
|
## Advanced Usage
|
|
1202
860
|
|
|
1203
861
|
### Activity Retry Configuration
|
|
1204
862
|
|
|
1205
|
-
Configure custom retry policies for different activity types:
|
|
1206
|
-
|
|
1207
863
|
```typescript
|
|
1208
864
|
// workflow.ts
|
|
1209
865
|
const paymentActivities = proxyActivities<typeof PaymentActivity.prototype>({
|
|
@@ -1216,25 +872,14 @@ const paymentActivities = proxyActivities<typeof PaymentActivity.prototype>({
|
|
|
1216
872
|
nonRetryableErrorTypes: ['InvalidPaymentMethod', 'InsufficientFunds'],
|
|
1217
873
|
},
|
|
1218
874
|
});
|
|
1219
|
-
|
|
1220
|
-
const emailActivities = proxyActivities<typeof EmailActivity.prototype>({
|
|
1221
|
-
startToCloseTimeout: '2m',
|
|
1222
|
-
retry: {
|
|
1223
|
-
maximumAttempts: 3,
|
|
1224
|
-
initialInterval: '500ms',
|
|
1225
|
-
},
|
|
1226
|
-
});
|
|
1227
875
|
```
|
|
1228
876
|
|
|
1229
877
|
### Workflow Testing
|
|
1230
878
|
|
|
1231
|
-
Test workflows using Temporal's testing framework:
|
|
1232
|
-
|
|
1233
879
|
```typescript
|
|
1234
880
|
import { TestWorkflowEnvironment } from '@temporalio/testing';
|
|
1235
881
|
import { Worker } from '@temporalio/worker';
|
|
1236
882
|
import { processOrderWorkflow } from './order.workflow';
|
|
1237
|
-
import { OrderActivity } from './order.activity';
|
|
1238
883
|
|
|
1239
884
|
describe('Order Workflow', () => {
|
|
1240
885
|
let testEnv: TestWorkflowEnvironment;
|
|
@@ -1283,102 +928,7 @@ describe('Order Workflow', () => {
|
|
|
1283
928
|
});
|
|
1284
929
|
```
|
|
1285
930
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
Organize complex workflows using child workflows:
|
|
1289
|
-
|
|
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);
|
|
1328
|
-
|
|
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
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
```
|
|
1339
|
-
|
|
1340
|
-
### Custom Error Handling
|
|
1341
|
-
|
|
1342
|
-
Implement custom error types and handling:
|
|
1343
|
-
|
|
1344
|
-
```typescript
|
|
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');
|
|
1369
|
-
}
|
|
1370
|
-
throw error;
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
// workflow configuration
|
|
1375
|
-
const activities = proxyActivities<typeof DataActivity.prototype>({
|
|
1376
|
-
startToCloseTimeout: '5m',
|
|
1377
|
-
retry: {
|
|
1378
|
-
nonRetryableErrorTypes: ['NonRetryableError'],
|
|
1379
|
-
},
|
|
1380
|
-
});
|
|
1381
|
-
```
|
|
931
|
+
[🔝 Back to top](#table-of-contents)
|
|
1382
932
|
|
|
1383
933
|
## Best Practices
|
|
1384
934
|
|
|
@@ -1453,6 +1003,8 @@ const activities = proxyActivities<typeof DataActivity.prototype>({
|
|
|
1453
1003
|
- Don't test against production Temporal server
|
|
1454
1004
|
- Don't assume workflows are correct without testing
|
|
1455
1005
|
|
|
1006
|
+
[🔝 Back to top](#table-of-contents)
|
|
1007
|
+
|
|
1456
1008
|
## Health Monitoring
|
|
1457
1009
|
|
|
1458
1010
|
The package includes comprehensive health monitoring capabilities for production deployments.
|
|
@@ -1491,7 +1043,7 @@ export class HealthController {
|
|
|
1491
1043
|
@Get('/status')
|
|
1492
1044
|
async getHealthStatus() {
|
|
1493
1045
|
const health = this.temporal.getHealth();
|
|
1494
|
-
|
|
1046
|
+
|
|
1495
1047
|
return {
|
|
1496
1048
|
status: health.overallHealth,
|
|
1497
1049
|
timestamp: new Date(),
|
|
@@ -1513,270 +1065,27 @@ export class HealthController {
|
|
|
1513
1065
|
uptime: health.uptime,
|
|
1514
1066
|
};
|
|
1515
1067
|
}
|
|
1516
|
-
|
|
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
1068
|
}
|
|
1532
1069
|
```
|
|
1533
1070
|
|
|
1534
|
-
|
|
1071
|
+
[🔝 Back to top](#table-of-contents)
|
|
1535
1072
|
|
|
1536
|
-
|
|
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
|
-
```
|
|
1073
|
+
## Troubleshooting
|
|
1557
1074
|
|
|
1558
|
-
|
|
1075
|
+
### Common Issues and Solutions
|
|
1559
1076
|
|
|
1560
|
-
|
|
1077
|
+
#### 1. Connection Errors
|
|
1561
1078
|
|
|
1562
|
-
|
|
1079
|
+
**Problem:** Cannot connect to Temporal server
|
|
1563
1080
|
|
|
1564
|
-
|
|
1081
|
+
**Solutions:**
|
|
1082
|
+
```typescript
|
|
1083
|
+
// Check connection configuration
|
|
1084
|
+
const health = temporalService.getHealth();
|
|
1085
|
+
console.log('Connection status:', health.client.connectionStatus);
|
|
1565
1086
|
|
|
1566
|
-
|
|
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
|
|
1581
|
-
|
|
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;
|
|
1588
|
-
|
|
1589
|
-
constructor(private temporal: TemporalService) {}
|
|
1590
|
-
|
|
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!
|
|
1595
|
-
|
|
1596
|
-
// Manual worker creation...
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
```
|
|
1600
|
-
|
|
1601
|
-
**After (v3.0.12):**
|
|
1602
|
-
```typescript
|
|
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
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
```
|
|
1663
|
-
|
|
1664
|
-
#### New APIs in v3.0.12
|
|
1665
|
-
|
|
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
|
|
1737
|
-
@Injectable()
|
|
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);
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
```
|
|
1759
|
-
|
|
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
|
|
1087
|
+
// Verify Temporal server is running
|
|
1088
|
+
// docker ps | grep temporal
|
|
1780
1089
|
|
|
1781
1090
|
// Check connection settings
|
|
1782
1091
|
TemporalModule.register({
|
|
@@ -1791,10 +1100,6 @@ TemporalModule.register({
|
|
|
1791
1100
|
|
|
1792
1101
|
**Problem:** Workflow cannot find registered activities
|
|
1793
1102
|
|
|
1794
|
-
```
|
|
1795
|
-
Error: Activity 'myActivity' not found
|
|
1796
|
-
```
|
|
1797
|
-
|
|
1798
1103
|
**Solutions:**
|
|
1799
1104
|
```typescript
|
|
1800
1105
|
// 1. Ensure activity is in activityClasses array
|
|
@@ -1825,10 +1130,6 @@ console.log('Activities discovered:', health.discovery.activitiesDiscovered);
|
|
|
1825
1130
|
|
|
1826
1131
|
**Problem:** Workflow not found or not executing
|
|
1827
1132
|
|
|
1828
|
-
```
|
|
1829
|
-
Error: Workflow 'myWorkflow' not found
|
|
1830
|
-
```
|
|
1831
|
-
|
|
1832
1133
|
**Solutions:**
|
|
1833
1134
|
```typescript
|
|
1834
1135
|
// 1. Ensure workflowsPath is correct
|
|
@@ -1855,10 +1156,6 @@ await temporal.startWorkflow(
|
|
|
1855
1156
|
|
|
1856
1157
|
**Problem:** Activities or workflows timing out
|
|
1857
1158
|
|
|
1858
|
-
```
|
|
1859
|
-
Error: Activity timed out after 10s
|
|
1860
|
-
```
|
|
1861
|
-
|
|
1862
1159
|
**Solutions:**
|
|
1863
1160
|
```typescript
|
|
1864
1161
|
// Configure appropriate timeouts
|
|
@@ -1876,67 +1173,6 @@ await temporal.startWorkflow('myWorkflow', [args], {
|
|
|
1876
1173
|
});
|
|
1877
1174
|
```
|
|
1878
1175
|
|
|
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
1176
|
### Debug Mode
|
|
1941
1177
|
|
|
1942
1178
|
Enable comprehensive debugging:
|
|
@@ -1971,10 +1207,85 @@ If you're still experiencing issues:
|
|
|
1971
1207
|
5. **Check GitHub Issues** - [Search existing issues](https://github.com/harsh-simform/nestjs-temporal-core/issues)
|
|
1972
1208
|
6. **Create an issue** - Provide logs, configuration, and minimal reproduction
|
|
1973
1209
|
|
|
1210
|
+
[🔝 Back to top](#table-of-contents)
|
|
1211
|
+
|
|
1212
|
+
## Migration Guide
|
|
1213
|
+
|
|
1214
|
+
### Migrating to v3.0.12+ (Multiple Workers Support)
|
|
1215
|
+
|
|
1216
|
+
Version 3.0.12 introduces support for multiple workers without breaking existing single-worker configurations.
|
|
1217
|
+
|
|
1218
|
+
#### No Changes Required for Single Worker
|
|
1219
|
+
|
|
1220
|
+
Your existing configuration continues to work:
|
|
1221
|
+
|
|
1222
|
+
```typescript
|
|
1223
|
+
// ✅ This still works exactly as before
|
|
1224
|
+
TemporalModule.register({
|
|
1225
|
+
connection: { address: 'localhost:7233' },
|
|
1226
|
+
taskQueue: 'my-queue',
|
|
1227
|
+
worker: {
|
|
1228
|
+
workflowsPath: require.resolve('./workflows'),
|
|
1229
|
+
activityClasses: [MyActivity],
|
|
1230
|
+
},
|
|
1231
|
+
})
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
#### Migrating to Multiple Workers
|
|
1235
|
+
|
|
1236
|
+
**After (v3.0.12):**
|
|
1237
|
+
```typescript
|
|
1238
|
+
// Option 1: Configure multiple workers in module
|
|
1239
|
+
TemporalModule.register({
|
|
1240
|
+
connection: { address: 'localhost:7233' },
|
|
1241
|
+
workers: [
|
|
1242
|
+
{
|
|
1243
|
+
taskQueue: 'main-queue',
|
|
1244
|
+
workflowsPath: require.resolve('./workflows/main'),
|
|
1245
|
+
activityClasses: [MainActivity],
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
taskQueue: 'schedule-queue',
|
|
1249
|
+
workflowsPath: require.resolve('./workflows/schedule'),
|
|
1250
|
+
activityClasses: [ScheduleActivity],
|
|
1251
|
+
},
|
|
1252
|
+
],
|
|
1253
|
+
})
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
#### New APIs in v3.0.12
|
|
1257
|
+
|
|
1258
|
+
```typescript
|
|
1259
|
+
// Get native connection for custom worker creation
|
|
1260
|
+
const workerManager = temporal.getWorkerManager();
|
|
1261
|
+
const connection: NativeConnection | null = workerManager.getConnection();
|
|
1262
|
+
|
|
1263
|
+
// Get specific worker by task queue
|
|
1264
|
+
const worker: Worker | null = temporal.getWorker('payments-queue');
|
|
1265
|
+
|
|
1266
|
+
// Get all workers information
|
|
1267
|
+
const workersInfo: MultipleWorkersInfo = temporal.getAllWorkers();
|
|
1268
|
+
console.log(`${workersInfo.runningWorkers}/${workersInfo.totalWorkers} workers running`);
|
|
1269
|
+
|
|
1270
|
+
// Control specific workers
|
|
1271
|
+
await temporal.startWorkerByTaskQueue('payments-queue');
|
|
1272
|
+
await temporal.stopWorkerByTaskQueue('notifications-queue');
|
|
1273
|
+
|
|
1274
|
+
// Register new worker dynamically
|
|
1275
|
+
const result = await temporal.registerWorker({
|
|
1276
|
+
taskQueue: 'new-queue',
|
|
1277
|
+
workflowsPath: require.resolve('./workflows/new'),
|
|
1278
|
+
activityClasses: [NewActivity],
|
|
1279
|
+
autoStart: true,
|
|
1280
|
+
});
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
[🔝 Back to top](#table-of-contents)
|
|
1284
|
+
|
|
1974
1285
|
## Requirements
|
|
1975
1286
|
|
|
1976
1287
|
- **Node.js**: >= 16.0.0
|
|
1977
|
-
- **NestJS**: >= 9.0.0
|
|
1288
|
+
- **NestJS**: >= 9.0.0
|
|
1978
1289
|
- **Temporal Server**: >= 1.20.0
|
|
1979
1290
|
|
|
1980
1291
|
## Contributing
|
|
@@ -1989,8 +1300,6 @@ We welcome contributions! To contribute:
|
|
|
1989
1300
|
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
1990
1301
|
7. Open a Pull Request
|
|
1991
1302
|
|
|
1992
|
-
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
|
|
1993
|
-
|
|
1994
1303
|
### Development Setup
|
|
1995
1304
|
|
|
1996
1305
|
```bash
|
|
@@ -2014,6 +1323,8 @@ npm run build
|
|
|
2014
1323
|
npm run docs:generate
|
|
2015
1324
|
```
|
|
2016
1325
|
|
|
1326
|
+
[🔝 Back to top](#table-of-contents)
|
|
1327
|
+
|
|
2017
1328
|
## License
|
|
2018
1329
|
|
|
2019
1330
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -2025,7 +1336,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
2025
1336
|
- 💬 **Discussions**: [GitHub Discussions](https://github.com/harsh-simform/nestjs-temporal-core/discussions)
|
|
2026
1337
|
- 📦 **NPM**: [nestjs-temporal-core](https://www.npmjs.com/package/nestjs-temporal-core)
|
|
2027
1338
|
- 🔄 **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
1339
|
|
|
2030
1340
|
## Related Projects
|
|
2031
1341
|
|
|
@@ -2035,3 +1345,10 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
2035
1345
|
|
|
2036
1346
|
---
|
|
2037
1347
|
|
|
1348
|
+
<div align="center">
|
|
1349
|
+
|
|
1350
|
+
**[⭐ Star us on GitHub](https://github.com/harsh-simform/nestjs-temporal-core)** if you find this project helpful!
|
|
1351
|
+
|
|
1352
|
+
Made with ❤️ by the Harsh Simform
|
|
1353
|
+
|
|
1354
|
+
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nestjs-temporal-core",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.5",
|
|
4
4
|
"description": "Complete NestJS integration for Temporal.io with auto-discovery, declarative scheduling, enhanced monitoring, and enterprise-ready features",
|
|
5
5
|
"author": "Harsh M",
|
|
6
6
|
"license": "MIT",
|