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 +1 -1
- package/types/index.d.ts +4 -0
- package/types/workflow/engine.d.mts +23 -0
- package/types/workflow/index.d.mts +1 -0
- package/workflow/engine.mjs +160 -56
package/package.json
CHANGED
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
|
package/workflow/engine.mjs
CHANGED
|
@@ -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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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('
|
|
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
|
-
|
|
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({
|
|
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
|