@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
- console.log(`Received task ${taskType} (ID: ${task.taskId})`);
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
- // If polling fails (e.g. 204 No Content or connection error), just log debug/warn and wait
75
- // console.debug(`Polling error for ${taskType}: ${error}`);
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geniehr/utilities",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "",
5
5
  "homepage": "https://github.com/Genie-HR/ghr-utilities#readme",
6
6
  "bugs": {