codehooks-js 1.3.5 → 1.3.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehooks-js",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "type": "module",
5
5
  "description": "Codehooks.io official library - provides express.JS like syntax",
6
6
  "main": "index.js",
package/types/index.d.ts CHANGED
@@ -67,6 +67,7 @@ declare class Codehooks {
67
67
  configure(config: {
68
68
  collectionName: string;
69
69
  queuePrefix: string;
70
+ timeout: number;
70
71
  }): void;
71
72
  getDefinition(stepsName: string, stepName: string): Function;
72
73
  validateStepDefinition(step: Function): void;
@@ -80,6 +81,9 @@ declare class Codehooks {
80
81
  continue(stepsName: string, instanceId: string): Promise<{
81
82
  qId: string;
82
83
  }>;
84
+ continueAllTimedOut(): Promise<Array<{
85
+ qId: string;
86
+ }>>;
83
87
  getStepsStatus(id: string): Promise<any>;
84
88
  getInstances(filter: any): Promise<any[]>;
85
89
  cancelSteps(id: string): Promise<any>;
@@ -11,6 +11,8 @@ declare class StepsEngine extends EventEmitter<[never]> {
11
11
  static instance: any;
12
12
  static collectionName: string;
13
13
  static queuePrefix: string;
14
+ static timeout: number;
15
+ static maxStepCount: number;
14
16
  /**
15
17
  * Set the collection name for storing steps data
16
18
  * @param {string} name - Collection name
@@ -23,6 +25,18 @@ declare class StepsEngine extends EventEmitter<[never]> {
23
25
  * @throws {Error} If prefix is not a non-empty string
24
26
  */
25
27
  static setQueuePrefix(prefix: string): void;
28
+ /**
29
+ * Set the timeout for steps jobs
30
+ * @param {number} timeout - Timeout in milliseconds
31
+ * @throws {Error} If timeout is not a positive number
32
+ */
33
+ static setTimeout(timeout: number): void;
34
+ /**
35
+ * Set the maximum step count for a steps workflow
36
+ * @param {number} maxStepCount - Maximum step count
37
+ * @throws {Error} If maxStepCount is not a positive number
38
+ */
39
+ static setMaxStepCount(maxStepCount: number): void;
26
40
  /**
27
41
  * Get the singleton instance of StepsEngine
28
42
  * @returns {StepsEngine} The singleton instance
@@ -35,10 +49,12 @@ declare class StepsEngine extends EventEmitter<[never]> {
35
49
  * @param {Object} config - Configuration object
36
50
  * @param {string} config.collectionName - Collection name
37
51
  * @param {string} config.queuePrefix - Queue prefix
52
+ * @param {number} config.timeout - Timeout in milliseconds
38
53
  */
39
54
  configure(config: {
40
55
  collectionName: string;
41
56
  queuePrefix: string;
57
+ timeout: number;
42
58
  }): void;
43
59
  /**
44
60
  * Get the step definition for a specific step
@@ -111,6 +127,13 @@ declare class StepsEngine extends EventEmitter<[never]> {
111
127
  continue(stepsName: string, instanceId: string): Promise<{
112
128
  qId: string;
113
129
  }>;
130
+ /**
131
+ * Continue all timed out steps instances
132
+ * @returns {Promise<Array<{qId: string}>>} Array of results containing queue IDs for continued workflows
133
+ */
134
+ continueAllTimedOut(): Promise<Array<{
135
+ qId: string;
136
+ }>>;
114
137
  /**
115
138
  * Get the status of a steps instance
116
139
  * @param {string} id - ID of the steps instance
@@ -5,6 +5,7 @@ export namespace StepsConfig {
5
5
  export const configure: (config: {
6
6
  collectionName: string;
7
7
  queuePrefix: string;
8
+ timeout: number;
8
9
  }) => void;
9
10
  export { Steps };
10
11
  export default StepsEngine;
@@ -22,6 +22,8 @@ class StepsEngine extends EventEmitter {
22
22
  // Configuration
23
23
  static collectionName = 'workflowdata';
24
24
  static queuePrefix = 'workflowqueue';
25
+ static timeout = 30000;
26
+ static maxStepCount = 3;
25
27
 
26
28
  /**
27
29
  * Set the collection name for storing steps data
@@ -47,11 +49,36 @@ class StepsEngine extends EventEmitter {
47
49
  StepsEngine.queuePrefix = prefix.trim();
48
50
  }
49
51
 
52
+ /**
53
+ * Set the timeout for steps jobs
54
+ * @param {number} timeout - Timeout in milliseconds
55
+ * @throws {Error} If timeout is not a positive number
56
+ */
57
+ static setTimeout(timeout) {
58
+ if (typeof timeout !== 'number' || timeout <= 0) {
59
+ throw new Error('Timeout must be a positive number');
60
+ }
61
+ StepsEngine.timeout = timeout;
62
+ }
63
+
64
+ /**
65
+ * Set the maximum step count for a steps workflow
66
+ * @param {number} maxStepCount - Maximum step count
67
+ * @throws {Error} If maxStepCount is not a positive number
68
+ */
69
+ static setMaxStepCount(maxStepCount) {
70
+ if (typeof maxStepCount !== 'number' || maxStepCount <= 0) {
71
+ throw new Error('Maximum step count must be a positive number');
72
+ }
73
+ StepsEngine.maxStepCount = maxStepCount;
74
+ }
75
+
50
76
  /**
51
77
  * Configure the steps engine
52
78
  * @param {Object} config - Configuration object
53
79
  * @param {string} config.collectionName - Collection name
54
80
  * @param {string} config.queuePrefix - Queue prefix
81
+ * @param {number} config.timeout - Timeout in milliseconds
55
82
  */
56
83
  configure(config) {
57
84
  if (config.collectionName) {
@@ -60,6 +87,12 @@ class StepsEngine extends EventEmitter {
60
87
  if (config.queuePrefix) {
61
88
  StepsEngine.setQueuePrefix(config.queuePrefix);
62
89
  }
90
+ if (config.timeout) {
91
+ StepsEngine.setTimeout(config.timeout);
92
+ }
93
+ if (config.maxStepCount) {
94
+ StepsEngine.setMaxStepCount(config.maxStepCount);
95
+ }
63
96
  }
64
97
 
65
98
  /**
@@ -118,39 +151,37 @@ class StepsEngine extends EventEmitter {
118
151
  * @throws {Error} If step execution fails
119
152
  */
120
153
  async handleNextStep(stepsName, nextStep, newState, instanceId, options) {
121
- try {
122
- // Handle array of next steps
123
- if (Array.isArray(nextStep)) {
124
- const results = [];
125
- for (const step of nextStep) {
126
- const qid = await this.handleNextStep(stepsName, step, newState, instanceId, options);
127
- results.push(qid);
128
- }
129
- return results;
130
- }
154
+
155
+ // open the connection to the database
156
+ const connection = await Datastore.open();
131
157
 
132
- // Handle single next step
133
- StepsEngine.getInstance().emit('stepStarted', { workflowName: stepsName, step: nextStep, state: newState, instanceId });
134
- const connection = await Datastore.open();
135
- // remove the _id from the newState
136
- delete newState._id;
137
- // Update the existing steps state in the database
138
- newState = await connection.updateOne(StepsEngine.collectionName,
139
- { _id: instanceId },
140
- { $set: { ...newState, nextStep: nextStep, updatedAt: new Date().toISOString() } });
141
-
142
- StepsEngine.getInstance().emit('stateUpdated', { workflowName: stepsName, state: newState, instanceId });
143
-
144
-
145
-
146
-
147
- try {
148
- // Get the next step function
149
- const func = StepsEngine.getInstance().getDefinition(stepsName, nextStep);
158
+ // Handle single next step
159
+ StepsEngine.getInstance().emit('stepStarted', { workflowName: stepsName, step: nextStep, state: newState, instanceId });
160
+
161
+ // remove the _id from the newState
162
+ delete newState._id;
163
+ // increment the step count
164
+ newState.stepCount[nextStep] = (newState.stepCount[nextStep] || 0) + 1;
165
+ // Update the existing steps state in the database
166
+ newState = await connection.updateOne(StepsEngine.collectionName,
167
+ { _id: instanceId },
168
+ { $set: { ...newState, nextStep: nextStep, updatedAt: new Date().toISOString(), stepCount: newState.stepCount } });
169
+
170
+ StepsEngine.getInstance().emit('stateUpdated', { workflowName: stepsName, state: newState, instanceId });
171
+
172
+ try {
173
+ // Get the next step function
174
+ const func = StepsEngine.getInstance().getDefinition(stepsName, nextStep);
150
175
 
151
- // Wrap the callback in a Promise to ensure proper async handling
152
- await new Promise((resolve, reject) => {
153
- func({...newState, instanceId: newState._id}, async (nextStep, userState, options) => {
176
+ // Wrap the callback in a Promise to ensure proper async handling
177
+ await new Promise(async (resolve, reject) => {
178
+ // check if the step count is greater than the max step count
179
+ if (newState.stepCount[nextStep] > StepsEngine.maxStepCount) {
180
+ reject(new Error(`Step ${nextStep} has been executed ${newState.stepCount[nextStep]} times, which is greater than the maximum step count of ${StepsEngine.maxStepCount}`));
181
+ return;
182
+ }
183
+ try {
184
+ await func({...newState, instanceId: newState._id}, async (nextStep, userState, options) => {
154
185
  try {
155
186
  // Protect system-level properties
156
187
  const protectedState = {
@@ -159,7 +190,10 @@ class StepsEngine extends EventEmitter {
159
190
  createdAt: newState.createdAt,
160
191
  updatedAt: newState.updatedAt,
161
192
  instanceId: newState.instanceId,
162
- workflowName: newState.workflowName
193
+ workflowName: newState.workflowName,
194
+ stepCount: newState.stepCount,
195
+ parallelSteps: newState.parallelSteps,
196
+ previousStep: newState.nextStep
163
197
  };
164
198
 
165
199
  // Merge states with userState taking precedence, but protecting system fields
@@ -168,6 +202,27 @@ class StepsEngine extends EventEmitter {
168
202
  ...protectedState
169
203
  };
170
204
 
205
+ // update the parallel steps metadata
206
+ if (mergedState.parallelSteps && mergedState.parallelSteps[mergedState.nextStep]) {
207
+ // get a fresh copy of the parallel steps
208
+ const fresh = await connection.findOne(StepsEngine.collectionName, { _id: instanceId });
209
+ fresh.parallelSteps[mergedState.nextStep].done = true;
210
+ fresh.parallelSteps[mergedState.nextStep].nextStep = nextStep;
211
+ delete fresh._id;
212
+ await connection.updateOne(StepsEngine.collectionName,
213
+ { _id: instanceId },
214
+ { $set: { ...fresh, parallelSteps: fresh.parallelSteps } });
215
+ // Check if all parallel steps are done
216
+ const allStepsDone = Object.values(fresh.parallelSteps).every(step => step.done);
217
+ if (!allStepsDone) {
218
+ console.debug('Waiting for other parallel steps to complete');
219
+ resolve();
220
+ return;
221
+ } else {
222
+ console.debug('All parallel steps are done');
223
+ }
224
+ }
225
+
171
226
  // If there is no next step, the workflow is completed
172
227
  if (nextStep === null) {
173
228
  delete mergedState._id;
@@ -182,16 +237,38 @@ class StepsEngine extends EventEmitter {
182
237
  return;
183
238
  }
184
239
 
185
- // Enqueue the next step
186
- console.debug('enqueuing step', nextStep, instanceId);
187
- await connection.enqueue(`${StepsEngine.queuePrefix}_${stepsName}_${nextStep}`, {
240
+ // Enqueue the next step, single or parallel
241
+ if (Array.isArray(nextStep)) {
242
+ const now = new Date().toISOString();
243
+ const metadata = nextStep.reduce((acc, step) => {
244
+ acc[step] = { done: false, startTime: now };
245
+ return acc;
246
+ }, {});
247
+ const metadataDoc = await connection.updateOne(StepsEngine.collectionName,
248
+ { _id: instanceId },
249
+ { $set: { parallelSteps: metadata } });
250
+ //console.log('metadataDoc', metadataDoc);
251
+ // enqueue all steps in parallel
252
+ for (const step of nextStep) {
253
+ console.debug('enqueue step', step, instanceId);
254
+ await connection.enqueue(`${StepsEngine.queuePrefix}_${stepsName}_${step}`, {
255
+ stepsName: stepsName,
256
+ goto: step,
257
+ state: mergedState,
258
+ options: options,
259
+ instanceId: instanceId
260
+ });
261
+ }
262
+ } else {
263
+ console.debug('enqueue step', nextStep, instanceId);
264
+ await connection.enqueue(`${StepsEngine.queuePrefix}_${stepsName}_${nextStep}`, {
188
265
  stepsName: stepsName,
189
266
  goto: nextStep,
190
267
  state: mergedState,
191
268
  options: options,
192
269
  instanceId: instanceId
193
270
  });
194
-
271
+ }
195
272
  StepsEngine.getInstance().emit('stepEnqueued', { workflowName: stepsName, step: nextStep, state: newState, instanceId });
196
273
  resolve();
197
274
  } catch (error) {
@@ -199,20 +276,15 @@ class StepsEngine extends EventEmitter {
199
276
  reject(error);
200
277
  }
201
278
  });
202
- });
203
- } catch (error) {
204
- console.error('Error in step function: '+nextStep, error.message);
205
- StepsEngine.getInstance().emit('error', { workflowName: stepsName, step: nextStep, state: newState, instanceId, error: error.message });
206
- const connection = await Datastore.open();
207
- await connection.updateOne(StepsEngine.collectionName,
208
- { _id: instanceId },
209
- { $set: { lastError: error.message, updatedAt: new Date().toISOString() } });
210
- throw error;
211
- }
212
- }
213
- catch (error) {
279
+ } catch (error) {
280
+ console.error('Error executing step function:', error);
281
+ reject(error);
282
+ }
283
+ });
284
+ } catch (error) {
285
+ console.error('error in handleNextStep outer', error.message);
214
286
  throw error;
215
- }
287
+ }
216
288
  }
217
289
 
218
290
  /**
@@ -233,13 +305,18 @@ class StepsEngine extends EventEmitter {
233
305
  if (stepName !== undefined) {
234
306
  //console.log('registering queue for step', `${StepsEngine.queuePrefix}_${name}_${stepName}`);
235
307
  app.worker(`${StepsEngine.queuePrefix}_${name}_${stepName}`, async function(req, res) {
236
- try {
237
-
308
+ try {
238
309
  const { stepsName, goto, state, instanceId, options } = req.body.payload;
239
- console.debug('worker job', stepName, instanceId);
310
+ console.debug('dequeue step', stepName, instanceId);
240
311
  const qid = await StepsEngine.getInstance().handleNextStep(stepsName, goto, state, instanceId, options);
241
312
  } catch (error) {
242
- console.error('Error in step function: ' + stepName, error.message);
313
+ const { stepsName, goto, state, instanceId, options } = req.body.payload;
314
+ const connection = await Datastore.open();
315
+ await connection.updateOne(StepsEngine.collectionName,
316
+ { _id: instanceId },
317
+ { $set: { lastError: error, updatedAt: new Date().toISOString() } });
318
+ console.error('Error in function: ' + stepName, error);
319
+ StepsEngine.getInstance().emit('error', error);
243
320
  } finally {
244
321
  res.end();
245
322
  }
@@ -277,7 +354,7 @@ class StepsEngine extends EventEmitter {
277
354
  const connection = await Datastore.open();
278
355
  // Create a new steps state in the database
279
356
  const newState = await connection.insertOne(StepsEngine.collectionName,
280
- { ...initialState, nextStep: firstStepName, createdAt: new Date().toISOString(), workflowName: name });
357
+ { ...initialState, nextStep: firstStepName, createdAt: new Date().toISOString(), workflowName: name, stepCount: {} });
281
358
  const { _id: ID } = await connection.enqueue(`${StepsEngine.queuePrefix}_${name}_${firstStepName}`, {
282
359
  stepsName: name,
283
360
  goto: firstStepName,
@@ -339,7 +416,11 @@ class StepsEngine extends EventEmitter {
339
416
  if (!state) {
340
417
  throw new Error(`No steps found with instanceId: ${instanceId}`);
341
418
  }
342
-
419
+ // update all step counts to 0
420
+ for (const step in state.stepCount) {
421
+ state.stepCount[step] = 0;
422
+ }
423
+ await connection.updateOne(StepsEngine.collectionName, { _id: instanceId }, { $set: { stepCount: state.stepCount } });
343
424
  console.debug('continue state', state);
344
425
  StepsEngine.getInstance().emit('workflowContinued', { stepsName, step: state.nextStep, instanceId });
345
426
 
@@ -352,10 +433,33 @@ class StepsEngine extends EventEmitter {
352
433
  instanceId: instanceId
353
434
  });
354
435
 
355
- resolve({ qId: ID });
436
+ resolve({ instanceId });
356
437
  });
357
438
  }
358
439
 
440
+ /**
441
+ * Continue all timed out steps instances
442
+ * @returns {Promise<Array<{qId: string}>>} Array of results containing queue IDs for continued workflows
443
+ */
444
+ async continueAllTimedOut() {
445
+ const db = await Datastore.open();
446
+ const timedOutWorkflows = await db.collection(StepsEngine.collectionName).find({nextStep: {$ne: null}}).toArray();
447
+ const now = new Date();
448
+ const results = [];
449
+ for (const workflow of timedOutWorkflows) {
450
+ const createdAt = new Date(workflow.createdAt);
451
+ const diffMillis = now.getTime() - createdAt.getTime();
452
+ if (diffMillis > StepsEngine.timeout) {
453
+ const diffMinutes = diffMillis / (1000 * 60);
454
+ console.log('Timed out:', workflow._id, workflow.nextStep, `(${diffMinutes.toFixed(1)} minutes old)`);
455
+ const result = await this.continue(workflow.workflowName, workflow._id);
456
+ console.log('Continued:', result._id);
457
+ results.push(result);
458
+ }
459
+ }
460
+ return results;
461
+ }
462
+
359
463
  /**
360
464
  * Get the status of a steps instance
361
465
  * @param {string} id - ID of the steps instance