nestjs-temporal-core 3.1.4 → 3.1.6
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 +166 -836
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
## [3.1.6] - 2025-11-12
|
|
3
|
+
|
|
4
|
+
### Changes
|
|
5
|
+
|
|
6
|
+
- chore: update release workflow to generate documentation after version bump; improve README with example project details (15cd112)
|
|
7
|
+
|
|
8
|
+
## [3.1.5] - 2025-11-12
|
|
9
|
+
|
|
10
|
+
### Changes
|
|
11
|
+
|
|
12
|
+
- Update documentation for TIMEOUTS and WORKFLOW_PARAMS_METADATA; remove WorkflowRun documentation (c293dc5)
|
|
13
|
+
|
|
2
14
|
## [3.1.4] - 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) • [Example Project](https://github.com/harsh-simform/nestjs-temporal-core-example)
|
|
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,59 @@ 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
|
-
### Example
|
|
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
|
-
}
|
|
848
|
+
### 🚀 Example Project
|
|
1057
849
|
|
|
1058
|
-
|
|
1059
|
-
await refundPayment(transactionId);
|
|
1060
|
-
}
|
|
850
|
+
Check out our **[complete example repository](https://github.com/harsh-simform/nestjs-temporal-core-example)** featuring:
|
|
1061
851
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
852
|
+
- ✅ **Real-world implementations** - Production-ready examples
|
|
853
|
+
- ✅ **Multiple use cases** - E-commerce, notifications, reports, and more
|
|
854
|
+
- ✅ **Best practices** - Following all recommended patterns
|
|
855
|
+
- ✅ **Docker setup** - Ready-to-run with docker-compose
|
|
856
|
+
- ✅ **Test coverage** - Comprehensive test examples
|
|
1066
857
|
|
|
1067
|
-
|
|
1068
|
-
@Injectable()
|
|
1069
|
-
export class OrderService {
|
|
1070
|
-
constructor(private readonly temporal: TemporalService) {}
|
|
858
|
+
### 📚 Documentation Examples
|
|
1071
859
|
|
|
1072
|
-
|
|
1073
|
-
const result = await this.temporal.startWorkflow(
|
|
1074
|
-
'processOrderWorkflow',
|
|
1075
|
-
[orderData],
|
|
1076
|
-
{
|
|
1077
|
-
workflowId: `order-${orderData.orderId}`,
|
|
1078
|
-
taskQueue: 'order-queue',
|
|
1079
|
-
}
|
|
1080
|
-
);
|
|
860
|
+
For more examples, visit our [documentation](https://harsh-simform.github.io/nestjs-temporal-core/). Key example scenarios include:
|
|
1081
861
|
|
|
1082
|
-
|
|
1083
|
-
|
|
862
|
+
1. **E-commerce Order Processing** - Complete example with compensation logic
|
|
863
|
+
2. **Scheduled Reports** - Creating and managing scheduled workflows
|
|
864
|
+
3. **Activity Retry Configuration** - Custom retry policies
|
|
865
|
+
4. **Child Workflows** - Organizing complex workflows
|
|
866
|
+
5. **Continue-As-New** - For long-running workflows
|
|
867
|
+
6. **Custom Error Handling** - Implementing custom error types
|
|
1084
868
|
|
|
1085
|
-
|
|
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
|
-
}
|
|
1190
|
-
|
|
1191
|
-
async deleteSchedule(scheduleId: string) {
|
|
1192
|
-
await this.temporal.deleteSchedule(scheduleId);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
async listAllSchedules() {
|
|
1196
|
-
return await this.temporal.listSchedules();
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
```
|
|
869
|
+
[🔝 Back to top](#table-of-contents)
|
|
1200
870
|
|
|
1201
871
|
## Advanced Usage
|
|
1202
872
|
|
|
1203
873
|
### Activity Retry Configuration
|
|
1204
874
|
|
|
1205
|
-
Configure custom retry policies for different activity types:
|
|
1206
|
-
|
|
1207
875
|
```typescript
|
|
1208
876
|
// workflow.ts
|
|
1209
877
|
const paymentActivities = proxyActivities<typeof PaymentActivity.prototype>({
|
|
@@ -1216,25 +884,14 @@ const paymentActivities = proxyActivities<typeof PaymentActivity.prototype>({
|
|
|
1216
884
|
nonRetryableErrorTypes: ['InvalidPaymentMethod', 'InsufficientFunds'],
|
|
1217
885
|
},
|
|
1218
886
|
});
|
|
1219
|
-
|
|
1220
|
-
const emailActivities = proxyActivities<typeof EmailActivity.prototype>({
|
|
1221
|
-
startToCloseTimeout: '2m',
|
|
1222
|
-
retry: {
|
|
1223
|
-
maximumAttempts: 3,
|
|
1224
|
-
initialInterval: '500ms',
|
|
1225
|
-
},
|
|
1226
|
-
});
|
|
1227
887
|
```
|
|
1228
888
|
|
|
1229
889
|
### Workflow Testing
|
|
1230
890
|
|
|
1231
|
-
Test workflows using Temporal's testing framework:
|
|
1232
|
-
|
|
1233
891
|
```typescript
|
|
1234
892
|
import { TestWorkflowEnvironment } from '@temporalio/testing';
|
|
1235
893
|
import { Worker } from '@temporalio/worker';
|
|
1236
894
|
import { processOrderWorkflow } from './order.workflow';
|
|
1237
|
-
import { OrderActivity } from './order.activity';
|
|
1238
895
|
|
|
1239
896
|
describe('Order Workflow', () => {
|
|
1240
897
|
let testEnv: TestWorkflowEnvironment;
|
|
@@ -1283,102 +940,7 @@ describe('Order Workflow', () => {
|
|
|
1283
940
|
});
|
|
1284
941
|
```
|
|
1285
942
|
|
|
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
|
-
```
|
|
943
|
+
[🔝 Back to top](#table-of-contents)
|
|
1382
944
|
|
|
1383
945
|
## Best Practices
|
|
1384
946
|
|
|
@@ -1453,6 +1015,8 @@ const activities = proxyActivities<typeof DataActivity.prototype>({
|
|
|
1453
1015
|
- Don't test against production Temporal server
|
|
1454
1016
|
- Don't assume workflows are correct without testing
|
|
1455
1017
|
|
|
1018
|
+
[🔝 Back to top](#table-of-contents)
|
|
1019
|
+
|
|
1456
1020
|
## Health Monitoring
|
|
1457
1021
|
|
|
1458
1022
|
The package includes comprehensive health monitoring capabilities for production deployments.
|
|
@@ -1491,7 +1055,7 @@ export class HealthController {
|
|
|
1491
1055
|
@Get('/status')
|
|
1492
1056
|
async getHealthStatus() {
|
|
1493
1057
|
const health = this.temporal.getHealth();
|
|
1494
|
-
|
|
1058
|
+
|
|
1495
1059
|
return {
|
|
1496
1060
|
status: health.overallHealth,
|
|
1497
1061
|
timestamp: new Date(),
|
|
@@ -1513,249 +1077,10 @@ export class HealthController {
|
|
|
1513
1077
|
uptime: health.uptime,
|
|
1514
1078
|
};
|
|
1515
1079
|
}
|
|
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
|
-
}
|
|
1532
|
-
```
|
|
1533
|
-
|
|
1534
|
-
### Health Check Response
|
|
1535
|
-
|
|
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
1080
|
}
|
|
1555
1081
|
```
|
|
1556
|
-
```
|
|
1557
1082
|
|
|
1558
|
-
|
|
1559
|
-
|
|
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
|
|
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
|
-
```
|
|
1083
|
+
[🔝 Back to top](#table-of-contents)
|
|
1759
1084
|
|
|
1760
1085
|
## Troubleshooting
|
|
1761
1086
|
|
|
@@ -1765,10 +1090,6 @@ export class WorkerHealthService {
|
|
|
1765
1090
|
|
|
1766
1091
|
**Problem:** Cannot connect to Temporal server
|
|
1767
1092
|
|
|
1768
|
-
```
|
|
1769
|
-
Error: Failed to connect to localhost:7233
|
|
1770
|
-
```
|
|
1771
|
-
|
|
1772
1093
|
**Solutions:**
|
|
1773
1094
|
```typescript
|
|
1774
1095
|
// Check connection configuration
|
|
@@ -1791,10 +1112,6 @@ TemporalModule.register({
|
|
|
1791
1112
|
|
|
1792
1113
|
**Problem:** Workflow cannot find registered activities
|
|
1793
1114
|
|
|
1794
|
-
```
|
|
1795
|
-
Error: Activity 'myActivity' not found
|
|
1796
|
-
```
|
|
1797
|
-
|
|
1798
1115
|
**Solutions:**
|
|
1799
1116
|
```typescript
|
|
1800
1117
|
// 1. Ensure activity is in activityClasses array
|
|
@@ -1825,10 +1142,6 @@ console.log('Activities discovered:', health.discovery.activitiesDiscovered);
|
|
|
1825
1142
|
|
|
1826
1143
|
**Problem:** Workflow not found or not executing
|
|
1827
1144
|
|
|
1828
|
-
```
|
|
1829
|
-
Error: Workflow 'myWorkflow' not found
|
|
1830
|
-
```
|
|
1831
|
-
|
|
1832
1145
|
**Solutions:**
|
|
1833
1146
|
```typescript
|
|
1834
1147
|
// 1. Ensure workflowsPath is correct
|
|
@@ -1855,10 +1168,6 @@ await temporal.startWorkflow(
|
|
|
1855
1168
|
|
|
1856
1169
|
**Problem:** Activities or workflows timing out
|
|
1857
1170
|
|
|
1858
|
-
```
|
|
1859
|
-
Error: Activity timed out after 10s
|
|
1860
|
-
```
|
|
1861
|
-
|
|
1862
1171
|
**Solutions:**
|
|
1863
1172
|
```typescript
|
|
1864
1173
|
// Configure appropriate timeouts
|
|
@@ -1876,67 +1185,6 @@ await temporal.startWorkflow('myWorkflow', [args], {
|
|
|
1876
1185
|
});
|
|
1877
1186
|
```
|
|
1878
1187
|
|
|
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
1188
|
### Debug Mode
|
|
1941
1189
|
|
|
1942
1190
|
Enable comprehensive debugging:
|
|
@@ -1971,10 +1219,85 @@ If you're still experiencing issues:
|
|
|
1971
1219
|
5. **Check GitHub Issues** - [Search existing issues](https://github.com/harsh-simform/nestjs-temporal-core/issues)
|
|
1972
1220
|
6. **Create an issue** - Provide logs, configuration, and minimal reproduction
|
|
1973
1221
|
|
|
1222
|
+
[🔝 Back to top](#table-of-contents)
|
|
1223
|
+
|
|
1224
|
+
## Migration Guide
|
|
1225
|
+
|
|
1226
|
+
### Migrating to v3.0.12+ (Multiple Workers Support)
|
|
1227
|
+
|
|
1228
|
+
Version 3.0.12 introduces support for multiple workers without breaking existing single-worker configurations.
|
|
1229
|
+
|
|
1230
|
+
#### No Changes Required for Single Worker
|
|
1231
|
+
|
|
1232
|
+
Your existing configuration continues to work:
|
|
1233
|
+
|
|
1234
|
+
```typescript
|
|
1235
|
+
// ✅ This still works exactly as before
|
|
1236
|
+
TemporalModule.register({
|
|
1237
|
+
connection: { address: 'localhost:7233' },
|
|
1238
|
+
taskQueue: 'my-queue',
|
|
1239
|
+
worker: {
|
|
1240
|
+
workflowsPath: require.resolve('./workflows'),
|
|
1241
|
+
activityClasses: [MyActivity],
|
|
1242
|
+
},
|
|
1243
|
+
})
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
#### Migrating to Multiple Workers
|
|
1247
|
+
|
|
1248
|
+
**After (v3.0.12):**
|
|
1249
|
+
```typescript
|
|
1250
|
+
// Option 1: Configure multiple workers in module
|
|
1251
|
+
TemporalModule.register({
|
|
1252
|
+
connection: { address: 'localhost:7233' },
|
|
1253
|
+
workers: [
|
|
1254
|
+
{
|
|
1255
|
+
taskQueue: 'main-queue',
|
|
1256
|
+
workflowsPath: require.resolve('./workflows/main'),
|
|
1257
|
+
activityClasses: [MainActivity],
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
taskQueue: 'schedule-queue',
|
|
1261
|
+
workflowsPath: require.resolve('./workflows/schedule'),
|
|
1262
|
+
activityClasses: [ScheduleActivity],
|
|
1263
|
+
},
|
|
1264
|
+
],
|
|
1265
|
+
})
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
#### New APIs in v3.0.12
|
|
1269
|
+
|
|
1270
|
+
```typescript
|
|
1271
|
+
// Get native connection for custom worker creation
|
|
1272
|
+
const workerManager = temporal.getWorkerManager();
|
|
1273
|
+
const connection: NativeConnection | null = workerManager.getConnection();
|
|
1274
|
+
|
|
1275
|
+
// Get specific worker by task queue
|
|
1276
|
+
const worker: Worker | null = temporal.getWorker('payments-queue');
|
|
1277
|
+
|
|
1278
|
+
// Get all workers information
|
|
1279
|
+
const workersInfo: MultipleWorkersInfo = temporal.getAllWorkers();
|
|
1280
|
+
console.log(`${workersInfo.runningWorkers}/${workersInfo.totalWorkers} workers running`);
|
|
1281
|
+
|
|
1282
|
+
// Control specific workers
|
|
1283
|
+
await temporal.startWorkerByTaskQueue('payments-queue');
|
|
1284
|
+
await temporal.stopWorkerByTaskQueue('notifications-queue');
|
|
1285
|
+
|
|
1286
|
+
// Register new worker dynamically
|
|
1287
|
+
const result = await temporal.registerWorker({
|
|
1288
|
+
taskQueue: 'new-queue',
|
|
1289
|
+
workflowsPath: require.resolve('./workflows/new'),
|
|
1290
|
+
activityClasses: [NewActivity],
|
|
1291
|
+
autoStart: true,
|
|
1292
|
+
});
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
[🔝 Back to top](#table-of-contents)
|
|
1296
|
+
|
|
1974
1297
|
## Requirements
|
|
1975
1298
|
|
|
1976
1299
|
- **Node.js**: >= 16.0.0
|
|
1977
|
-
- **NestJS**: >= 9.0.0
|
|
1300
|
+
- **NestJS**: >= 9.0.0
|
|
1978
1301
|
- **Temporal Server**: >= 1.20.0
|
|
1979
1302
|
|
|
1980
1303
|
## Contributing
|
|
@@ -1989,8 +1312,6 @@ We welcome contributions! To contribute:
|
|
|
1989
1312
|
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
1990
1313
|
7. Open a Pull Request
|
|
1991
1314
|
|
|
1992
|
-
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
|
|
1993
|
-
|
|
1994
1315
|
### Development Setup
|
|
1995
1316
|
|
|
1996
1317
|
```bash
|
|
@@ -2014,6 +1335,8 @@ npm run build
|
|
|
2014
1335
|
npm run docs:generate
|
|
2015
1336
|
```
|
|
2016
1337
|
|
|
1338
|
+
[🔝 Back to top](#table-of-contents)
|
|
1339
|
+
|
|
2017
1340
|
## License
|
|
2018
1341
|
|
|
2019
1342
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -2035,3 +1358,10 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
2035
1358
|
|
|
2036
1359
|
---
|
|
2037
1360
|
|
|
1361
|
+
<div align="center">
|
|
1362
|
+
|
|
1363
|
+
**[⭐ Star us on GitHub](https://github.com/harsh-simform/nestjs-temporal-core)** if you find this project helpful!
|
|
1364
|
+
|
|
1365
|
+
Made with ❤️ by the Harsh Simform
|
|
1366
|
+
|
|
1367
|
+
</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.6",
|
|
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",
|