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 +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 +137 -67
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
|
/**
|
|
@@ -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
|
-
//
|
|
151
|
-
func
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
445
|
+
console.debug('listSteps', StepsEngine.collectionName, filter, states.length);
|
|
376
446
|
resolve(states);
|
|
377
447
|
});
|
|
378
448
|
}
|