codehooks-js 1.3.5 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehooks-js",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
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
  /**
@@ -134,15 +167,14 @@ class StepsEngine extends EventEmitter {
134
167
  const connection = await Datastore.open();
135
168
  // remove the _id from the newState
136
169
  delete newState._id;
170
+ // increment the step count
171
+ newState.stepCount[nextStep] = (newState.stepCount[nextStep] || 0) + 1;
137
172
  // Update the existing steps state in the database
138
173
  newState = await connection.updateOne(StepsEngine.collectionName,
139
174
  { _id: instanceId },
140
- { $set: { ...newState, nextStep: nextStep, updatedAt: new Date().toISOString() } });
141
-
142
- StepsEngine.getInstance().emit('stateUpdated', { workflowName: stepsName, state: newState, instanceId });
143
-
144
-
175
+ { $set: { ...newState, nextStep: nextStep, updatedAt: new Date().toISOString(), stepCount: newState.stepCount } });
145
176
 
177
+ StepsEngine.getInstance().emit('stateUpdated', { workflowName: stepsName, state: newState, instanceId });
146
178
 
147
179
  try {
148
180
  // Get the next step function
@@ -150,6 +182,11 @@ class StepsEngine extends EventEmitter {
150
182
 
151
183
  // Wrap the callback in a Promise to ensure proper async handling
152
184
  await new Promise((resolve, reject) => {
185
+ // check if the step count is greater than the max step count
186
+ if (newState.stepCount[nextStep] > StepsEngine.maxStepCount) {
187
+ reject(new Error(`Step ${nextStep} has been executed ${newState.stepCount[nextStep]} times, which is greater than the maximum step count of ${StepsEngine.maxStepCount}`));
188
+ return;
189
+ }
153
190
  func({...newState, instanceId: newState._id}, async (nextStep, userState, options) => {
154
191
  try {
155
192
  // Protect system-level properties
@@ -159,7 +196,8 @@ class StepsEngine extends EventEmitter {
159
196
  createdAt: newState.createdAt,
160
197
  updatedAt: newState.updatedAt,
161
198
  instanceId: newState.instanceId,
162
- workflowName: newState.workflowName
199
+ workflowName: newState.workflowName,
200
+ stepCount: newState.stepCount
163
201
  };
164
202
 
165
203
  // Merge states with userState taking precedence, but protecting system fields
@@ -201,12 +239,6 @@ class StepsEngine extends EventEmitter {
201
239
  });
202
240
  });
203
241
  } 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
242
  throw error;
211
243
  }
212
244
  }
@@ -233,13 +265,18 @@ class StepsEngine extends EventEmitter {
233
265
  if (stepName !== undefined) {
234
266
  //console.log('registering queue for step', `${StepsEngine.queuePrefix}_${name}_${stepName}`);
235
267
  app.worker(`${StepsEngine.queuePrefix}_${name}_${stepName}`, async function(req, res) {
236
- try {
237
-
268
+ try {
238
269
  const { stepsName, goto, state, instanceId, options } = req.body.payload;
239
270
  console.debug('worker job', stepName, instanceId);
240
271
  const qid = await StepsEngine.getInstance().handleNextStep(stepsName, goto, state, instanceId, options);
241
272
  } catch (error) {
242
- console.error('Error in step function: ' + stepName, error.message);
273
+ const { stepsName, goto, state, instanceId, options } = req.body.payload;
274
+ const connection = await Datastore.open();
275
+ await connection.updateOne(StepsEngine.collectionName,
276
+ { _id: instanceId },
277
+ { $set: { lastError: error, updatedAt: new Date().toISOString() } });
278
+ console.error('Error in function: ' + stepName, error);
279
+ StepsEngine.getInstance().emit('error', error);
243
280
  } finally {
244
281
  res.end();
245
282
  }
@@ -277,7 +314,7 @@ class StepsEngine extends EventEmitter {
277
314
  const connection = await Datastore.open();
278
315
  // Create a new steps state in the database
279
316
  const newState = await connection.insertOne(StepsEngine.collectionName,
280
- { ...initialState, nextStep: firstStepName, createdAt: new Date().toISOString(), workflowName: name });
317
+ { ...initialState, nextStep: firstStepName, createdAt: new Date().toISOString(), workflowName: name, stepCount: {} });
281
318
  const { _id: ID } = await connection.enqueue(`${StepsEngine.queuePrefix}_${name}_${firstStepName}`, {
282
319
  stepsName: name,
283
320
  goto: firstStepName,
@@ -339,7 +376,11 @@ class StepsEngine extends EventEmitter {
339
376
  if (!state) {
340
377
  throw new Error(`No steps found with instanceId: ${instanceId}`);
341
378
  }
342
-
379
+ // update all step counts to 0
380
+ for (const step in state.stepCount) {
381
+ state.stepCount[step] = 0;
382
+ }
383
+ await connection.updateOne(StepsEngine.collectionName, { _id: instanceId }, { $set: { stepCount: state.stepCount } });
343
384
  console.debug('continue state', state);
344
385
  StepsEngine.getInstance().emit('workflowContinued', { stepsName, step: state.nextStep, instanceId });
345
386
 
@@ -356,6 +397,29 @@ class StepsEngine extends EventEmitter {
356
397
  });
357
398
  }
358
399
 
400
+ /**
401
+ * Continue all timed out steps instances
402
+ * @returns {Promise<Array<{qId: string}>>} Array of results containing queue IDs for continued workflows
403
+ */
404
+ async continueAllTimedOut() {
405
+ const db = await Datastore.open();
406
+ const timedOutWorkflows = await db.collection(StepsEngine.collectionName).find({nextStep: {$ne: null}}).toArray();
407
+ const now = new Date();
408
+ const results = [];
409
+ for (const workflow of timedOutWorkflows) {
410
+ const createdAt = new Date(workflow.createdAt);
411
+ const diffMillis = now.getTime() - createdAt.getTime();
412
+ if (diffMillis > StepsEngine.timeout) {
413
+ const diffMinutes = diffMillis / (1000 * 60);
414
+ console.log('Timed out:', workflow._id, workflow.nextStep, `(${diffMinutes.toFixed(1)} minutes old)`);
415
+ const result = await this.continue(workflow.workflowName, workflow._id);
416
+ console.log('Continued:', result._id);
417
+ results.push(result);
418
+ }
419
+ }
420
+ return results;
421
+ }
422
+
359
423
  /**
360
424
  * Get the status of a steps instance
361
425
  * @param {string} id - ID of the steps instance