codehooks-js 1.3.8 → 1.3.10

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 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 {Steps as Workflow, StepsConfig as WorkflowConfig} from './workflow/index.mjs';
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.register(this, name, description, steps, options);
122
- return Workflow;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehooks-js",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
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
@@ -1032,21 +1032,29 @@ export class Codehooks {
1032
1032
  };
1033
1033
  addListener: (observer: any) => void;
1034
1034
  /**
1035
- * Create a new steps workflow
1036
- * @param name - Unique identifier for the steps workflow
1037
- * @param description - Human-readable description of the steps workflow
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 WorkflowEngine instance for managing the workflow
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
- createWorkflow: (name: string, description: string, steps: StepsDefinition, options?: any) => WorkflowEngine;
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 StepsEvents = {
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
- 'stepContinued': { workflowName: string; step: string; instanceId: string };
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 steps in a workflow
1114
+ * Definition of a workflow step function
1101
1115
  */
1102
- export type StepsDefinition = Record<string, (state: any, goto: (step: string | string[] | null, state: any) => void) => void | Promise<void>>;
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
- * Engine for managing step-based workflows
1119
+ * Configuration options for a workflow step
1106
1120
  */
1107
- export type StepsEngine = {
1108
- /**
1109
- * Configure the workflow engine
1110
- * @param options - Configuration options
1111
- */
1112
- configure: (options: any) => void;
1113
- } & {
1114
- /**
1115
- * Get the singleton instance
1116
- */
1117
- getInstance: () => Steps;
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 step-based workflows
1122
- * @extends Steps
1145
+ * Engine for managing workflow-based applications
1146
+ * @extends Workflow
1123
1147
  */
1124
- export type WorkflowEngine = Steps & {
1148
+ export type WorkflowEngine = Workflow & {
1125
1149
  /**
1126
- * Register a new steps workflow
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: StepsDefinition) => Promise<string>;
1155
+ register: (name: string, description: string, steps: WorkflowDefinition) => Promise<string>;
1132
1156
  };
1133
1157
 
1134
1158
  /**
1135
- * Steps workflow engine for managing step-based workflows
1159
+ * Workflow engine for managing step-based applications
1136
1160
  * @extends EventEmitter
1137
1161
  */
1138
- export type Steps = {
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: any) => void;
1185
+ configure: (options: WorkflowConfig) => void;
1144
1186
 
1145
1187
  /**
1146
- * Register a new steps workflow
1188
+ * Register a new workflow
1147
1189
  * @param app - Codehooks application instance
1148
- * @param name - Unique identifier for the steps workflow
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 steps name
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 steps instance
1157
- * @param name - Name of the steps workflow to start
1158
- * @param initialState - Initial state for the steps instance
1159
- * @returns Promise with the steps instance ID
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 steps instance
1165
- * @param stepsName - Name of the steps workflow
1166
- * @param instanceId - ID of the steps instance
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 the step
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: (stepsName: string, instanceId: string, state: any, options?: any) => Promise<any>;
1213
+ updateState: (workflowName: string, instanceId: string, state: any, options?: UpdateOptions) => Promise<any>;
1172
1214
 
1173
1215
  /**
1174
- * Continue a paused steps instance
1175
- * @param stepsName - Name of the steps workflow
1176
- * @param instanceId - ID of the steps instance
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: (stepsName: string, instanceId: string) => Promise<{ qId: string }>;
1222
+ continue: (workflowName: string, instanceId: string, reset?: boolean) => Promise<{ qId: string }>;
1180
1223
 
1181
1224
  /**
1182
- * Get the status of a steps instance
1183
- * @param id - ID of the steps instance
1184
- * @returns Promise with the steps status
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
- getStepsStatus: (id: string) => Promise<any>;
1229
+ getWorkflowStatus: (id: string) => Promise<any>;
1187
1230
 
1188
1231
  /**
1189
- * Get all steps instances matching a filter
1190
- * @param filter - Filter criteria for steps workflows
1191
- * @returns Promise with list of steps instances
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 steps instance
1197
- * @param id - ID of the steps instance to cancel
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
- cancelSteps: (id: string) => Promise<any>;
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
- * Steps.on('stepStarted', ({ stepsName, step, state, instanceId }) => {
1208
- * console.log(`Step ${step} started in workflow ${stepsName}`);
1250
+ * workflow.on('stepStarted', ({ workflowName, step, state, instanceId }) => {
1251
+ * console.log(`Step ${step} started in workflow ${workflowName}`);
1209
1252
  * });
1210
1253
  */
1211
- on: (event: string, listener: (data: any) => void) => Steps;
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: string, listener: (data: any) => void) => Steps;
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: string, listener: (data: any) => void) => Steps;
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: string, data: any) => boolean;
1275
+ emit: (event: WorkflowEvent, data: WorkflowEventData) => boolean;
1233
1276
 
1234
1277
  /**
1235
- * Continue all timed out steps instances
1236
- * @returns {Promise<Array<{qId: string}>>} Array of results containing queue IDs for continued workflows
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
- * Configuration for the Steps workflow engine
1352
+ * Options for updating workflow state
1243
1353
  */
1244
- export type StepsConfig = {
1245
- /**
1246
- * Set the collection name for storing steps data
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
- export const stepsconfig: StepsConfig;
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
- * Configure the workflow engine
1262
- * @param options - Configuration options
1374
+ * Data structure for workflow events
1263
1375
  */
1264
- export function configure(options: any): void;
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
@@ -1,40 +1,76 @@
1
1
  /*
2
- Implements a steps 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 step-based workflows.
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
- * StepsEngine class that manages step-based workflows
9
+ * Workflow class that manages step-based workflows
10
10
  * @extends EventEmitter
11
11
  */
12
- class StepsEngine extends EventEmitter {
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
- constructor() {
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.definitions = new Map();
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
- // Singleton instance
20
- static instance = null;
21
-
22
- // Configuration
23
- static collectionName = 'workflowdata';
24
- static queuePrefix = 'workflowqueue';
25
- static timeout = 30000;
26
- static maxStepCount = 3;
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
- static setCollectionName(name) {
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
- StepsEngine.collectionName = name.trim();
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
- static setQueuePrefix(prefix) {
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
- StepsEngine.queuePrefix = prefix.trim();
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
- static setTimeout(timeout) {
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
- StepsEngine.timeout = timeout;
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
- static setMaxStepCount(maxStepCount) {
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
- StepsEngine.maxStepCount = maxStepCount;
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
- StepsEngine.setCollectionName(config.collectionName);
167
+ this.setCollectionName(config.collectionName);
86
168
  }
87
169
  if (config.queuePrefix) {
88
- StepsEngine.setQueuePrefix(config.queuePrefix);
170
+ this.setQueuePrefix(config.queuePrefix);
89
171
  }
90
172
  if (config.timeout) {
91
- StepsEngine.setTimeout(config.timeout);
173
+ this.setTimeout(config.timeout);
92
174
  }
93
175
  if (config.maxStepCount) {
94
- StepsEngine.setMaxStepCount(config.maxStepCount);
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.definitions.get(stepsName);
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
- StepsEngine.getInstance().emit('stepStarted', { workflowName: stepsName, step: nextStep, state: newState, instanceId });
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
- newState.stepCount[nextStep] = (newState.stepCount[nextStep] || 0) + 1;
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(StepsEngine.collectionName,
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
- StepsEngine.getInstance().emit('stateUpdated', { workflowName: stepsName, state: newState, instanceId });
250
+ this.emit('stateUpdated', { workflowName: stepsName, state: newState, instanceId });
171
251
 
172
252
  try {
173
253
  // Get the next step function
174
- const func = StepsEngine.getInstance().getDefinition(stepsName, nextStep);
254
+ const func = this.getDefinition(stepsName, nextStep);
175
255
 
176
256
  // Wrap the callback in a Promise to ensure proper async handling
177
- await new Promise(async (resolve, reject) => {
257
+ return 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] > 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}`));
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(StepsEngine.collectionName, { _id: instanceId });
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(StepsEngine.collectionName,
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)) / 1000;
324
+ const completionTime = (new Date(mergedState.updatedAt) - new Date(mergedState.createdAt));
241
325
 
242
- const finalresult = await connection.updateOne(StepsEngine.collectionName,
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
- StepsEngine.getInstance().emit('completed', { ...finalresult});
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(StepsEngine.collectionName,
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(`${StepsEngine.queuePrefix}_${stepsName}_${step}`, {
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(`${StepsEngine.queuePrefix}_${stepsName}_${nextStep}`, {
359
+ await connection.enqueue(`${this.#queuePrefix}_${stepsName}_${nextStep}`, {
276
360
  stepsName: stepsName,
277
361
  goto: nextStep,
278
362
  state: mergedState,
@@ -280,13 +364,14 @@ class StepsEngine extends EventEmitter {
280
364
  instanceId: instanceId
281
365
  });
282
366
  }
283
- StepsEngine.getInstance().emit('stepEnqueued', { workflowName: stepsName, step: nextStep, state: newState, instanceId });
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);
287
371
  reject(error);
288
372
  }
289
373
  });
374
+ resolve();
290
375
  } catch (error) {
291
376
  console.error('Error executing step function:', error);
292
377
  reject(error);
@@ -299,36 +384,67 @@ class StepsEngine extends EventEmitter {
299
384
  }
300
385
 
301
386
  /**
302
- * Register a new steps workflow
387
+ * Register the workflow with a Codehooks application
388
+ * @param {Codehooks} app - Codehooks application instance
389
+ * @returns {Promise<string>} The registered workflow name
390
+ */
391
+ async register(app) {
392
+ if (!this.#name || !this.#description) {
393
+ throw new Error('Workflow name and description must be set before registration');
394
+ }
395
+
396
+ const definition = this.#definitions.get(this.#name);
397
+ if (!definition) {
398
+ throw new Error(`No workflow definition found for: ${this.#name}`);
399
+ }
400
+
401
+ return this.registerWithApp(app, this.#name, this.#description, definition);
402
+ }
403
+
404
+ /**
405
+ * Register a new workflow with a Codehooks application
303
406
  * @param {Codehooks} app - Codehooks application instance
304
- * @param {string} name - Unique identifier for the steps workflow
407
+ * @param {string} name - Unique identifier for the workflow
305
408
  * @param {string} description - Human-readable description
306
409
  * @param {Object} definition - Object containing step definitions
307
- * @returns {Promise<string>} The registered steps name
308
- * @throws {Error} If step definition is invalid
410
+ * @returns {Promise<string>} The registered workflow name
411
+ * @private
309
412
  */
310
- async register(app, name, description, definition) {
311
- StepsEngine.getInstance().emit('workflowCreated', { name, description });
413
+ async registerWithApp(app, name, description, definition) {
414
+ this.emit('workflowCreated', { name, description });
415
+
416
+ // Log initial state of definitions Map
417
+ console.debug('Before registration - Current definitions Map:', {
418
+ size: this.#definitions.size,
419
+ keys: Array.from(this.#definitions.keys()),
420
+ entries: Array.from(this.#definitions.entries()).map(([key, value]) => ({
421
+ key,
422
+ stepNames: Object.keys(value)
423
+ }))
424
+ });
312
425
 
313
426
  // Validate each step in the definition
314
427
  for (const [stepName, step] of Object.entries(definition)) {
315
428
  try {
316
429
  if (stepName !== undefined) {
317
- //console.log('registering queue for step', `${StepsEngine.queuePrefix}_${name}_${stepName}`);
318
- app.worker(`${StepsEngine.queuePrefix}_${name}_${stepName}`, async function(req, res) {
430
+ console.debug('registering queue for step', `${this.#queuePrefix}_${name}_${stepName}`);
431
+ app.worker(`${this.#queuePrefix}_${name}_${stepName}`, async (req, res) => {
319
432
  try {
320
433
  const { stepsName, goto, state, instanceId, options } = req.body.payload;
321
434
  console.debug('dequeue step', stepName, instanceId);
322
- const qid = await StepsEngine.getInstance().handleNextStep(stepsName, goto, state, instanceId, options);
435
+ const qid = await this.handleNextStep(stepsName, goto, state, instanceId, options);
436
+ //console.debug('Worker qid', qid);
437
+ //res.end();
323
438
  } catch (error) {
324
439
  const { stepsName, goto, state, instanceId, options } = req.body.payload;
325
440
  const connection = await Datastore.open();
326
- await connection.updateOne(StepsEngine.collectionName,
441
+ await connection.updateOne(this.#collectionName,
327
442
  { _id: instanceId },
328
443
  { $set: { lastError: error, updatedAt: new Date().toISOString() } });
329
444
  console.error('Error in function: ' + stepName, error);
330
- StepsEngine.getInstance().emit('error', error);
445
+ this.emit('error', error);
331
446
  } finally {
447
+ //console.debug('Worker res.end', stepName);
332
448
  res.end();
333
449
  }
334
450
  });
@@ -339,7 +455,19 @@ class StepsEngine extends EventEmitter {
339
455
  }
340
456
  }
341
457
 
342
- this.definitions.set(name, definition);
458
+ // Store the definition
459
+ this.#definitions.set(name, definition);
460
+
461
+ // Log state after registration
462
+ console.debug('After registration - Updated definitions Map:', {
463
+ size: this.#definitions.size,
464
+ keys: Array.from(this.#definitions.keys()),
465
+ entries: Array.from(this.#definitions.entries()).map(([key, value]) => ({
466
+ key,
467
+ stepNames: Object.keys(value)
468
+ }))
469
+ });
470
+
343
471
  return name;
344
472
  }
345
473
 
@@ -351,22 +479,50 @@ class StepsEngine extends EventEmitter {
351
479
  * @throws {Error} If starting steps fails
352
480
  */
353
481
  async start(name, initialState) {
354
- StepsEngine.getInstance().emit('workflowStarted', { name, initialState });
482
+ this.emit('workflowStarted', { name, initialState });
483
+
484
+ // Log definitions Map state at start
485
+ console.debug('At workflow start - Current definitions Map:', {
486
+ size: this.#definitions.size,
487
+ keys: Array.from(this.#definitions.keys()),
488
+ entries: Array.from(this.#definitions.entries()).map(([key, value]) => ({
489
+ key,
490
+ stepNames: Object.keys(value)
491
+ }))
492
+ });
355
493
 
356
494
  return new Promise(async (resolve, reject) => {
357
495
  try {
358
- const funcs = this.definitions.get(name);
496
+ console.debug('Starting workflow', name);
497
+ const funcs = this.#definitions.get(name);
498
+ console.debug('Retrieved workflow definition:', {
499
+ exists: !!funcs,
500
+ stepNames: funcs ? Object.keys(funcs) : []
501
+ });
502
+
503
+ if (!funcs) {
504
+ reject(new Error(`No workflow definition found for: ${name}`));
505
+ return;
506
+ }
507
+
359
508
  const firstStepName = Object.keys(funcs)[0];
360
509
  const firstStep = funcs[firstStepName];
510
+ console.debug('First step details:', {
511
+ name: firstStepName,
512
+ exists: !!firstStep,
513
+ type: firstStep ? typeof firstStep : 'undefined'
514
+ });
515
+
361
516
  if (!firstStep) {
362
- reject(new Error('No start step defined in steps'));
517
+ reject(new Error('No start step defined in workflow'));
363
518
  return;
364
519
  }
520
+
365
521
  const connection = await Datastore.open();
366
- // Create a new steps state in the database
367
- const newState = await connection.insertOne(StepsEngine.collectionName,
368
- { ...initialState, nextStep: firstStepName, createdAt: new Date().toISOString(), workflowName: name, stepCount: {} });
369
- const { _id: ID } = await connection.enqueue(`${StepsEngine.queuePrefix}_${name}_${firstStepName}`, {
522
+ // Create a new workflow state in the database
523
+ const newState = await connection.insertOne(this.#collectionName,
524
+ { ...initialState, nextStep: firstStepName, createdAt: new Date().toISOString(), workflowName: name, stepCount: { } });
525
+ const { _id: ID } = await connection.enqueue(`${this.#queuePrefix}_${name}_${firstStepName}`, {
370
526
  stepsName: name,
371
527
  goto: firstStepName,
372
528
  state: newState,
@@ -375,29 +531,29 @@ class StepsEngine extends EventEmitter {
375
531
  });
376
532
  resolve(newState);
377
533
  } catch (error) {
378
- console.error('Error starting steps:', error.message);
534
+ console.error('Error starting workflow:', error.message);
379
535
  reject(error);
380
536
  }
381
537
  });
382
538
  }
383
539
 
384
540
  /**
385
- * Update the state of a steps instance
386
- * @param {string} stepsName - Name of the steps workflow
387
- * @param {string} instanceId - ID of the steps instance
541
+ * Update the state of a workflow instance
542
+ * @param {string} stepsName - Name of the workflow
543
+ * @param {string} instanceId - ID of the workflow instance
388
544
  * @param {Object} state - New state to update with
389
545
  * @param {Object} options - Options for the update, { continue: false } to avoid continuing the the step
390
546
  * @returns {Promise<Object>} The updated state
391
547
  */
392
548
  async updateState(stepsName, instanceId, state, options={continue: true}) {
393
- StepsEngine.getInstance().emit('stepsStateUpdating', { stepsName, instanceId, state });
549
+ this.emit('stepsStateUpdating', { stepsName, instanceId, state });
394
550
  const connection = await Datastore.open();
395
551
  return new Promise(async (resolve, reject) => {
396
- const doc = await connection.updateOne(StepsEngine.collectionName,
552
+ const doc = await connection.updateOne(this.#collectionName,
397
553
  { _id: instanceId },
398
554
  { $set: { ...state, updatedAt: new Date().toISOString() } });
399
555
  if (options.continue) {
400
- await this.continue(stepsName, instanceId);
556
+ await this.continue(stepsName, instanceId, false);
401
557
  }
402
558
  resolve({ ...doc });
403
559
  });
@@ -411,7 +567,7 @@ class StepsEngine extends EventEmitter {
411
567
  */
412
568
  async setState(instanceId, { _id, state }) {
413
569
  const connection = await Datastore.open();
414
- await connection.replaceOne(StepsEngine.collectionName, { _id: _id }, { ...state });
570
+ await connection.replaceOne(this.#collectionName, { _id: _id }, { ...state });
415
571
  }
416
572
 
417
573
  /**
@@ -421,22 +577,29 @@ class StepsEngine extends EventEmitter {
421
577
  * @returns {Promise<{qId: string}>} Queue ID for the continued step
422
578
  * @throws {Error} If steps instance not found
423
579
  */
424
- async continue(stepsName, instanceId) {
580
+ async continue(stepsName, instanceId, reset=false) {
425
581
  const connection = await Datastore.open();
426
- const state = await connection.findOne(StepsEngine.collectionName, { _id: instanceId });
582
+ const state = await connection.findOne(this.#collectionName, { _id: instanceId });
427
583
  if (!state) {
428
584
  throw new Error(`No steps found with instanceId: ${instanceId}`);
429
585
  }
430
- // update all step counts to 0
431
- for (const step in state.stepCount) {
432
- state.stepCount[step] = 0;
586
+ if (reset) {
587
+ // reset the step count
588
+ // update all step counts to 0
589
+ for (const step in state.stepCount) {
590
+ state.stepCount[step] = { visits: 0, startTime: new Date().toISOString() };
591
+ }
592
+ } else {
593
+ // update the step count
594
+ state.stepCount[state.nextStep] = { visits: 0, startTime: new Date().toISOString() };
433
595
  }
434
- await connection.updateOne(StepsEngine.collectionName, { _id: instanceId }, { $set: { stepCount: state.stepCount } });
596
+
597
+ await connection.updateOne(this.#collectionName, { _id: instanceId }, { $set: { stepCount: state.stepCount } });
435
598
  console.debug('continue state', state);
436
- StepsEngine.getInstance().emit('workflowContinued', { stepsName, step: state.nextStep, instanceId });
599
+ this.emit('workflowContinued', { stepsName, step: state.nextStep, instanceId });
437
600
 
438
601
  return new Promise(async (resolve, reject) => {
439
- const { _id: ID } = await connection.enqueue(`${StepsEngine.queuePrefix}_${stepsName}_${state.nextStep}`, {
602
+ const { _id: ID } = await connection.enqueue(`${this.#queuePrefix}_${stepsName}_${state.nextStep}`, {
440
603
  stepsName,
441
604
  goto: state.nextStep,
442
605
  state: state,
@@ -449,21 +612,21 @@ class StepsEngine extends EventEmitter {
449
612
  }
450
613
 
451
614
  /**
452
- * Continue all timed out steps instances
615
+ * Continue all timed out workflows instances
453
616
  * @returns {Promise<Array<{qId: string}>>} Array of results containing queue IDs for continued workflows
454
617
  */
455
618
  async continueAllTimedOut() {
456
619
  const db = await Datastore.open();
457
- const timedOutWorkflows = await db.collection(StepsEngine.collectionName).find({nextStep: {$ne: null}}).toArray();
620
+ const timedOutWorkflows = await db.collection(this.#collectionName).find({nextStep: {$ne: null}}).toArray();
458
621
  const now = new Date();
459
622
  const results = [];
460
623
  for (const workflow of timedOutWorkflows) {
461
624
  const createdAt = new Date(workflow.createdAt);
462
625
  const diffMillis = now.getTime() - createdAt.getTime();
463
- if (diffMillis > StepsEngine.timeout) {
626
+ if (diffMillis > this.#timeout) {
464
627
  const diffMinutes = diffMillis / (1000 * 60);
465
628
  console.log('Timed out:', workflow._id, workflow.nextStep, `(${diffMinutes.toFixed(1)} minutes old)`);
466
- const result = await this.continue(workflow.workflowName, workflow._id);
629
+ const result = await this.continue(workflow.workflowName, workflow._id, true);
467
630
  console.log('Continued:', result._id);
468
631
  results.push(result);
469
632
  }
@@ -479,7 +642,7 @@ class StepsEngine extends EventEmitter {
479
642
  async getStepsStatus(id) {
480
643
  return new Promise(async (resolve, reject) => {
481
644
  const connection = await Datastore.open();
482
- const state = await connection.findOne(StepsEngine.collectionName, { _id: id });
645
+ const state = await connection.findOne(this.#collectionName, { _id: id });
483
646
  resolve(state);
484
647
  });
485
648
  }
@@ -492,8 +655,8 @@ class StepsEngine extends EventEmitter {
492
655
  async getInstances(filter) {
493
656
  return new Promise(async (resolve, reject) => {
494
657
  const connection = await Datastore.open();
495
- const states = await connection.find(StepsEngine.collectionName, filter).toArray();
496
- console.debug('listSteps', StepsEngine.collectionName, filter, states.length);
658
+ const states = await connection.find(this.#collectionName, filter).toArray();
659
+ console.debug('listSteps', this.#collectionName, filter, states.length);
497
660
  resolve(states);
498
661
  });
499
662
  }
@@ -504,21 +667,104 @@ class StepsEngine extends EventEmitter {
504
667
  * @returns {Promise<Object>} The cancellation result
505
668
  */
506
669
  async cancelSteps(id) {
507
- StepsEngine.getInstance().emit('cancelled', { id });
670
+ this.emit('cancelled', { id });
508
671
  return new Promise(async (resolve, reject) => {
509
672
  const connection = await Datastore.open();
510
- const state = await connection.updateOne(StepsEngine.collectionName,
673
+ const state = await connection.updateOne(this.#collectionName,
511
674
  { _id: id },
512
675
  { $set: { status: 'cancelled' } });
513
676
  resolve(state);
514
677
  });
515
678
  }
516
- }
517
679
 
518
- // Export the static methods directly
519
- export const setCollectionName = StepsEngine.setCollectionName.bind(StepsEngine);
520
- export const setQueuePrefix = StepsEngine.setQueuePrefix.bind(StepsEngine);
680
+ /**
681
+ * Check if a specific step in a workflow instance has timed out
682
+ * @param {Object} workflow - The workflow instance
683
+ * @param {string} stepName - The name of the step to check (defaults to previousStep)
684
+ * @returns {Object} Object containing timeout status and details
685
+ */
686
+ isStepTimedOut(workflow) {
687
+ // Use previousStep if no stepName provided
688
+ const stepToCheck = workflow.nextStep;
689
+
690
+ if (!stepToCheck || !workflow.stepCount || !workflow.stepCount[stepToCheck]) {
691
+ console.debug('no step', stepToCheck, workflow.stepCount);
692
+ return {
693
+ isTimedOut: false,
694
+ reason: 'Step not found in workflow',
695
+ step: stepToCheck
696
+ };
697
+ }
698
+
699
+ const step = workflow.stepCount[stepToCheck];
700
+
701
+ // Get the timeout value for this step
702
+ // First try step-specific config, then default options, finally fallback to global timeout
703
+ const stepConfig = this.#steps[stepToCheck];
704
+ const stepTimeout = stepConfig?.timeout ?? this.#defaultStepOptions.timeout ?? this.#timeout;
705
+
706
+ // If the step hasn't finished, check if it's been running too long
707
+ console.debug('isStepTimedOut', stepToCheck, stepTimeout);
708
+ if (!step.finishTime) {
709
+ const startTime = new Date(step.startTime);
710
+ const now = new Date();
711
+ const runningTime = now.getTime() - startTime.getTime();
712
+ console.debug('runningTime', runningTime, stepTimeout);
713
+ return {
714
+ isTimedOut: runningTime > stepTimeout,
715
+ runningTime,
716
+ timeout: stepTimeout,
717
+ step: stepToCheck,
718
+ startTime: step.startTime,
719
+ currentTime: now.toISOString(),
720
+ timeoutSource: stepConfig ? 'stepConfig' : (this.#defaultStepOptions.timeout ? 'defaultOptions' : 'globalTimeout')
721
+ };
722
+ }
723
+
724
+ // If the step has finished, check if it took too long
725
+ const startTime = new Date(step.startTime);
726
+ const finishTime = new Date(step.finishTime);
727
+ const executionTime = finishTime.getTime() - startTime.getTime();
728
+ console.debug('executionTime', executionTime, stepTimeout);
729
+ return {
730
+ isTimedOut: executionTime > stepTimeout,
731
+ executionTime,
732
+ timeout: stepTimeout,
733
+ step: stepToCheck,
734
+ startTime: step.startTime,
735
+ finishTime: step.finishTime,
736
+ timeoutSource: stepConfig ? 'stepConfig' : (this.#defaultStepOptions.timeout ? 'defaultOptions' : 'globalTimeout')
737
+ };
738
+ }
739
+
740
+ /**
741
+ * Find all workflow instances with timed out steps
742
+ * @param {Object} filter - Optional filter criteria for workflows
743
+ * @returns {Promise<Array<Object>>} Array of workflow instances with timed out steps
744
+ */
745
+ async findTimedOutSteps(filter = {}) {
746
+ const db = await Datastore.open();
747
+ const workflows = await db.getMany(this.#collectionName, {"nextStep": {$ne: null}}).toArray();
748
+ if (workflows.length > 0) {
749
+ console.debug('TimedOutSteps', workflows.length);
750
+ }
751
+ const timedOutWorkflows = workflows.map(workflow => {
752
+ console.debug('isStepTimedOut', workflow.nextStep);
753
+ const timeoutStatus = this.isStepTimedOut(workflow);
754
+ if (timeoutStatus.isTimedOut) {
755
+ return {
756
+ workflowId: workflow._id,
757
+ workflowName: workflow.workflowName,
758
+ ...timeoutStatus
759
+ };
760
+ }
761
+ return null;
762
+ }).filter(Boolean);
763
+
764
+ return timedOutWorkflows;
765
+ }
766
+ }
521
767
 
522
- // Export the singleton instance
523
- export const Steps = StepsEngine.getInstance();
524
- export default StepsEngine.getInstance();
768
+ // Export the class directly instead of a singleton instance
769
+ export { Workflow };
770
+ export default Workflow;
@@ -1,16 +1,4 @@
1
- import StepsEngine, { setCollectionName, setQueuePrefix, Steps } from './engine.mjs';
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 StepsEngine;
4
+ export default Workflow;