codehooks-js 1.3.4 → 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.4",
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,75 +167,78 @@ 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 });
175
+ { $set: { ...newState, nextStep: nextStep, updatedAt: new Date().toISOString(), stepCount: newState.stepCount } });
143
176
 
177
+ StepsEngine.getInstance().emit('stateUpdated', { workflowName: stepsName, state: newState, instanceId });
144
178
 
145
-
146
- // Get the next step function
147
- const func = StepsEngine.getInstance().getDefinition(stepsName, nextStep);
148
-
149
179
  try {
150
- // Call the next step function with the step context
151
- func.call(this, {...newState, instanceId: newState._id}, async function (nextStep, userState, options) {
152
- try {
153
-
154
- // Protect system-level properties
155
- const protectedState = {
156
- _id: newState._id,
157
- nextStep: newState.nextStep,
158
- createdAt: newState.createdAt,
159
- updatedAt: newState.updatedAt,
160
- instanceId: newState.instanceId,
161
- workflowName: newState.workflowName
162
- };
163
-
164
- // Merge states with userState taking precedence, but protecting system fields
165
- const mergedState = {
166
- //...newState,
167
- ...userState,
168
- ...protectedState
169
- };
170
-
171
- // If there is no next step, the workflow is completed
172
- if (nextStep === null) {
173
- delete mergedState._id;
174
- const completionTime = (new Date(mergedState.updatedAt) - new Date(mergedState.createdAt)) / 1000;
175
- console.log(`Workflow ${stepsName} ${instanceId} is completed in time: ${completionTime}s 🎉`);
176
- const db = await Datastore.open();
177
- await db.updateOne(StepsEngine.collectionName,
178
- { _id: instanceId },
179
- { $set: { ...mergedState, nextStep: null, updatedAt: new Date().toISOString() } });
180
- StepsEngine.getInstance().emit('completed', { message: 'Steps workflow completed', ...userState });
181
- return;
182
- }
180
+ // Get the next step function
181
+ const func = StepsEngine.getInstance().getDefinition(stepsName, nextStep);
183
182
 
184
- // Enqueue the next step
185
- //console.log('enqueuing step', `${StepsEngine.queuePrefix}_${stepsName}_${nextStep}`);
186
- connection.enqueue(`${StepsEngine.queuePrefix}_${stepsName}_${nextStep}`, {
187
- stepsName: stepsName,
188
- goto: nextStep,
189
- state: mergedState,
190
- options: options,
191
- instanceId: instanceId
192
- });
193
-
194
- StepsEngine.getInstance().emit('stepEnqueued', { workflowName: stepsName, step: nextStep, state: newState, instanceId });
195
- } catch (error) {
196
- console.log('error', error.message);
197
- throw error;
183
+ // Wrap the callback in a Promise to ensure proper async handling
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;
198
189
  }
190
+ func({...newState, instanceId: newState._id}, async (nextStep, userState, options) => {
191
+ try {
192
+ // Protect system-level properties
193
+ const protectedState = {
194
+ _id: newState._id,
195
+ nextStep: newState.nextStep,
196
+ createdAt: newState.createdAt,
197
+ updatedAt: newState.updatedAt,
198
+ instanceId: newState.instanceId,
199
+ workflowName: newState.workflowName,
200
+ stepCount: newState.stepCount
201
+ };
202
+
203
+ // Merge states with userState taking precedence, but protecting system fields
204
+ const mergedState = {
205
+ ...userState,
206
+ ...protectedState
207
+ };
208
+
209
+ // If there is no next step, the workflow is completed
210
+ if (nextStep === null) {
211
+ delete mergedState._id;
212
+ const completionTime = (new Date(mergedState.updatedAt) - new Date(mergedState.createdAt)) / 1000;
213
+
214
+ const finalresult = await connection.updateOne(StepsEngine.collectionName,
215
+ { _id: instanceId },
216
+ { $set: { ...mergedState, nextStep: null, updatedAt: new Date().toISOString() } });
217
+ console.log(`Workflow ${stepsName} ${instanceId} is completed in time: ${completionTime}s 🎉`);
218
+ StepsEngine.getInstance().emit('completed', { ...finalresult});
219
+ resolve();
220
+ return;
221
+ }
222
+
223
+ // Enqueue the next step
224
+ console.debug('enqueuing step', nextStep, instanceId);
225
+ await connection.enqueue(`${StepsEngine.queuePrefix}_${stepsName}_${nextStep}`, {
226
+ stepsName: stepsName,
227
+ goto: nextStep,
228
+ state: mergedState,
229
+ options: options,
230
+ instanceId: instanceId
231
+ });
232
+
233
+ StepsEngine.getInstance().emit('stepEnqueued', { workflowName: stepsName, step: nextStep, state: newState, instanceId });
234
+ resolve();
235
+ } catch (error) {
236
+ console.error('error', error.message);
237
+ reject(error);
238
+ }
239
+ });
199
240
  });
200
241
  } catch (error) {
201
- console.error('Error in step function: '+nextStep, error.message);
202
- const connection = await Datastore.open();
203
- await connection.updateOne(StepsEngine.collectionName,
204
- { _id: instanceId },
205
- { $set: { lastError: error.message, updatedAt: new Date().toISOString() } });
206
242
  throw error;
207
243
  }
208
244
  }
@@ -228,12 +264,19 @@ class StepsEngine extends EventEmitter {
228
264
  try {
229
265
  if (stepName !== undefined) {
230
266
  //console.log('registering queue for step', `${StepsEngine.queuePrefix}_${name}_${stepName}`);
231
- app.worker(`${StepsEngine.queuePrefix}_${name}_${stepName}`, async (req, res) => {
232
- const { stepsName, goto, state, instanceId, options } = req.body.payload;
233
- try {
267
+ app.worker(`${StepsEngine.queuePrefix}_${name}_${stepName}`, async function(req, res) {
268
+ try {
269
+ const { stepsName, goto, state, instanceId, options } = req.body.payload;
270
+ console.debug('worker job', stepName, instanceId);
234
271
  const qid = await StepsEngine.getInstance().handleNextStep(stepsName, goto, state, instanceId, options);
235
272
  } catch (error) {
236
- 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);
237
280
  } finally {
238
281
  res.end();
239
282
  }
@@ -271,7 +314,7 @@ class StepsEngine extends EventEmitter {
271
314
  const connection = await Datastore.open();
272
315
  // Create a new steps state in the database
273
316
  const newState = await connection.insertOne(StepsEngine.collectionName,
274
- { ...initialState, nextStep: firstStepName, createdAt: new Date().toISOString(), workflowName: name });
317
+ { ...initialState, nextStep: firstStepName, createdAt: new Date().toISOString(), workflowName: name, stepCount: {} });
275
318
  const { _id: ID } = await connection.enqueue(`${StepsEngine.queuePrefix}_${name}_${firstStepName}`, {
276
319
  stepsName: name,
277
320
  goto: firstStepName,
@@ -333,8 +376,12 @@ class StepsEngine extends EventEmitter {
333
376
  if (!state) {
334
377
  throw new Error(`No steps found with instanceId: ${instanceId}`);
335
378
  }
336
-
337
- console.log('continue state', state);
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 } });
384
+ console.debug('continue state', state);
338
385
  StepsEngine.getInstance().emit('workflowContinued', { stepsName, step: state.nextStep, instanceId });
339
386
 
340
387
  return new Promise(async (resolve, reject) => {
@@ -350,6 +397,29 @@ class StepsEngine extends EventEmitter {
350
397
  });
351
398
  }
352
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
+
353
423
  /**
354
424
  * Get the status of a steps instance
355
425
  * @param {string} id - ID of the steps instance
@@ -372,7 +442,7 @@ class StepsEngine extends EventEmitter {
372
442
  return new Promise(async (resolve, reject) => {
373
443
  const connection = await Datastore.open();
374
444
  const states = await connection.find(StepsEngine.collectionName, filter).toArray();
375
- console.log('listSteps', StepsEngine.collectionName, filter, states.length);
445
+ console.debug('listSteps', StepsEngine.collectionName, filter, states.length);
376
446
  resolve(states);
377
447
  });
378
448
  }