codehooks-js 1.3.8 → 1.3.9
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/index.js +5 -6
- package/package.json +1 -1
- package/types/index.d.ts +199 -80
- package/workflow/engine.mjs +349 -107
- package/workflow/index.mjs +2 -14
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {agg} from './aggregation/index.mjs';
|
|
2
2
|
import {crudlify as crud} from './crudlify/index.mjs';
|
|
3
3
|
import {serveStatic as ws, render as renderView} from './webserver.mjs';
|
|
4
|
-
import
|
|
4
|
+
import Workflow from './workflow/engine.mjs';
|
|
5
5
|
|
|
6
6
|
function createRoute(str) {
|
|
7
7
|
if(str instanceof RegExp) {
|
|
@@ -118,8 +118,9 @@ class Codehooks {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
createWorkflow = (name, description, steps, options={}) => {
|
|
121
|
-
Workflow
|
|
122
|
-
|
|
121
|
+
const wf = new Workflow(name, description, steps, options);
|
|
122
|
+
wf.register(this);
|
|
123
|
+
return wf;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
init = (hook) => {
|
|
@@ -193,10 +194,8 @@ export const aggregation = agg;
|
|
|
193
194
|
export const crudlify = crud;
|
|
194
195
|
export const coho = _coho;
|
|
195
196
|
export const app = _coho;
|
|
196
|
-
export const workflowconfig = WorkflowConfig;
|
|
197
197
|
export {
|
|
198
|
-
Workflow
|
|
199
|
-
WorkflowConfig
|
|
198
|
+
Workflow
|
|
200
199
|
};
|
|
201
200
|
export const realtime = {
|
|
202
201
|
createChannel: (path, ...hook) => {
|
package/package.json
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -1032,21 +1032,29 @@ export class Codehooks {
|
|
|
1032
1032
|
};
|
|
1033
1033
|
addListener: (observer: any) => void;
|
|
1034
1034
|
/**
|
|
1035
|
-
* Create a new
|
|
1036
|
-
* @param name - Unique identifier for the
|
|
1037
|
-
* @param description - Human-readable description of the
|
|
1035
|
+
* Create a new workflow
|
|
1036
|
+
* @param name - Unique identifier for the workflow
|
|
1037
|
+
* @param description - Human-readable description of the workflow
|
|
1038
1038
|
* @param steps - Object containing step definitions
|
|
1039
1039
|
* @param options - Optional configuration options
|
|
1040
|
-
* @returns
|
|
1040
|
+
* @returns Workflow instance for managing the workflow
|
|
1041
|
+
*/
|
|
1042
|
+
createWorkflow: (name: string, description: string, steps: WorkflowDefinition, options?: WorkflowConfig) => Workflow;
|
|
1043
|
+
/**
|
|
1044
|
+
* Register a workflow with the application
|
|
1045
|
+
* @param name - Unique identifier for the workflow
|
|
1046
|
+
* @param description - Human-readable description
|
|
1047
|
+
* @param steps - Step definitions
|
|
1048
|
+
* @returns Promise with the registered workflow name
|
|
1041
1049
|
*/
|
|
1042
|
-
|
|
1050
|
+
registerWorkflow: (name: string, description: string, steps: WorkflowDefinition) => Promise<string>;
|
|
1043
1051
|
}
|
|
1044
1052
|
declare const _coho: Codehooks;
|
|
1045
1053
|
|
|
1046
1054
|
/**
|
|
1047
1055
|
* Events emitted by the workflow engine
|
|
1048
1056
|
*/
|
|
1049
|
-
export type
|
|
1057
|
+
export type WorkflowEvents = {
|
|
1050
1058
|
/**
|
|
1051
1059
|
* Emitted when a new workflow is registered
|
|
1052
1060
|
* @event
|
|
@@ -1081,7 +1089,7 @@ export type StepsEvents = {
|
|
|
1081
1089
|
* Emitted when a workflow instance is continued after waiting
|
|
1082
1090
|
* @event
|
|
1083
1091
|
*/
|
|
1084
|
-
'
|
|
1092
|
+
'workflowContinued': { workflowName: string; step: string; instanceId: string };
|
|
1085
1093
|
|
|
1086
1094
|
/**
|
|
1087
1095
|
* Emitted when a workflow instance is completed
|
|
@@ -1094,173 +1102,284 @@ export type StepsEvents = {
|
|
|
1094
1102
|
* @event
|
|
1095
1103
|
*/
|
|
1096
1104
|
'cancelled': { id: string };
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Emitted when an error occurs
|
|
1108
|
+
* @event
|
|
1109
|
+
*/
|
|
1110
|
+
'error': { error: Error };
|
|
1097
1111
|
};
|
|
1098
1112
|
|
|
1099
1113
|
/**
|
|
1100
|
-
* Definition of
|
|
1114
|
+
* Definition of a workflow step function
|
|
1101
1115
|
*/
|
|
1102
|
-
export type
|
|
1116
|
+
export type WorkflowDefinition = Record<string, (state: any, callback: (nextStep: string | string[] | null, newState: any, options?: any) => void) => Promise<void>>;
|
|
1103
1117
|
|
|
1104
1118
|
/**
|
|
1105
|
-
*
|
|
1119
|
+
* Configuration options for a workflow step
|
|
1106
1120
|
*/
|
|
1107
|
-
export type
|
|
1108
|
-
/**
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1121
|
+
export type StepOptions = {
|
|
1122
|
+
/** Timeout in milliseconds for this specific step */
|
|
1123
|
+
timeout?: number;
|
|
1124
|
+
/** Maximum number of retries for this step */
|
|
1125
|
+
maxRetries?: number;
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Configuration options for the Workflow engine
|
|
1130
|
+
*/
|
|
1131
|
+
export type WorkflowConfig = {
|
|
1132
|
+
/** Collection name for storing workflow data */
|
|
1133
|
+
collectionName?: string;
|
|
1134
|
+
/** Queue prefix for workflow jobs */
|
|
1135
|
+
queuePrefix?: string;
|
|
1136
|
+
/** Global timeout in milliseconds for workflow steps */
|
|
1137
|
+
timeout?: number;
|
|
1138
|
+
/** Maximum number of times a step can be executed */
|
|
1139
|
+
maxStepCount?: number;
|
|
1140
|
+
/** Step-specific configuration options */
|
|
1141
|
+
steps?: Record<string, StepOptions>;
|
|
1118
1142
|
};
|
|
1119
1143
|
|
|
1120
1144
|
/**
|
|
1121
|
-
* Engine for managing
|
|
1122
|
-
* @extends
|
|
1145
|
+
* Engine for managing workflow-based applications
|
|
1146
|
+
* @extends Workflow
|
|
1123
1147
|
*/
|
|
1124
|
-
export type WorkflowEngine =
|
|
1148
|
+
export type WorkflowEngine = Workflow & {
|
|
1125
1149
|
/**
|
|
1126
|
-
* Register a new
|
|
1150
|
+
* Register a new workflow
|
|
1127
1151
|
* @param name - Unique identifier for the workflow
|
|
1128
1152
|
* @param description - Human-readable description
|
|
1129
1153
|
* @param steps - Step definitions
|
|
1130
1154
|
*/
|
|
1131
|
-
register: (name: string, description: string, steps:
|
|
1155
|
+
register: (name: string, description: string, steps: WorkflowDefinition) => Promise<string>;
|
|
1132
1156
|
};
|
|
1133
1157
|
|
|
1134
1158
|
/**
|
|
1135
|
-
*
|
|
1159
|
+
* Workflow engine for managing step-based applications
|
|
1136
1160
|
* @extends EventEmitter
|
|
1137
1161
|
*/
|
|
1138
|
-
export type
|
|
1162
|
+
export type Workflow = {
|
|
1139
1163
|
/**
|
|
1140
1164
|
* Configure the workflow engine
|
|
1141
1165
|
* @param options - Configuration options
|
|
1166
|
+
* @param options.collectionName - Collection name for storing workflow data
|
|
1167
|
+
* @param options.queuePrefix - Queue prefix for workflow jobs
|
|
1168
|
+
* @param options.timeout - Timeout in milliseconds for workflow steps
|
|
1169
|
+
* @param options.maxStepCount - Maximum number of times a step can be executed
|
|
1170
|
+
* @param options.steps - Workflow step configuration
|
|
1171
|
+
* @example
|
|
1172
|
+
* workflow.configure({
|
|
1173
|
+
* collectionName: 'workflows',
|
|
1174
|
+
* queuePrefix: 'workflow',
|
|
1175
|
+
* timeout: 30000,
|
|
1176
|
+
* maxStepCount: 3,
|
|
1177
|
+
* steps: {
|
|
1178
|
+
* stepName: {
|
|
1179
|
+
* timeout: 3000,
|
|
1180
|
+
* maxRetries: 3
|
|
1181
|
+
* }
|
|
1182
|
+
* }
|
|
1183
|
+
* });
|
|
1142
1184
|
*/
|
|
1143
|
-
configure: (options:
|
|
1185
|
+
configure: (options: WorkflowConfig) => void;
|
|
1144
1186
|
|
|
1145
1187
|
/**
|
|
1146
|
-
* Register a new
|
|
1188
|
+
* Register a new workflow
|
|
1147
1189
|
* @param app - Codehooks application instance
|
|
1148
|
-
* @param name - Unique identifier for the
|
|
1190
|
+
* @param name - Unique identifier for the workflow
|
|
1149
1191
|
* @param description - Human-readable description
|
|
1150
1192
|
* @param definition - Object containing step definitions
|
|
1151
|
-
* @returns Promise with the registered
|
|
1193
|
+
* @returns Promise with the registered workflow name
|
|
1152
1194
|
*/
|
|
1153
1195
|
register: (app: Codehooks, name: string, description: string, definition: Record<string, Function>) => Promise<string>;
|
|
1154
1196
|
|
|
1155
1197
|
/**
|
|
1156
|
-
* Start a new
|
|
1157
|
-
* @param name - Name of the
|
|
1158
|
-
* @param initialState - Initial state for the
|
|
1159
|
-
* @returns Promise with the
|
|
1198
|
+
* Start a new workflow instance
|
|
1199
|
+
* @param name - Name of the workflow to start
|
|
1200
|
+
* @param initialState - Initial state for the workflow instance
|
|
1201
|
+
* @returns Promise with the workflow instance ID
|
|
1160
1202
|
*/
|
|
1161
1203
|
start: (name: string, initialState: any) => Promise<{ id: string }>;
|
|
1162
1204
|
|
|
1163
1205
|
/**
|
|
1164
|
-
* Update the state of a
|
|
1165
|
-
* @param
|
|
1166
|
-
* @param instanceId - ID of the
|
|
1206
|
+
* Update the state of a workflow instance
|
|
1207
|
+
* @param workflowName - Name of the workflow
|
|
1208
|
+
* @param instanceId - ID of the workflow instance
|
|
1167
1209
|
* @param state - New state to update with
|
|
1168
|
-
* @param options - Options for the update, { continue: false } to avoid continuing the
|
|
1210
|
+
* @param options - Options for the update, { continue: false } to avoid continuing the step
|
|
1169
1211
|
* @returns Promise with the updated state
|
|
1170
1212
|
*/
|
|
1171
|
-
updateState: (
|
|
1213
|
+
updateState: (workflowName: string, instanceId: string, state: any, options?: UpdateOptions) => Promise<any>;
|
|
1172
1214
|
|
|
1173
1215
|
/**
|
|
1174
|
-
* Continue a paused
|
|
1175
|
-
* @param
|
|
1176
|
-
* @param instanceId - ID of the
|
|
1216
|
+
* Continue a paused workflow instance
|
|
1217
|
+
* @param workflowName - Name of the workflow
|
|
1218
|
+
* @param instanceId - ID of the workflow instance
|
|
1219
|
+
* @param reset - Whether to reset all step counts (true) or just the current step (false)
|
|
1177
1220
|
* @returns Promise with the queue ID for the continued step
|
|
1178
1221
|
*/
|
|
1179
|
-
continue: (
|
|
1222
|
+
continue: (workflowName: string, instanceId: string, reset?: boolean) => Promise<{ qId: string }>;
|
|
1180
1223
|
|
|
1181
1224
|
/**
|
|
1182
|
-
* Get the status of a
|
|
1183
|
-
* @param id - ID of the
|
|
1184
|
-
* @returns Promise with the
|
|
1225
|
+
* Get the status of a workflow instance
|
|
1226
|
+
* @param id - ID of the workflow instance
|
|
1227
|
+
* @returns Promise with the workflow status
|
|
1185
1228
|
*/
|
|
1186
|
-
|
|
1229
|
+
getWorkflowStatus: (id: string) => Promise<any>;
|
|
1187
1230
|
|
|
1188
1231
|
/**
|
|
1189
|
-
* Get all
|
|
1190
|
-
* @param filter - Filter criteria for
|
|
1191
|
-
* @returns Promise with list of
|
|
1232
|
+
* Get all workflow instances matching a filter
|
|
1233
|
+
* @param filter - Filter criteria for workflows
|
|
1234
|
+
* @returns Promise with list of workflow instances
|
|
1192
1235
|
*/
|
|
1193
1236
|
getInstances: (filter: any) => Promise<any[]>;
|
|
1194
1237
|
|
|
1195
1238
|
/**
|
|
1196
|
-
* Cancel a
|
|
1197
|
-
* @param id - ID of the
|
|
1239
|
+
* Cancel a workflow instance
|
|
1240
|
+
* @param id - ID of the workflow instance to cancel
|
|
1198
1241
|
* @returns Promise with the cancellation result
|
|
1199
1242
|
*/
|
|
1200
|
-
|
|
1243
|
+
cancelWorkflow: (id: string) => Promise<any>;
|
|
1201
1244
|
|
|
1202
1245
|
/**
|
|
1203
1246
|
* Register an event listener
|
|
1204
1247
|
* @param event - Name of the event to listen for
|
|
1205
1248
|
* @param listener - Callback function to handle the event
|
|
1206
1249
|
* @example
|
|
1207
|
-
*
|
|
1208
|
-
* console.log(`Step ${step} started in workflow ${
|
|
1250
|
+
* workflow.on('stepStarted', ({ workflowName, step, state, instanceId }) => {
|
|
1251
|
+
* console.log(`Step ${step} started in workflow ${workflowName}`);
|
|
1209
1252
|
* });
|
|
1210
1253
|
*/
|
|
1211
|
-
on: (event:
|
|
1254
|
+
on: (event: WorkflowEvent, listener: (data: WorkflowEventData) => void) => Workflow;
|
|
1212
1255
|
|
|
1213
1256
|
/**
|
|
1214
1257
|
* Register a one-time event listener
|
|
1215
1258
|
* @param event - Name of the event to listen for
|
|
1216
1259
|
* @param listener - Callback function to handle the event
|
|
1217
1260
|
*/
|
|
1218
|
-
once: (event:
|
|
1261
|
+
once: (event: WorkflowEvent, listener: (data: WorkflowEventData) => void) => Workflow;
|
|
1219
1262
|
|
|
1220
1263
|
/**
|
|
1221
1264
|
* Remove an event listener
|
|
1222
1265
|
* @param event - Name of the event
|
|
1223
1266
|
* @param listener - Callback function to remove
|
|
1224
1267
|
*/
|
|
1225
|
-
off: (event:
|
|
1268
|
+
off: (event: WorkflowEvent, listener: (data: WorkflowEventData) => void) => Workflow;
|
|
1226
1269
|
|
|
1227
1270
|
/**
|
|
1228
1271
|
* Emit an event
|
|
1229
1272
|
* @param event - Name of the event to emit
|
|
1230
1273
|
* @param data - Event data
|
|
1231
1274
|
*/
|
|
1232
|
-
emit: (event:
|
|
1275
|
+
emit: (event: WorkflowEvent, data: WorkflowEventData) => boolean;
|
|
1233
1276
|
|
|
1234
1277
|
/**
|
|
1235
|
-
* Continue all timed out
|
|
1236
|
-
* @returns
|
|
1278
|
+
* Continue all timed out workflow instances
|
|
1279
|
+
* @returns Promise with array of results containing queue IDs for continued workflows
|
|
1237
1280
|
*/
|
|
1238
1281
|
continueAllTimedOut: () => Promise<Array<{qId: string}>>;
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Check if a specific step in a workflow instance has timed out
|
|
1285
|
+
* @param workflow - The workflow instance to check
|
|
1286
|
+
* @returns Object containing timeout status and details
|
|
1287
|
+
* @example
|
|
1288
|
+
* const status = workflow.isStepTimedOut(workflowInstance);
|
|
1289
|
+
* // Returns:
|
|
1290
|
+
* // {
|
|
1291
|
+
* // isTimedOut: boolean,
|
|
1292
|
+
* // executionTime?: number, // if step has finished
|
|
1293
|
+
* // runningTime?: number, // if step is still running
|
|
1294
|
+
* // timeout: number, // actual timeout value used
|
|
1295
|
+
* // step: string, // name of the step checked
|
|
1296
|
+
* // startTime: string, // ISO timestamp
|
|
1297
|
+
* // finishTime?: string, // if step has finished
|
|
1298
|
+
* // currentTime?: string, // if step is still running
|
|
1299
|
+
* // timeoutSource: 'stepConfig' | 'defaultOptions' | 'globalTimeout'
|
|
1300
|
+
* // }
|
|
1301
|
+
*/
|
|
1302
|
+
isStepTimedOut: (workflow: any) => {
|
|
1303
|
+
isTimedOut: boolean;
|
|
1304
|
+
executionTime?: number;
|
|
1305
|
+
runningTime?: number;
|
|
1306
|
+
timeout: number;
|
|
1307
|
+
step: string;
|
|
1308
|
+
startTime: string;
|
|
1309
|
+
finishTime?: string;
|
|
1310
|
+
currentTime?: string;
|
|
1311
|
+
timeoutSource: 'stepConfig' | 'defaultOptions' | 'globalTimeout';
|
|
1312
|
+
reason?: string;
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Find all workflow instances with timed out steps
|
|
1317
|
+
* @param filter - Optional filter criteria for workflows
|
|
1318
|
+
* @returns Promise with array of workflow instances that have timed out steps
|
|
1319
|
+
* @example
|
|
1320
|
+
* const timedOutWorkflows = await workflow.findTimedOutSteps();
|
|
1321
|
+
* // Returns array of workflows with timeout details:
|
|
1322
|
+
* // [{
|
|
1323
|
+
* // workflowId: string,
|
|
1324
|
+
* // workflowName: string,
|
|
1325
|
+
* // isTimedOut: true,
|
|
1326
|
+
* // executionTime?: number,
|
|
1327
|
+
* // runningTime?: number,
|
|
1328
|
+
* // timeout: number,
|
|
1329
|
+
* // step: string,
|
|
1330
|
+
* // startTime: string,
|
|
1331
|
+
* // finishTime?: string,
|
|
1332
|
+
* // currentTime?: string,
|
|
1333
|
+
* // timeoutSource: string
|
|
1334
|
+
* // }]
|
|
1335
|
+
*/
|
|
1336
|
+
findTimedOutSteps: (filter?: any) => Promise<Array<{
|
|
1337
|
+
workflowId: string;
|
|
1338
|
+
workflowName: string;
|
|
1339
|
+
isTimedOut: boolean;
|
|
1340
|
+
executionTime?: number;
|
|
1341
|
+
runningTime?: number;
|
|
1342
|
+
timeout: number;
|
|
1343
|
+
step: string;
|
|
1344
|
+
startTime: string;
|
|
1345
|
+
finishTime?: string;
|
|
1346
|
+
currentTime?: string;
|
|
1347
|
+
timeoutSource: 'stepConfig' | 'defaultOptions' | 'globalTimeout';
|
|
1348
|
+
}>>;
|
|
1239
1349
|
};
|
|
1240
1350
|
|
|
1241
1351
|
/**
|
|
1242
|
-
*
|
|
1352
|
+
* Options for updating workflow state
|
|
1243
1353
|
*/
|
|
1244
|
-
export type
|
|
1245
|
-
/**
|
|
1246
|
-
|
|
1247
|
-
* @param name - Collection name
|
|
1248
|
-
*/
|
|
1249
|
-
setCollectionName: (name: string) => void;
|
|
1250
|
-
|
|
1251
|
-
/**
|
|
1252
|
-
* Set the queue prefix for steps jobs
|
|
1253
|
-
* @param prefix - Queue prefix
|
|
1254
|
-
*/
|
|
1255
|
-
setQueuePrefix: (prefix: string) => void;
|
|
1354
|
+
export type UpdateOptions = {
|
|
1355
|
+
/** Whether to continue to the next step after update */
|
|
1356
|
+
continue?: boolean;
|
|
1256
1357
|
};
|
|
1257
1358
|
|
|
1258
|
-
|
|
1359
|
+
/**
|
|
1360
|
+
* Workflow event types
|
|
1361
|
+
*/
|
|
1362
|
+
export type WorkflowEvent =
|
|
1363
|
+
| 'workflowCreated'
|
|
1364
|
+
| 'workflowStarted'
|
|
1365
|
+
| 'workflowContinued'
|
|
1366
|
+
| 'stepStarted'
|
|
1367
|
+
| 'stepEnqueued'
|
|
1368
|
+
| 'stateUpdated'
|
|
1369
|
+
| 'completed'
|
|
1370
|
+
| 'cancelled'
|
|
1371
|
+
| 'error';
|
|
1259
1372
|
|
|
1260
1373
|
/**
|
|
1261
|
-
*
|
|
1262
|
-
* @param options - Configuration options
|
|
1374
|
+
* Data structure for workflow events
|
|
1263
1375
|
*/
|
|
1264
|
-
export
|
|
1376
|
+
export type WorkflowEventData = {
|
|
1377
|
+
workflowName: string;
|
|
1378
|
+
step?: string;
|
|
1379
|
+
state?: any;
|
|
1380
|
+
instanceId?: string;
|
|
1381
|
+
error?: Error;
|
|
1382
|
+
[key: string]: any;
|
|
1383
|
+
};
|
|
1265
1384
|
|
|
1266
1385
|
//# sourceMappingURL=index.d.ts.map
|
package/workflow/engine.mjs
CHANGED
|
@@ -1,40 +1,76 @@
|
|
|
1
1
|
/*
|
|
2
|
-
Implements a
|
|
3
|
-
The engine manages step transitions, state persistence, and event handling for
|
|
2
|
+
Implements a workflow engine that uses Codehooks.io as the state storage and queues for persistent workers.
|
|
3
|
+
The engine manages step transitions, state persistence, and event handling for workflow-based applications.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { EventEmitter } from 'events';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Workflow class that manages step-based workflows
|
|
10
10
|
* @extends EventEmitter
|
|
11
11
|
*/
|
|
12
|
-
class
|
|
12
|
+
class Workflow extends EventEmitter {
|
|
13
|
+
|
|
14
|
+
// Protected instance variables
|
|
15
|
+
#collectionName = 'workflowdata'; // collection name for storing workflow data
|
|
16
|
+
#queuePrefix = 'workflowqueue'; // queue prefix for storing workflow data
|
|
17
|
+
#timeout = 30000; // timeout for a workflow instance
|
|
18
|
+
#maxStepCount = 3; // maximum number of steps to execute
|
|
19
|
+
#steps = {}; // workflowsteps configuration
|
|
20
|
+
#definitions = null; // workflow definition map
|
|
21
|
+
#name = null; // workflow name
|
|
22
|
+
#description = null; // workflow description
|
|
23
|
+
#defaultStepOptions = { // default step options
|
|
24
|
+
timeout: 30000, // timeout for a step
|
|
25
|
+
maxRetries: 3 // maximum number of retries for a step
|
|
26
|
+
};
|
|
13
27
|
|
|
14
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Create a new workflow instance
|
|
30
|
+
* @param {string} name - Unique identifier for the workflow
|
|
31
|
+
* @param {string} description - Human-readable description
|
|
32
|
+
* @param {Object} definition - Object containing step definitions
|
|
33
|
+
* @param {Object} options - Optional configuration options
|
|
34
|
+
*/
|
|
35
|
+
constructor(name, description, definition, options = {}) {
|
|
15
36
|
super();
|
|
16
|
-
this
|
|
17
|
-
|
|
37
|
+
this.#definitions = new Map();
|
|
38
|
+
this.#name = name;
|
|
39
|
+
this.#description = description;
|
|
40
|
+
|
|
41
|
+
// Apply any configuration options
|
|
42
|
+
if (options) {
|
|
43
|
+
this.configure(options);
|
|
44
|
+
}
|
|
18
45
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
// Store the initial definition if provided
|
|
47
|
+
if (definition) {
|
|
48
|
+
this.#definitions.set(name, definition);
|
|
49
|
+
console.debug('Initial workflow definition stored:', {
|
|
50
|
+
name,
|
|
51
|
+
stepNames: Object.keys(definition)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
27
55
|
|
|
28
56
|
/**
|
|
29
57
|
* Set the collection name for storing steps data
|
|
30
58
|
* @param {string} name - Collection name
|
|
31
59
|
* @throws {Error} If name is not a non-empty string
|
|
32
60
|
*/
|
|
33
|
-
|
|
61
|
+
setCollectionName(name) {
|
|
34
62
|
if (typeof name !== 'string' || !name.trim()) {
|
|
35
63
|
throw new Error('Collection name must be a non-empty string');
|
|
36
64
|
}
|
|
37
|
-
|
|
65
|
+
this.#collectionName = name.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the collection name
|
|
70
|
+
* @returns {string} The collection name
|
|
71
|
+
*/
|
|
72
|
+
getCollectionName() {
|
|
73
|
+
return this.#collectionName;
|
|
38
74
|
}
|
|
39
75
|
|
|
40
76
|
/**
|
|
@@ -42,11 +78,19 @@ class StepsEngine extends EventEmitter {
|
|
|
42
78
|
* @param {string} prefix - Queue prefix
|
|
43
79
|
* @throws {Error} If prefix is not a non-empty string
|
|
44
80
|
*/
|
|
45
|
-
|
|
81
|
+
setQueuePrefix(prefix) {
|
|
46
82
|
if (typeof prefix !== 'string' || !prefix.trim()) {
|
|
47
83
|
throw new Error('Queue prefix must be a non-empty string');
|
|
48
84
|
}
|
|
49
|
-
|
|
85
|
+
this.#queuePrefix = prefix.trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get the queue prefix
|
|
90
|
+
* @returns {string} The queue prefix
|
|
91
|
+
*/
|
|
92
|
+
getQueuePrefix() {
|
|
93
|
+
return this.#queuePrefix;
|
|
50
94
|
}
|
|
51
95
|
|
|
52
96
|
/**
|
|
@@ -54,11 +98,19 @@ class StepsEngine extends EventEmitter {
|
|
|
54
98
|
* @param {number} timeout - Timeout in milliseconds
|
|
55
99
|
* @throws {Error} If timeout is not a positive number
|
|
56
100
|
*/
|
|
57
|
-
|
|
101
|
+
setTimeout(timeout) {
|
|
58
102
|
if (typeof timeout !== 'number' || timeout <= 0) {
|
|
59
103
|
throw new Error('Timeout must be a positive number');
|
|
60
104
|
}
|
|
61
|
-
|
|
105
|
+
this.#timeout = timeout;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the timeout
|
|
110
|
+
* @returns {number} The timeout in milliseconds
|
|
111
|
+
*/
|
|
112
|
+
getTimeout() {
|
|
113
|
+
return this.#timeout;
|
|
62
114
|
}
|
|
63
115
|
|
|
64
116
|
/**
|
|
@@ -66,11 +118,39 @@ class StepsEngine extends EventEmitter {
|
|
|
66
118
|
* @param {number} maxStepCount - Maximum step count
|
|
67
119
|
* @throws {Error} If maxStepCount is not a positive number
|
|
68
120
|
*/
|
|
69
|
-
|
|
121
|
+
setMaxStepCount(maxStepCount) {
|
|
70
122
|
if (typeof maxStepCount !== 'number' || maxStepCount <= 0) {
|
|
71
123
|
throw new Error('Maximum step count must be a positive number');
|
|
72
124
|
}
|
|
73
|
-
|
|
125
|
+
this.#maxStepCount = maxStepCount;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get the maximum step count
|
|
130
|
+
* @returns {number} The maximum step count
|
|
131
|
+
*/
|
|
132
|
+
getMaxStepCount() {
|
|
133
|
+
return this.#maxStepCount;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Set the steps configuration
|
|
138
|
+
* @param {Object} steps - Steps configuration
|
|
139
|
+
* @throws {Error} If steps is not an object or is null
|
|
140
|
+
*/
|
|
141
|
+
setStepsConfig(steps) {
|
|
142
|
+
if (typeof steps !== 'object' || steps === null) {
|
|
143
|
+
throw new Error('Steps must be an object');
|
|
144
|
+
}
|
|
145
|
+
this.#steps = steps;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the steps configuration
|
|
150
|
+
* @returns {Object} The steps configuration
|
|
151
|
+
*/
|
|
152
|
+
getStepsConfig() {
|
|
153
|
+
return this.#steps;
|
|
74
154
|
}
|
|
75
155
|
|
|
76
156
|
/**
|
|
@@ -79,33 +159,27 @@ class StepsEngine extends EventEmitter {
|
|
|
79
159
|
* @param {string} config.collectionName - Collection name
|
|
80
160
|
* @param {string} config.queuePrefix - Queue prefix
|
|
81
161
|
* @param {number} config.timeout - Timeout in milliseconds
|
|
162
|
+
* @param {number} config.maxStepCount - Maximum step count
|
|
163
|
+
* @param {Object} config.steps - Steps configuration
|
|
82
164
|
*/
|
|
83
165
|
configure(config) {
|
|
84
166
|
if (config.collectionName) {
|
|
85
|
-
|
|
167
|
+
this.setCollectionName(config.collectionName);
|
|
86
168
|
}
|
|
87
169
|
if (config.queuePrefix) {
|
|
88
|
-
|
|
170
|
+
this.setQueuePrefix(config.queuePrefix);
|
|
89
171
|
}
|
|
90
172
|
if (config.timeout) {
|
|
91
|
-
|
|
173
|
+
this.setTimeout(config.timeout);
|
|
92
174
|
}
|
|
93
175
|
if (config.maxStepCount) {
|
|
94
|
-
|
|
176
|
+
this.setMaxStepCount(config.maxStepCount);
|
|
95
177
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Get the singleton instance of StepsEngine
|
|
100
|
-
* @returns {StepsEngine} The singleton instance
|
|
101
|
-
*/
|
|
102
|
-
static getInstance() {
|
|
103
|
-
if (!StepsEngine.instance) {
|
|
104
|
-
StepsEngine.instance = new StepsEngine();
|
|
178
|
+
if (config.steps) {
|
|
179
|
+
this.setStepsConfig(config.steps);
|
|
105
180
|
}
|
|
106
|
-
return StepsEngine.instance;
|
|
107
181
|
}
|
|
108
|
-
|
|
182
|
+
|
|
109
183
|
/**
|
|
110
184
|
* Get the step definition for a specific step
|
|
111
185
|
* @param {string} stepsName - Name of the steps workflow
|
|
@@ -114,7 +188,7 @@ class StepsEngine extends EventEmitter {
|
|
|
114
188
|
* @throws {Error} If steps definition or step function not found
|
|
115
189
|
*/
|
|
116
190
|
getDefinition(stepsName, stepName) {
|
|
117
|
-
const stepsDef = this
|
|
191
|
+
const stepsDef = this.#definitions.get(stepsName);
|
|
118
192
|
if (!stepsDef) {
|
|
119
193
|
throw new Error(`No Steps definition found for: ${stepsName}`);
|
|
120
194
|
}
|
|
@@ -156,28 +230,34 @@ class StepsEngine extends EventEmitter {
|
|
|
156
230
|
const connection = await Datastore.open();
|
|
157
231
|
|
|
158
232
|
// Handle single next step
|
|
159
|
-
|
|
233
|
+
this.emit('stepStarted', { workflowName: stepsName, step: nextStep, state: newState, instanceId });
|
|
160
234
|
|
|
161
235
|
// remove the _id from the newState
|
|
162
236
|
delete newState._id;
|
|
163
237
|
// increment the step count
|
|
164
|
-
|
|
238
|
+
if (!newState.stepCount) {
|
|
239
|
+
newState.stepCount = {};
|
|
240
|
+
}
|
|
241
|
+
if (!newState.stepCount[nextStep]) {
|
|
242
|
+
newState.stepCount[nextStep] = { visits: 0, startTime: new Date().toISOString(), totalTime: 0 };
|
|
243
|
+
}
|
|
244
|
+
newState.stepCount[nextStep].visits += 1;
|
|
165
245
|
// Update the existing steps state in the database
|
|
166
|
-
newState = await connection.updateOne(
|
|
246
|
+
newState = await connection.updateOne(this.#collectionName,
|
|
167
247
|
{ _id: instanceId },
|
|
168
248
|
{ $set: { ...newState, nextStep: nextStep, updatedAt: new Date().toISOString(), stepCount: newState.stepCount } });
|
|
169
249
|
|
|
170
|
-
|
|
250
|
+
this.emit('stateUpdated', { workflowName: stepsName, state: newState, instanceId });
|
|
171
251
|
|
|
172
252
|
try {
|
|
173
253
|
// Get the next step function
|
|
174
|
-
const func =
|
|
254
|
+
const func = this.getDefinition(stepsName, nextStep);
|
|
175
255
|
|
|
176
256
|
// Wrap the callback in a Promise to ensure proper async handling
|
|
177
257
|
await new Promise(async (resolve, reject) => {
|
|
178
258
|
// check if the step count is greater than the max step count
|
|
179
|
-
if (newState.stepCount[nextStep] >
|
|
180
|
-
reject(new Error(`Step ${nextStep} has been executed ${newState.stepCount[nextStep]} times, which is greater than the maximum step count of ${
|
|
259
|
+
if (newState.stepCount[nextStep].visits > this.getMaxStepCount()) {
|
|
260
|
+
reject(new Error(`Step ${nextStep} has been executed ${newState.stepCount[nextStep].visits} times, which is greater than the maximum step count of ${this.getMaxStepCount()}`));
|
|
181
261
|
return;
|
|
182
262
|
}
|
|
183
263
|
try {
|
|
@@ -202,15 +282,19 @@ class StepsEngine extends EventEmitter {
|
|
|
202
282
|
...protectedState
|
|
203
283
|
};
|
|
204
284
|
|
|
285
|
+
// set step finish time
|
|
286
|
+
mergedState.stepCount[mergedState.nextStep].finishTime = new Date().toISOString();
|
|
287
|
+
mergedState.stepCount[mergedState.nextStep].totalTime = (new Date(mergedState.stepCount[mergedState.nextStep].finishTime) - new Date(mergedState.stepCount[mergedState.nextStep].startTime));
|
|
288
|
+
|
|
205
289
|
// update the parallel steps metadata
|
|
206
290
|
if (mergedState.parallelSteps && mergedState.parallelSteps[mergedState.nextStep]) {
|
|
207
291
|
// get a fresh copy of the parallel steps
|
|
208
|
-
const fresh = await connection.findOne(
|
|
292
|
+
const fresh = await connection.findOne(this.#collectionName, { _id: instanceId });
|
|
209
293
|
fresh.parallelSteps[mergedState.nextStep].done = true;
|
|
210
294
|
fresh.parallelSteps[mergedState.nextStep].nextStep = nextStep;
|
|
211
295
|
//fresh.parallelSteps[mergedState.nextStep].previousStep = mergedState.previousStep || null;
|
|
212
296
|
delete fresh._id;
|
|
213
|
-
const updated = await connection.updateOne(
|
|
297
|
+
const updated = await connection.updateOne(this.#collectionName,
|
|
214
298
|
{ _id: instanceId },
|
|
215
299
|
{ $set: { ...fresh, parallelSteps: fresh.parallelSteps } });
|
|
216
300
|
//console.debug('updated', updated.parallelSteps);
|
|
@@ -237,13 +321,13 @@ class StepsEngine extends EventEmitter {
|
|
|
237
321
|
// If there is no next step, the workflow is completed
|
|
238
322
|
if (nextStep === null) {
|
|
239
323
|
delete mergedState._id;
|
|
240
|
-
const completionTime = (new Date(mergedState.updatedAt) - new Date(mergedState.createdAt))
|
|
324
|
+
const completionTime = (new Date(mergedState.updatedAt) - new Date(mergedState.createdAt));
|
|
241
325
|
|
|
242
|
-
const finalresult = await connection.updateOne(
|
|
326
|
+
const finalresult = await connection.updateOne(this.#collectionName,
|
|
243
327
|
{ _id: instanceId },
|
|
244
|
-
{ $set: { ...mergedState, nextStep: null, updatedAt: new Date().toISOString() } });
|
|
245
|
-
console.log(`Workflow ${stepsName} ${instanceId} is completed in time: ${completionTime}s 🎉`);
|
|
246
|
-
|
|
328
|
+
{ $set: { ...mergedState, nextStep: null, updatedAt: new Date().toISOString(), totalTime: completionTime } });
|
|
329
|
+
console.log(`Workflow ${stepsName} ${instanceId} is completed in time: ${completionTime / 1000}s 🎉`);
|
|
330
|
+
this.emit('completed', { ...finalresult});
|
|
247
331
|
resolve();
|
|
248
332
|
return;
|
|
249
333
|
}
|
|
@@ -255,14 +339,14 @@ class StepsEngine extends EventEmitter {
|
|
|
255
339
|
acc[step] = { done: false, startTime: now, previousStep: mergedState.previousStep };
|
|
256
340
|
return acc;
|
|
257
341
|
}, {});
|
|
258
|
-
const metadataDoc = await connection.updateOne(
|
|
342
|
+
const metadataDoc = await connection.updateOne(this.#collectionName,
|
|
259
343
|
{ _id: instanceId },
|
|
260
344
|
{ $set: { parallelSteps: metadata } });
|
|
261
345
|
//console.log('metadataDoc', metadataDoc);
|
|
262
346
|
// enqueue all steps in parallel
|
|
263
347
|
for (const step of nextStep) {
|
|
264
348
|
console.debug('enqueue step', step, instanceId);
|
|
265
|
-
await connection.enqueue(`${
|
|
349
|
+
await connection.enqueue(`${this.#queuePrefix}_${stepsName}_${step}`, {
|
|
266
350
|
stepsName: stepsName,
|
|
267
351
|
goto: step,
|
|
268
352
|
state: mergedState,
|
|
@@ -272,7 +356,7 @@ class StepsEngine extends EventEmitter {
|
|
|
272
356
|
}
|
|
273
357
|
} else {
|
|
274
358
|
console.debug('enqueue step', nextStep, instanceId);
|
|
275
|
-
await connection.enqueue(`${
|
|
359
|
+
await connection.enqueue(`${this.#queuePrefix}_${stepsName}_${nextStep}`, {
|
|
276
360
|
stepsName: stepsName,
|
|
277
361
|
goto: nextStep,
|
|
278
362
|
state: mergedState,
|
|
@@ -280,7 +364,7 @@ class StepsEngine extends EventEmitter {
|
|
|
280
364
|
instanceId: instanceId
|
|
281
365
|
});
|
|
282
366
|
}
|
|
283
|
-
|
|
367
|
+
this.emit('stepEnqueued', { workflowName: stepsName, step: nextStep, state: newState, instanceId });
|
|
284
368
|
resolve();
|
|
285
369
|
} catch (error) {
|
|
286
370
|
console.error('error', error.message);
|
|
@@ -299,35 +383,63 @@ class StepsEngine extends EventEmitter {
|
|
|
299
383
|
}
|
|
300
384
|
|
|
301
385
|
/**
|
|
302
|
-
* Register a
|
|
386
|
+
* Register the workflow with a Codehooks application
|
|
387
|
+
* @param {Codehooks} app - Codehooks application instance
|
|
388
|
+
* @returns {Promise<string>} The registered workflow name
|
|
389
|
+
*/
|
|
390
|
+
async register(app) {
|
|
391
|
+
if (!this.#name || !this.#description) {
|
|
392
|
+
throw new Error('Workflow name and description must be set before registration');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const definition = this.#definitions.get(this.#name);
|
|
396
|
+
if (!definition) {
|
|
397
|
+
throw new Error(`No workflow definition found for: ${this.#name}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return this.registerWithApp(app, this.#name, this.#description, definition);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Register a new workflow with a Codehooks application
|
|
303
405
|
* @param {Codehooks} app - Codehooks application instance
|
|
304
|
-
* @param {string} name - Unique identifier for the
|
|
406
|
+
* @param {string} name - Unique identifier for the workflow
|
|
305
407
|
* @param {string} description - Human-readable description
|
|
306
408
|
* @param {Object} definition - Object containing step definitions
|
|
307
|
-
* @returns {Promise<string>} The registered
|
|
308
|
-
* @
|
|
409
|
+
* @returns {Promise<string>} The registered workflow name
|
|
410
|
+
* @private
|
|
309
411
|
*/
|
|
310
|
-
async
|
|
311
|
-
|
|
412
|
+
async registerWithApp(app, name, description, definition) {
|
|
413
|
+
this.emit('workflowCreated', { name, description });
|
|
414
|
+
|
|
415
|
+
// Log initial state of definitions Map
|
|
416
|
+
console.debug('Before registration - Current definitions Map:', {
|
|
417
|
+
size: this.#definitions.size,
|
|
418
|
+
keys: Array.from(this.#definitions.keys()),
|
|
419
|
+
entries: Array.from(this.#definitions.entries()).map(([key, value]) => ({
|
|
420
|
+
key,
|
|
421
|
+
stepNames: Object.keys(value)
|
|
422
|
+
}))
|
|
423
|
+
});
|
|
312
424
|
|
|
313
425
|
// Validate each step in the definition
|
|
314
426
|
for (const [stepName, step] of Object.entries(definition)) {
|
|
315
427
|
try {
|
|
316
428
|
if (stepName !== undefined) {
|
|
317
|
-
|
|
318
|
-
app.worker(`${
|
|
429
|
+
console.debug('registering queue for step', `${this.#queuePrefix}_${name}_${stepName}`);
|
|
430
|
+
app.worker(`${this.#queuePrefix}_${name}_${stepName}`, async (req, res) => {
|
|
319
431
|
try {
|
|
320
432
|
const { stepsName, goto, state, instanceId, options } = req.body.payload;
|
|
321
433
|
console.debug('dequeue step', stepName, instanceId);
|
|
322
|
-
const qid = await
|
|
434
|
+
const qid = await this.handleNextStep(stepsName, goto, state, instanceId, options);
|
|
323
435
|
} catch (error) {
|
|
324
436
|
const { stepsName, goto, state, instanceId, options } = req.body.payload;
|
|
325
437
|
const connection = await Datastore.open();
|
|
326
|
-
await connection.updateOne(
|
|
438
|
+
await connection.updateOne(this.#collectionName,
|
|
327
439
|
{ _id: instanceId },
|
|
328
440
|
{ $set: { lastError: error, updatedAt: new Date().toISOString() } });
|
|
329
441
|
console.error('Error in function: ' + stepName, error);
|
|
330
|
-
|
|
442
|
+
this.emit('error', error);
|
|
331
443
|
} finally {
|
|
332
444
|
res.end();
|
|
333
445
|
}
|
|
@@ -339,7 +451,19 @@ class StepsEngine extends EventEmitter {
|
|
|
339
451
|
}
|
|
340
452
|
}
|
|
341
453
|
|
|
342
|
-
|
|
454
|
+
// Store the definition
|
|
455
|
+
this.#definitions.set(name, definition);
|
|
456
|
+
|
|
457
|
+
// Log state after registration
|
|
458
|
+
console.debug('After registration - Updated definitions Map:', {
|
|
459
|
+
size: this.#definitions.size,
|
|
460
|
+
keys: Array.from(this.#definitions.keys()),
|
|
461
|
+
entries: Array.from(this.#definitions.entries()).map(([key, value]) => ({
|
|
462
|
+
key,
|
|
463
|
+
stepNames: Object.keys(value)
|
|
464
|
+
}))
|
|
465
|
+
});
|
|
466
|
+
|
|
343
467
|
return name;
|
|
344
468
|
}
|
|
345
469
|
|
|
@@ -351,22 +475,50 @@ class StepsEngine extends EventEmitter {
|
|
|
351
475
|
* @throws {Error} If starting steps fails
|
|
352
476
|
*/
|
|
353
477
|
async start(name, initialState) {
|
|
354
|
-
|
|
478
|
+
this.emit('workflowStarted', { name, initialState });
|
|
479
|
+
|
|
480
|
+
// Log definitions Map state at start
|
|
481
|
+
console.debug('At workflow start - Current definitions Map:', {
|
|
482
|
+
size: this.#definitions.size,
|
|
483
|
+
keys: Array.from(this.#definitions.keys()),
|
|
484
|
+
entries: Array.from(this.#definitions.entries()).map(([key, value]) => ({
|
|
485
|
+
key,
|
|
486
|
+
stepNames: Object.keys(value)
|
|
487
|
+
}))
|
|
488
|
+
});
|
|
355
489
|
|
|
356
490
|
return new Promise(async (resolve, reject) => {
|
|
357
491
|
try {
|
|
358
|
-
|
|
492
|
+
console.debug('Starting workflow', name);
|
|
493
|
+
const funcs = this.#definitions.get(name);
|
|
494
|
+
console.debug('Retrieved workflow definition:', {
|
|
495
|
+
exists: !!funcs,
|
|
496
|
+
stepNames: funcs ? Object.keys(funcs) : []
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
if (!funcs) {
|
|
500
|
+
reject(new Error(`No workflow definition found for: ${name}`));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
359
504
|
const firstStepName = Object.keys(funcs)[0];
|
|
360
505
|
const firstStep = funcs[firstStepName];
|
|
506
|
+
console.debug('First step details:', {
|
|
507
|
+
name: firstStepName,
|
|
508
|
+
exists: !!firstStep,
|
|
509
|
+
type: firstStep ? typeof firstStep : 'undefined'
|
|
510
|
+
});
|
|
511
|
+
|
|
361
512
|
if (!firstStep) {
|
|
362
|
-
reject(new Error('No start step defined in
|
|
513
|
+
reject(new Error('No start step defined in workflow'));
|
|
363
514
|
return;
|
|
364
515
|
}
|
|
516
|
+
|
|
365
517
|
const connection = await Datastore.open();
|
|
366
|
-
// Create a new
|
|
367
|
-
const newState = await connection.insertOne(
|
|
368
|
-
{ ...initialState, nextStep: firstStepName, createdAt: new Date().toISOString(), workflowName: name, stepCount: {} });
|
|
369
|
-
const { _id: ID } = await connection.enqueue(`${
|
|
518
|
+
// Create a new workflow state in the database
|
|
519
|
+
const newState = await connection.insertOne(this.#collectionName,
|
|
520
|
+
{ ...initialState, nextStep: firstStepName, createdAt: new Date().toISOString(), workflowName: name, stepCount: { } });
|
|
521
|
+
const { _id: ID } = await connection.enqueue(`${this.#queuePrefix}_${name}_${firstStepName}`, {
|
|
370
522
|
stepsName: name,
|
|
371
523
|
goto: firstStepName,
|
|
372
524
|
state: newState,
|
|
@@ -375,25 +527,25 @@ class StepsEngine extends EventEmitter {
|
|
|
375
527
|
});
|
|
376
528
|
resolve(newState);
|
|
377
529
|
} catch (error) {
|
|
378
|
-
console.error('Error starting
|
|
530
|
+
console.error('Error starting workflow:', error.message);
|
|
379
531
|
reject(error);
|
|
380
532
|
}
|
|
381
533
|
});
|
|
382
534
|
}
|
|
383
535
|
|
|
384
536
|
/**
|
|
385
|
-
* Update the state of a
|
|
386
|
-
* @param {string} stepsName - Name of the
|
|
387
|
-
* @param {string} instanceId - ID of the
|
|
537
|
+
* Update the state of a workflow instance
|
|
538
|
+
* @param {string} stepsName - Name of the workflow
|
|
539
|
+
* @param {string} instanceId - ID of the workflow instance
|
|
388
540
|
* @param {Object} state - New state to update with
|
|
389
541
|
* @param {Object} options - Options for the update, { continue: false } to avoid continuing the the step
|
|
390
542
|
* @returns {Promise<Object>} The updated state
|
|
391
543
|
*/
|
|
392
544
|
async updateState(stepsName, instanceId, state, options={continue: true}) {
|
|
393
|
-
|
|
545
|
+
this.emit('stepsStateUpdating', { stepsName, instanceId, state });
|
|
394
546
|
const connection = await Datastore.open();
|
|
395
547
|
return new Promise(async (resolve, reject) => {
|
|
396
|
-
const doc = await connection.updateOne(
|
|
548
|
+
const doc = await connection.updateOne(this.#collectionName,
|
|
397
549
|
{ _id: instanceId },
|
|
398
550
|
{ $set: { ...state, updatedAt: new Date().toISOString() } });
|
|
399
551
|
if (options.continue) {
|
|
@@ -411,7 +563,7 @@ class StepsEngine extends EventEmitter {
|
|
|
411
563
|
*/
|
|
412
564
|
async setState(instanceId, { _id, state }) {
|
|
413
565
|
const connection = await Datastore.open();
|
|
414
|
-
await connection.replaceOne(
|
|
566
|
+
await connection.replaceOne(this.#collectionName, { _id: _id }, { ...state });
|
|
415
567
|
}
|
|
416
568
|
|
|
417
569
|
/**
|
|
@@ -421,22 +573,29 @@ class StepsEngine extends EventEmitter {
|
|
|
421
573
|
* @returns {Promise<{qId: string}>} Queue ID for the continued step
|
|
422
574
|
* @throws {Error} If steps instance not found
|
|
423
575
|
*/
|
|
424
|
-
async continue(stepsName, instanceId) {
|
|
576
|
+
async continue(stepsName, instanceId, reset=false) {
|
|
425
577
|
const connection = await Datastore.open();
|
|
426
|
-
const state = await connection.findOne(
|
|
578
|
+
const state = await connection.findOne(this.#collectionName, { _id: instanceId });
|
|
427
579
|
if (!state) {
|
|
428
580
|
throw new Error(`No steps found with instanceId: ${instanceId}`);
|
|
429
581
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
582
|
+
if (reset) {
|
|
583
|
+
// reset the step count
|
|
584
|
+
// update all step counts to 0
|
|
585
|
+
for (const step in state.stepCount) {
|
|
586
|
+
state.stepCount[step] = { visits: 0, startTime: new Date().toISOString() };
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
// update the step count
|
|
590
|
+
state.stepCount[state.nextStep] = { visits: 0, startTime: new Date().toISOString() };
|
|
433
591
|
}
|
|
434
|
-
|
|
592
|
+
|
|
593
|
+
await connection.updateOne(this.#collectionName, { _id: instanceId }, { $set: { stepCount: state.stepCount } });
|
|
435
594
|
console.debug('continue state', state);
|
|
436
|
-
|
|
595
|
+
this.emit('workflowContinued', { stepsName, step: state.nextStep, instanceId });
|
|
437
596
|
|
|
438
597
|
return new Promise(async (resolve, reject) => {
|
|
439
|
-
const { _id: ID } = await connection.enqueue(`${
|
|
598
|
+
const { _id: ID } = await connection.enqueue(`${this.#queuePrefix}_${stepsName}_${state.nextStep}`, {
|
|
440
599
|
stepsName,
|
|
441
600
|
goto: state.nextStep,
|
|
442
601
|
state: state,
|
|
@@ -449,21 +608,21 @@ class StepsEngine extends EventEmitter {
|
|
|
449
608
|
}
|
|
450
609
|
|
|
451
610
|
/**
|
|
452
|
-
* Continue all timed out
|
|
611
|
+
* Continue all timed out workflows instances
|
|
453
612
|
* @returns {Promise<Array<{qId: string}>>} Array of results containing queue IDs for continued workflows
|
|
454
613
|
*/
|
|
455
614
|
async continueAllTimedOut() {
|
|
456
615
|
const db = await Datastore.open();
|
|
457
|
-
const timedOutWorkflows = await db.collection(
|
|
616
|
+
const timedOutWorkflows = await db.collection(this.#collectionName).find({nextStep: {$ne: null}}).toArray();
|
|
458
617
|
const now = new Date();
|
|
459
618
|
const results = [];
|
|
460
619
|
for (const workflow of timedOutWorkflows) {
|
|
461
620
|
const createdAt = new Date(workflow.createdAt);
|
|
462
621
|
const diffMillis = now.getTime() - createdAt.getTime();
|
|
463
|
-
if (diffMillis >
|
|
622
|
+
if (diffMillis > this.#timeout) {
|
|
464
623
|
const diffMinutes = diffMillis / (1000 * 60);
|
|
465
624
|
console.log('Timed out:', workflow._id, workflow.nextStep, `(${diffMinutes.toFixed(1)} minutes old)`);
|
|
466
|
-
const result = await this.continue(workflow.workflowName, workflow._id);
|
|
625
|
+
const result = await this.continue(workflow.workflowName, workflow._id, true);
|
|
467
626
|
console.log('Continued:', result._id);
|
|
468
627
|
results.push(result);
|
|
469
628
|
}
|
|
@@ -479,7 +638,7 @@ class StepsEngine extends EventEmitter {
|
|
|
479
638
|
async getStepsStatus(id) {
|
|
480
639
|
return new Promise(async (resolve, reject) => {
|
|
481
640
|
const connection = await Datastore.open();
|
|
482
|
-
const state = await connection.findOne(
|
|
641
|
+
const state = await connection.findOne(this.#collectionName, { _id: id });
|
|
483
642
|
resolve(state);
|
|
484
643
|
});
|
|
485
644
|
}
|
|
@@ -492,8 +651,8 @@ class StepsEngine extends EventEmitter {
|
|
|
492
651
|
async getInstances(filter) {
|
|
493
652
|
return new Promise(async (resolve, reject) => {
|
|
494
653
|
const connection = await Datastore.open();
|
|
495
|
-
const states = await connection.find(
|
|
496
|
-
console.debug('listSteps',
|
|
654
|
+
const states = await connection.find(this.#collectionName, filter).toArray();
|
|
655
|
+
console.debug('listSteps', this.#collectionName, filter, states.length);
|
|
497
656
|
resolve(states);
|
|
498
657
|
});
|
|
499
658
|
}
|
|
@@ -504,21 +663,104 @@ class StepsEngine extends EventEmitter {
|
|
|
504
663
|
* @returns {Promise<Object>} The cancellation result
|
|
505
664
|
*/
|
|
506
665
|
async cancelSteps(id) {
|
|
507
|
-
|
|
666
|
+
this.emit('cancelled', { id });
|
|
508
667
|
return new Promise(async (resolve, reject) => {
|
|
509
668
|
const connection = await Datastore.open();
|
|
510
|
-
const state = await connection.updateOne(
|
|
669
|
+
const state = await connection.updateOne(this.#collectionName,
|
|
511
670
|
{ _id: id },
|
|
512
671
|
{ $set: { status: 'cancelled' } });
|
|
513
672
|
resolve(state);
|
|
514
673
|
});
|
|
515
674
|
}
|
|
516
|
-
}
|
|
517
675
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
676
|
+
/**
|
|
677
|
+
* Check if a specific step in a workflow instance has timed out
|
|
678
|
+
* @param {Object} workflow - The workflow instance
|
|
679
|
+
* @param {string} stepName - The name of the step to check (defaults to previousStep)
|
|
680
|
+
* @returns {Object} Object containing timeout status and details
|
|
681
|
+
*/
|
|
682
|
+
isStepTimedOut(workflow) {
|
|
683
|
+
// Use previousStep if no stepName provided
|
|
684
|
+
const stepToCheck = workflow.nextStep;
|
|
685
|
+
|
|
686
|
+
if (!stepToCheck || !workflow.stepCount || !workflow.stepCount[stepToCheck]) {
|
|
687
|
+
console.debug('no step', stepToCheck, workflow.stepCount);
|
|
688
|
+
return {
|
|
689
|
+
isTimedOut: false,
|
|
690
|
+
reason: 'Step not found in workflow',
|
|
691
|
+
step: stepToCheck
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const step = workflow.stepCount[stepToCheck];
|
|
696
|
+
|
|
697
|
+
// Get the timeout value for this step
|
|
698
|
+
// First try step-specific config, then default options, finally fallback to global timeout
|
|
699
|
+
const stepConfig = this.#steps[stepToCheck];
|
|
700
|
+
const stepTimeout = stepConfig?.timeout ?? this.#defaultStepOptions.timeout ?? this.#timeout;
|
|
701
|
+
|
|
702
|
+
// If the step hasn't finished, check if it's been running too long
|
|
703
|
+
console.debug('isStepTimedOut', stepToCheck, stepTimeout);
|
|
704
|
+
if (!step.finishTime) {
|
|
705
|
+
const startTime = new Date(step.startTime);
|
|
706
|
+
const now = new Date();
|
|
707
|
+
const runningTime = now.getTime() - startTime.getTime();
|
|
708
|
+
console.debug('runningTime', runningTime, stepTimeout);
|
|
709
|
+
return {
|
|
710
|
+
isTimedOut: runningTime > stepTimeout,
|
|
711
|
+
runningTime,
|
|
712
|
+
timeout: stepTimeout,
|
|
713
|
+
step: stepToCheck,
|
|
714
|
+
startTime: step.startTime,
|
|
715
|
+
currentTime: now.toISOString(),
|
|
716
|
+
timeoutSource: stepConfig ? 'stepConfig' : (this.#defaultStepOptions.timeout ? 'defaultOptions' : 'globalTimeout')
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// If the step has finished, check if it took too long
|
|
721
|
+
const startTime = new Date(step.startTime);
|
|
722
|
+
const finishTime = new Date(step.finishTime);
|
|
723
|
+
const executionTime = finishTime.getTime() - startTime.getTime();
|
|
724
|
+
console.debug('executionTime', executionTime, stepTimeout);
|
|
725
|
+
return {
|
|
726
|
+
isTimedOut: executionTime > stepTimeout,
|
|
727
|
+
executionTime,
|
|
728
|
+
timeout: stepTimeout,
|
|
729
|
+
step: stepToCheck,
|
|
730
|
+
startTime: step.startTime,
|
|
731
|
+
finishTime: step.finishTime,
|
|
732
|
+
timeoutSource: stepConfig ? 'stepConfig' : (this.#defaultStepOptions.timeout ? 'defaultOptions' : 'globalTimeout')
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Find all workflow instances with timed out steps
|
|
738
|
+
* @param {Object} filter - Optional filter criteria for workflows
|
|
739
|
+
* @returns {Promise<Array<Object>>} Array of workflow instances with timed out steps
|
|
740
|
+
*/
|
|
741
|
+
async findTimedOutSteps(filter = {}) {
|
|
742
|
+
const db = await Datastore.open();
|
|
743
|
+
const workflows = await db.getMany(this.#collectionName, {"nextStep": {$ne: null}}).toArray();
|
|
744
|
+
if (workflows.length > 0) {
|
|
745
|
+
console.debug('TimedOutSteps', workflows.length);
|
|
746
|
+
}
|
|
747
|
+
const timedOutWorkflows = workflows.map(workflow => {
|
|
748
|
+
console.debug('isStepTimedOut', workflow.nextStep);
|
|
749
|
+
const timeoutStatus = this.isStepTimedOut(workflow);
|
|
750
|
+
if (timeoutStatus.isTimedOut) {
|
|
751
|
+
return {
|
|
752
|
+
workflowId: workflow._id,
|
|
753
|
+
workflowName: workflow.workflowName,
|
|
754
|
+
...timeoutStatus
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
}).filter(Boolean);
|
|
759
|
+
|
|
760
|
+
return timedOutWorkflows;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
521
763
|
|
|
522
|
-
// Export the singleton instance
|
|
523
|
-
export
|
|
524
|
-
export default
|
|
764
|
+
// Export the class directly instead of a singleton instance
|
|
765
|
+
export { Workflow };
|
|
766
|
+
export default Workflow;
|
package/workflow/index.mjs
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
// Re-export the static methods
|
|
4
|
-
export const StepsConfig = {
|
|
5
|
-
setCollectionName,
|
|
6
|
-
setQueuePrefix
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
// Re-export the configure method
|
|
10
|
-
export const configure = StepsEngine.configure;
|
|
11
|
-
|
|
12
|
-
// Re-export the workflow instance
|
|
13
|
-
export { Steps };
|
|
1
|
+
import Workflow from './engine.mjs';
|
|
14
2
|
|
|
15
3
|
// Export the engine as default
|
|
16
|
-
export default
|
|
4
|
+
export default Workflow;
|