@geniehr/utilities 1.0.16 → 1.0.17
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.
|
@@ -3,9 +3,11 @@ export declare class ConductorClient {
|
|
|
3
3
|
private workers;
|
|
4
4
|
constructor(serviceName?: string);
|
|
5
5
|
startWorkflow(name: string, input: any, correlationId?: string, version?: number): Promise<any>;
|
|
6
|
+
executeWorkflow(name: string, input: any, correlationId?: string, version?: number, timeout?: number): Promise<any>;
|
|
6
7
|
registerWorker(taskType: string, execute: (input: any) => Promise<any>, pollInterval?: number, domain?: string): Promise<void>;
|
|
7
8
|
private poll;
|
|
8
9
|
updateTask(taskId: string, workflowInstanceId: string, status: string, outputData: any, reasonForIncompletion?: string): Promise<void>;
|
|
9
10
|
getTask(workflowInstanceId: string, taskReferenceName: string): Promise<any>;
|
|
10
11
|
getWorkflow(workflowInstanceId: string, includeTasks?: boolean): Promise<any>;
|
|
12
|
+
getWorkflowStatus(workflowInstanceId: string): Promise<string>;
|
|
11
13
|
}
|
|
@@ -31,6 +31,41 @@ export class ConductorClient {
|
|
|
31
31
|
throw error;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
async executeWorkflow(name, input, correlationId, version, timeout = 30000) {
|
|
35
|
+
try {
|
|
36
|
+
// 1. Start the workflow
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
const workflowId = await this.startWorkflow(name, input, correlationId, version);
|
|
39
|
+
console.log(`[${correlationId}] Started workflow ${name} (ID: ${workflowId})`);
|
|
40
|
+
// 2. Poll for completion with exponential backoff
|
|
41
|
+
let pollCount = 0;
|
|
42
|
+
let pollInterval = 50; // Start at 50ms
|
|
43
|
+
const maxInterval = 1000; // Cap at 1 second
|
|
44
|
+
while (Date.now() - startTime < timeout) {
|
|
45
|
+
pollCount++;
|
|
46
|
+
const elapsed = Date.now() - startTime;
|
|
47
|
+
// Use lightweight status check
|
|
48
|
+
const status = await this.getWorkflowStatus(workflowId);
|
|
49
|
+
if (status === 'COMPLETED') {
|
|
50
|
+
console.log(`[${correlationId}] Workflow completed in ${elapsed}ms after ${pollCount} polls`);
|
|
51
|
+
// Fetch full workflow only on completion
|
|
52
|
+
return await this.getWorkflow(workflowId, true);
|
|
53
|
+
}
|
|
54
|
+
if (['FAILED', 'TERMINATED', 'PAUSED'].includes(status)) {
|
|
55
|
+
const workflow = await this.getWorkflow(workflowId, true);
|
|
56
|
+
throw new Error(`Workflow execution failed with status: ${workflow.status}. Reason: ${workflow.reasonForIncompletion}`);
|
|
57
|
+
}
|
|
58
|
+
// Exponential backoff
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
60
|
+
pollInterval = Math.min(pollInterval * 1.5, maxInterval);
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Workflow execution timed out after ${timeout}ms`);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error(`Error executing synchronous workflow ${name}:`, error.message);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
34
69
|
async registerWorker(taskType, execute, pollInterval = 1000, domain) {
|
|
35
70
|
if (this.workers[taskType]) {
|
|
36
71
|
console.warn(`Worker for ${taskType} already registered.`);
|
|
@@ -43,18 +78,21 @@ export class ConductorClient {
|
|
|
43
78
|
async poll(taskType, execute, interval, domain) {
|
|
44
79
|
while (this.workers[taskType]) {
|
|
45
80
|
try {
|
|
46
|
-
// Poll for a task
|
|
47
|
-
// GET /tasks/poll/{taskType}?workerid=...&domain=...
|
|
81
|
+
// Poll for a task with long polling enabled
|
|
48
82
|
const workerId = `worker-${process.pid}`;
|
|
83
|
+
const pollStart = Date.now();
|
|
49
84
|
const response = await this.client.get(`/tasks/poll/${taskType}`, {
|
|
50
85
|
params: {
|
|
51
86
|
workerid: workerId,
|
|
52
|
-
domain
|
|
87
|
+
domain,
|
|
88
|
+
count: 1,
|
|
89
|
+
timeout: 5000 // Enable long polling (5s)
|
|
53
90
|
}
|
|
54
91
|
});
|
|
55
92
|
const task = response.data;
|
|
56
93
|
if (task && task.taskId) {
|
|
57
|
-
|
|
94
|
+
const pickupLatency = Date.now() - pollStart;
|
|
95
|
+
console.log(`Received task ${taskType} (ID: ${task.taskId}) after ${pickupLatency}ms`);
|
|
58
96
|
let status = 'COMPLETED';
|
|
59
97
|
let outputData = {};
|
|
60
98
|
let reasonForIncompletion = '';
|
|
@@ -69,13 +107,12 @@ export class ConductorClient {
|
|
|
69
107
|
// Update task status
|
|
70
108
|
await this.updateTask(task.taskId, task.workflowInstanceId, status, outputData, reasonForIncompletion);
|
|
71
109
|
}
|
|
110
|
+
// No sleep here - long-polling handles the wait
|
|
72
111
|
}
|
|
73
112
|
catch (error) {
|
|
74
|
-
//
|
|
75
|
-
|
|
113
|
+
// On error, wait briefly before retry
|
|
114
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
76
115
|
}
|
|
77
|
-
// Wait before next poll
|
|
78
|
-
await new Promise(resolve => setTimeout(resolve, interval));
|
|
79
116
|
}
|
|
80
117
|
}
|
|
81
118
|
async updateTask(taskId, workflowInstanceId, status, outputData, reasonForIncompletion) {
|
|
@@ -117,4 +154,20 @@ export class ConductorClient {
|
|
|
117
154
|
throw error;
|
|
118
155
|
}
|
|
119
156
|
}
|
|
157
|
+
async getWorkflowStatus(workflowInstanceId) {
|
|
158
|
+
try {
|
|
159
|
+
// Try status-only endpoint first (may not exist in all Conductor versions)
|
|
160
|
+
const response = await this.client.get(`/workflow/${workflowInstanceId}/status`);
|
|
161
|
+
return response.data.status;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// Fallback to full workflow fetch with minimal data if status endpoint doesn't exist
|
|
165
|
+
if (error.response?.status === 404 || error.response?.status === 405) {
|
|
166
|
+
const workflow = await this.getWorkflow(workflowInstanceId, false);
|
|
167
|
+
return workflow.status;
|
|
168
|
+
}
|
|
169
|
+
console.error(`Error getting workflow status:`, error.message);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
120
173
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { validateRequest } from './schema/validation.js';
|
|
2
2
|
export { MQClient } from './mq/client.js';
|
|
3
3
|
export { getContext, setExtras, setContext } from './shared/context/index.js';
|
|
4
|
+
export { GHRContext } from './types/GHRContext.js';
|
|
4
5
|
export { LoggerService as Logger } from './logger/LoggerService.js';
|
|
5
6
|
export { HttpStatusCode } from './shared/enums/HttpStatusCodes.js';
|
|
6
7
|
export { InvalidRequestException, InvalidEnvironmentException, AppException } from './shared/exceptions/index.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MQClient } from '../mq/client.js';
|
|
2
|
+
import type { ConductorClient } from '../conductor/ConductorClient.js';
|
|
2
3
|
import Redis from 'ioredis';
|
|
3
4
|
export type GHRContext<TPrismaClient = any> = {
|
|
4
5
|
service?: string;
|
|
@@ -7,5 +8,6 @@ export type GHRContext<TPrismaClient = any> = {
|
|
|
7
8
|
prisma?: TPrismaClient;
|
|
8
9
|
redis?: Redis;
|
|
9
10
|
mongo?: any;
|
|
11
|
+
conductor?: ConductorClient;
|
|
10
12
|
extras?: Record<string, any>;
|
|
11
13
|
};
|