@spfn/workflow 0.1.0-alpha.2

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/dist/index.js ADDED
@@ -0,0 +1,824 @@
1
+ import { eq, and, desc, inArray } from 'drizzle-orm';
2
+ import { Value } from '@sinclair/typebox/value';
3
+ import { createSchema, timestamps } from '@spfn/core/db';
4
+ import { timestamp, text, integer, jsonb, index } from 'drizzle-orm/pg-core';
5
+
6
+ // src/builder/workflow-builder.ts
7
+ var WorkflowBuilder = class _WorkflowBuilder {
8
+ _name;
9
+ _inputSchema;
10
+ _steps = [];
11
+ _resumable = false;
12
+ _rollbackEnabled = true;
13
+ _notifyConfigs = [];
14
+ constructor(name) {
15
+ this._name = name;
16
+ }
17
+ /**
18
+ * Define input schema
19
+ */
20
+ input(schema) {
21
+ const builder = new _WorkflowBuilder(this._name);
22
+ builder._inputSchema = schema;
23
+ builder._steps = this._steps;
24
+ builder._resumable = this._resumable;
25
+ builder._rollbackEnabled = this._rollbackEnabled;
26
+ builder._notifyConfigs = [...this._notifyConfigs];
27
+ return builder;
28
+ }
29
+ /**
30
+ * Add a sequential step
31
+ */
32
+ pipe(job, mapper) {
33
+ const stepName = job.name;
34
+ const step = {
35
+ name: stepName,
36
+ job,
37
+ mapper,
38
+ type: "sequential"
39
+ };
40
+ const builder = new _WorkflowBuilder(this._name);
41
+ builder._inputSchema = this._inputSchema;
42
+ builder._steps = [...this._steps, step];
43
+ builder._resumable = this._resumable;
44
+ builder._rollbackEnabled = this._rollbackEnabled;
45
+ builder._notifyConfigs = [...this._notifyConfigs];
46
+ return builder;
47
+ }
48
+ /**
49
+ * Add parallel steps
50
+ */
51
+ parallel(steps) {
52
+ const parallelGroup = `parallel_${this._steps.length}`;
53
+ const parallelSteps = [];
54
+ for (const [name, [job, mapper]] of Object.entries(steps)) {
55
+ parallelSteps.push({
56
+ name,
57
+ job,
58
+ mapper,
59
+ type: "parallel",
60
+ parallelGroup
61
+ });
62
+ }
63
+ const builder = new _WorkflowBuilder(this._name);
64
+ builder._inputSchema = this._inputSchema;
65
+ builder._steps = [...this._steps, ...parallelSteps];
66
+ builder._resumable = this._resumable;
67
+ builder._rollbackEnabled = this._rollbackEnabled;
68
+ builder._notifyConfigs = [...this._notifyConfigs];
69
+ return builder;
70
+ }
71
+ /**
72
+ * Enable/disable resumable (restart from failure point)
73
+ */
74
+ resumable(enabled = true) {
75
+ this._resumable = enabled;
76
+ return this;
77
+ }
78
+ /**
79
+ * Enable/disable rollback on failure
80
+ */
81
+ rollback(enabled = true) {
82
+ this._rollbackEnabled = enabled;
83
+ return this;
84
+ }
85
+ /**
86
+ * Configure notifications (chainable — each call adds a separate config)
87
+ */
88
+ notify(config) {
89
+ this._notifyConfigs.push(config);
90
+ return this;
91
+ }
92
+ /**
93
+ * Build the workflow definition
94
+ */
95
+ build() {
96
+ return {
97
+ name: this._name,
98
+ inputSchema: this._inputSchema,
99
+ steps: this._steps,
100
+ resumable: this._resumable,
101
+ rollbackEnabled: this._rollbackEnabled,
102
+ notifyConfigs: this._notifyConfigs,
103
+ _input: void 0
104
+ };
105
+ }
106
+ };
107
+ function workflow(name) {
108
+ return new WorkflowBuilder(name);
109
+ }
110
+ var workflowSchema = createSchema("@spfn/workflow");
111
+ var workflowExecutions = workflowSchema.table(
112
+ "executions",
113
+ {
114
+ id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
115
+ /**
116
+ * Workflow name (identifier)
117
+ */
118
+ workflowName: text("workflow_name").notNull(),
119
+ /**
120
+ * Execution status
121
+ */
122
+ status: text("status").$type().notNull().default("pending"),
123
+ /**
124
+ * Input data (JSON)
125
+ */
126
+ input: jsonb("input"),
127
+ /**
128
+ * Current step index
129
+ */
130
+ currentStep: integer("current_step").notNull().default(0),
131
+ /**
132
+ * Error message (if failed)
133
+ */
134
+ error: text("error"),
135
+ /**
136
+ * Completed timestamp
137
+ */
138
+ completedAt: timestamp("completed_at", { withTimezone: true }),
139
+ ...timestamps()
140
+ },
141
+ (table) => [
142
+ index("wf_exec_workflow_name_idx").on(table.workflowName),
143
+ index("wf_exec_status_idx").on(table.status),
144
+ index("wf_exec_created_at_idx").on(table.createdAt),
145
+ index("wf_exec_workflow_status_idx").on(table.workflowName, table.status)
146
+ ]
147
+ );
148
+ var workflowStepExecutions = workflowSchema.table(
149
+ "step_executions",
150
+ {
151
+ id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
152
+ /**
153
+ * Parent workflow execution ID
154
+ */
155
+ executionId: text("execution_id").notNull().references(() => workflowExecutions.id, { onDelete: "cascade" }),
156
+ /**
157
+ * Step name (job name)
158
+ */
159
+ stepName: text("step_name").notNull(),
160
+ /**
161
+ * Step index in the workflow
162
+ */
163
+ stepIndex: integer("step_index").notNull(),
164
+ /**
165
+ * Step execution status
166
+ */
167
+ status: text("status").$type().notNull().default("pending"),
168
+ /**
169
+ * Step output data (JSON or URL reference for large data)
170
+ */
171
+ output: jsonb("output"),
172
+ /**
173
+ * Error message (if failed)
174
+ */
175
+ error: text("error"),
176
+ /**
177
+ * Started timestamp
178
+ */
179
+ startedAt: timestamp("started_at", { withTimezone: true }),
180
+ /**
181
+ * Completed timestamp
182
+ */
183
+ completedAt: timestamp("completed_at", { withTimezone: true }),
184
+ ...timestamps()
185
+ },
186
+ (table) => [
187
+ index("wf_step_exec_execution_id_idx").on(table.executionId),
188
+ index("wf_step_exec_status_idx").on(table.status),
189
+ index("wf_step_exec_exec_step_idx").on(table.executionId, table.stepIndex)
190
+ ]
191
+ );
192
+
193
+ // src/engine/types.ts
194
+ var defaultLogger = {
195
+ info: (message, ...args) => console.log(message, ...args),
196
+ error: (message, ...args) => console.error(message, ...args),
197
+ warn: (message, ...args) => console.warn(message, ...args),
198
+ debug: (message, ...args) => console.debug(message, ...args)
199
+ };
200
+
201
+ // src/engine/workflow-engine.ts
202
+ var DEFAULT_LARGE_OUTPUT_THRESHOLD = 1024 * 1024;
203
+ var WorkflowEngineImpl = class {
204
+ config;
205
+ workflows;
206
+ subscribers;
207
+ logger;
208
+ /** Track executions currently being processed to prevent race conditions */
209
+ processingExecutions;
210
+ constructor(workflows, config) {
211
+ this.config = config;
212
+ this.workflows = /* @__PURE__ */ new Map();
213
+ this.subscribers = /* @__PURE__ */ new Map();
214
+ this.logger = config.logger ?? defaultLogger;
215
+ this.processingExecutions = /* @__PURE__ */ new Set();
216
+ for (const wf of workflows) {
217
+ this.workflows.set(wf.name, wf);
218
+ }
219
+ }
220
+ /**
221
+ * Get database instance
222
+ */
223
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
224
+ get db() {
225
+ return this.config.db;
226
+ }
227
+ /**
228
+ * Start a workflow execution
229
+ */
230
+ async start(name, input) {
231
+ const workflow2 = this.workflows.get(name);
232
+ if (!workflow2) {
233
+ throw new Error(`Workflow '${name}' not found`);
234
+ }
235
+ if (this.config.validateInput !== false && workflow2.inputSchema) {
236
+ if (!Value.Check(workflow2.inputSchema, input)) {
237
+ const errors = [...Value.Errors(workflow2.inputSchema, input)];
238
+ const errorMessages = errors.map((e) => `${e.path}: ${e.message}`).join(", ");
239
+ throw new Error(`Invalid workflow input: ${errorMessages}`);
240
+ }
241
+ }
242
+ const execution = await this.db.transaction(async (tx) => {
243
+ const [exec] = await tx.insert(workflowExecutions).values({
244
+ workflowName: name,
245
+ status: "pending",
246
+ input,
247
+ currentStep: 0
248
+ }).returning();
249
+ const stepRecords = workflow2.steps.map((step, index3) => ({
250
+ executionId: exec.id,
251
+ stepName: step.name,
252
+ stepIndex: index3,
253
+ status: "pending"
254
+ }));
255
+ if (stepRecords.length > 0) {
256
+ await tx.insert(workflowStepExecutions).values(stepRecords);
257
+ }
258
+ return exec;
259
+ });
260
+ this.emitEvent({
261
+ type: "started",
262
+ workflowName: name,
263
+ executionId: execution.id,
264
+ input,
265
+ timestamp: /* @__PURE__ */ new Date()
266
+ });
267
+ this.executeNextStep(execution.id, workflow2, input).catch((error) => {
268
+ this.logger.error(`[Workflow:${name}] Execution error:`, error);
269
+ });
270
+ return {
271
+ id: execution.id,
272
+ workflowName: name,
273
+ status: "pending"
274
+ };
275
+ }
276
+ /**
277
+ * Execute steps iteratively until completion or failure
278
+ */
279
+ async executeNextStep(executionId, workflow2, input) {
280
+ while (true) {
281
+ const execution = await this.getExecution(executionId);
282
+ if (!execution || execution.status !== "pending" && execution.status !== "running") {
283
+ return;
284
+ }
285
+ if (execution.status === "pending") {
286
+ await this.updateExecutionStatus(executionId, "running");
287
+ }
288
+ const results = await this.getCompletedResults(executionId);
289
+ const pendingSteps = execution.steps.filter((s) => s.status === "pending");
290
+ if (pendingSteps.length === 0) {
291
+ await this.completeExecution(executionId);
292
+ return;
293
+ }
294
+ const currentStepIndex = Math.min(...pendingSteps.map((s) => s.stepIndex));
295
+ await this.db.update(workflowExecutions).set({ currentStep: currentStepIndex, updatedAt: /* @__PURE__ */ new Date() }).where(eq(workflowExecutions.id, executionId));
296
+ const stepDef = workflow2.steps[currentStepIndex];
297
+ if (stepDef.type === "parallel") {
298
+ const parallelSteps = workflow2.steps.filter(
299
+ (s) => s.parallelGroup === stepDef.parallelGroup
300
+ );
301
+ const errors = await Promise.all(
302
+ parallelSteps.map(
303
+ (step) => this.executeStep(executionId, workflow2, step, input, results)
304
+ )
305
+ );
306
+ const firstError = errors.find((e) => e !== null);
307
+ if (firstError) {
308
+ await this.handleStepFailure(executionId, workflow2, firstError);
309
+ return;
310
+ }
311
+ } else {
312
+ const error = await this.executeStep(executionId, workflow2, stepDef, input, results);
313
+ if (error) {
314
+ await this.handleStepFailure(executionId, workflow2, error);
315
+ return;
316
+ }
317
+ }
318
+ }
319
+ }
320
+ /**
321
+ * Execute a single step
322
+ *
323
+ * Returns the error message if the step failed, or null if successful.
324
+ */
325
+ async executeStep(executionId, workflow2, step, input, results) {
326
+ const stepExecution = await this.getStepExecution(executionId, step.name);
327
+ if (!stepExecution || stepExecution.status !== "pending") {
328
+ return null;
329
+ }
330
+ await this.updateStepStatus(stepExecution.id, "running");
331
+ this.emitEvent({
332
+ type: "step.started",
333
+ workflowName: workflow2.name,
334
+ executionId,
335
+ stepName: step.name,
336
+ stepIndex: stepExecution.stepIndex,
337
+ timestamp: /* @__PURE__ */ new Date()
338
+ });
339
+ try {
340
+ const context = {
341
+ input,
342
+ results,
343
+ execution: {
344
+ id: executionId,
345
+ workflowName: workflow2.name,
346
+ startedAt: /* @__PURE__ */ new Date()
347
+ }
348
+ };
349
+ const stepInput = step.mapper(context);
350
+ const output = await step.job.run(stepInput);
351
+ const storedOutput = await this.storeOutput(output);
352
+ await this.updateStepStatus(stepExecution.id, "completed", storedOutput);
353
+ this.emitEvent({
354
+ type: "step.completed",
355
+ workflowName: workflow2.name,
356
+ executionId,
357
+ stepName: step.name,
358
+ stepIndex: stepExecution.stepIndex,
359
+ output: storedOutput,
360
+ timestamp: /* @__PURE__ */ new Date()
361
+ });
362
+ return null;
363
+ } catch (error) {
364
+ const errorMessage = error instanceof Error ? error.message : String(error);
365
+ await this.updateStepStatus(stepExecution.id, "failed", void 0, errorMessage);
366
+ this.emitEvent({
367
+ type: "step.failed",
368
+ workflowName: workflow2.name,
369
+ executionId,
370
+ stepName: step.name,
371
+ stepIndex: stepExecution.stepIndex,
372
+ error: errorMessage,
373
+ timestamp: /* @__PURE__ */ new Date()
374
+ });
375
+ return errorMessage;
376
+ }
377
+ }
378
+ /**
379
+ * Handle step failure - rollback if enabled
380
+ */
381
+ async handleStepFailure(executionId, workflow2, error) {
382
+ await this.db.update(workflowExecutions).set({
383
+ status: "failed",
384
+ error,
385
+ updatedAt: /* @__PURE__ */ new Date()
386
+ }).where(eq(workflowExecutions.id, executionId));
387
+ this.emitEvent({
388
+ type: "failed",
389
+ workflowName: workflow2.name,
390
+ executionId,
391
+ error,
392
+ timestamp: /* @__PURE__ */ new Date()
393
+ });
394
+ if (workflow2.rollbackEnabled) {
395
+ await this.executeRollback(executionId, workflow2);
396
+ }
397
+ }
398
+ /**
399
+ * Execute rollback in reverse order
400
+ */
401
+ async executeRollback(executionId, workflow2) {
402
+ await this.updateExecutionStatus(executionId, "compensating");
403
+ const execution = await this.getExecution(executionId);
404
+ if (!execution) return;
405
+ const completedSteps = execution.steps.filter((s) => s.status === "completed").sort((a, b) => b.stepIndex - a.stepIndex);
406
+ for (const stepExecution of completedSteps) {
407
+ const stepDef = workflow2.steps.find((s) => s.name === stepExecution.stepName);
408
+ if (!stepDef || !stepDef.job.compensate) {
409
+ continue;
410
+ }
411
+ try {
412
+ const input = execution.input;
413
+ const output = await this.resolveOutput(stepExecution.output);
414
+ await stepDef.job.compensate(input, output);
415
+ await this.updateStepStatus(stepExecution.id, "compensated");
416
+ } catch (compensateError) {
417
+ this.logger.error(
418
+ `[Workflow:${workflow2.name}] Compensate error for step ${stepExecution.stepName}:`,
419
+ compensateError
420
+ );
421
+ }
422
+ }
423
+ await this.updateExecutionStatus(executionId, "compensated");
424
+ }
425
+ /**
426
+ * Complete the workflow execution
427
+ */
428
+ async completeExecution(executionId) {
429
+ const execution = await this.getExecution(executionId);
430
+ if (!execution) return;
431
+ await this.db.update(workflowExecutions).set({
432
+ status: "completed",
433
+ completedAt: /* @__PURE__ */ new Date(),
434
+ updatedAt: /* @__PURE__ */ new Date()
435
+ }).where(eq(workflowExecutions.id, executionId));
436
+ this.emitEvent({
437
+ type: "completed",
438
+ workflowName: execution.workflowName,
439
+ executionId,
440
+ timestamp: /* @__PURE__ */ new Date()
441
+ });
442
+ }
443
+ /**
444
+ * Get execution with steps
445
+ */
446
+ async getExecution(executionId) {
447
+ const [execution] = await this.db.select().from(workflowExecutions).where(eq(workflowExecutions.id, executionId));
448
+ if (!execution) return null;
449
+ const steps = await this.db.select().from(workflowStepExecutions).where(eq(workflowStepExecutions.executionId, executionId)).orderBy(workflowStepExecutions.stepIndex);
450
+ return { ...execution, steps };
451
+ }
452
+ /**
453
+ * Get step execution
454
+ */
455
+ async getStepExecution(executionId, stepName) {
456
+ const [step] = await this.db.select().from(workflowStepExecutions).where(
457
+ and(
458
+ eq(workflowStepExecutions.executionId, executionId),
459
+ eq(workflowStepExecutions.stepName, stepName)
460
+ )
461
+ );
462
+ return step || null;
463
+ }
464
+ /**
465
+ * Get completed step results
466
+ */
467
+ async getCompletedResults(executionId) {
468
+ const steps = await this.db.select().from(workflowStepExecutions).where(
469
+ and(
470
+ eq(workflowStepExecutions.executionId, executionId),
471
+ eq(workflowStepExecutions.status, "completed")
472
+ )
473
+ );
474
+ const results = {};
475
+ for (const step of steps) {
476
+ results[step.stepName] = await this.resolveOutput(step.output);
477
+ }
478
+ return results;
479
+ }
480
+ /**
481
+ * Update execution status
482
+ */
483
+ async updateExecutionStatus(executionId, status) {
484
+ await this.db.update(workflowExecutions).set({
485
+ status,
486
+ updatedAt: /* @__PURE__ */ new Date()
487
+ }).where(eq(workflowExecutions.id, executionId));
488
+ }
489
+ /**
490
+ * Update step status
491
+ */
492
+ async updateStepStatus(stepId, status, output, error) {
493
+ const updates = {
494
+ status,
495
+ updatedAt: /* @__PURE__ */ new Date()
496
+ };
497
+ if (status === "running") {
498
+ updates.startedAt = /* @__PURE__ */ new Date();
499
+ }
500
+ if (status === "completed" || status === "failed") {
501
+ updates.completedAt = /* @__PURE__ */ new Date();
502
+ }
503
+ if (output !== void 0) {
504
+ updates.output = output;
505
+ }
506
+ if (error !== void 0) {
507
+ updates.error = error;
508
+ }
509
+ await this.db.update(workflowStepExecutions).set(updates).where(eq(workflowStepExecutions.id, stepId));
510
+ }
511
+ /**
512
+ * Store output (handle large data)
513
+ */
514
+ async storeOutput(output) {
515
+ if (!output || !this.config.storage) return output;
516
+ const json = JSON.stringify(output);
517
+ const threshold = this.config.largeOutputThreshold ?? DEFAULT_LARGE_OUTPUT_THRESHOLD;
518
+ if (Buffer.byteLength(json, "utf8") > threshold) {
519
+ const url = await this.config.storage.upload(output);
520
+ return { $ref: url };
521
+ }
522
+ return output;
523
+ }
524
+ /**
525
+ * Resolve output (fetch from storage if needed)
526
+ */
527
+ async resolveOutput(output) {
528
+ if (!output) return output;
529
+ const ref = output;
530
+ if (ref.$ref && this.config.storage) {
531
+ return await this.config.storage.download(ref.$ref);
532
+ }
533
+ return output;
534
+ }
535
+ /**
536
+ * Emit event to subscribers and notification providers
537
+ */
538
+ emitEvent(event) {
539
+ const subscribers = this.subscribers.get(event.executionId);
540
+ if (subscribers) {
541
+ for (const callback of subscribers) {
542
+ try {
543
+ callback(event);
544
+ } catch (error) {
545
+ this.logger.error("[WorkflowEngine] Subscriber error:", error);
546
+ }
547
+ }
548
+ const terminalEvents = ["completed", "failed", "cancelled"];
549
+ if (terminalEvents.includes(event.type)) {
550
+ this.subscribers.delete(event.executionId);
551
+ }
552
+ }
553
+ this.sendNotifications(event).catch((error) => {
554
+ this.logger.error("[WorkflowEngine] Notification error:", error);
555
+ });
556
+ }
557
+ /**
558
+ * Send notifications based on workflow config
559
+ */
560
+ async sendNotifications(event) {
561
+ const workflow2 = this.workflows.get(event.workflowName);
562
+ if (!workflow2 || workflow2.notifyConfigs.length === 0) {
563
+ return;
564
+ }
565
+ for (const { on, when, providers } of workflow2.notifyConfigs) {
566
+ if (!on.includes(event.type)) {
567
+ continue;
568
+ }
569
+ if (when && !when(event)) {
570
+ continue;
571
+ }
572
+ await Promise.all(
573
+ providers.map(async (provider) => {
574
+ try {
575
+ await provider.notify(event);
576
+ } catch (error) {
577
+ this.logger.error(
578
+ `[WorkflowEngine] Notification provider '${provider.name}' error:`,
579
+ error
580
+ );
581
+ }
582
+ })
583
+ );
584
+ }
585
+ }
586
+ // Public API methods
587
+ async get(executionId) {
588
+ return this.getExecution(executionId);
589
+ }
590
+ async getStepOutput(executionId, stepName) {
591
+ const step = await this.getStepExecution(executionId, stepName);
592
+ if (!step) return null;
593
+ return this.resolveOutput(step.output);
594
+ }
595
+ async list(options) {
596
+ const conditions = [];
597
+ if (options?.workflowName) {
598
+ conditions.push(eq(workflowExecutions.workflowName, options.workflowName));
599
+ }
600
+ if (options?.status) {
601
+ conditions.push(eq(workflowExecutions.status, options.status));
602
+ }
603
+ let query = this.db.select().from(workflowExecutions).orderBy(desc(workflowExecutions.createdAt));
604
+ if (conditions.length > 0) {
605
+ query = query.where(conditions.length === 1 ? conditions[0] : and(...conditions));
606
+ }
607
+ if (options?.limit) {
608
+ query = query.limit(options.limit);
609
+ }
610
+ if (options?.offset) {
611
+ query = query.offset(options.offset);
612
+ }
613
+ const executions = await query;
614
+ if (executions.length === 0) {
615
+ return [];
616
+ }
617
+ const executionIds = executions.map((e) => e.id);
618
+ const allSteps = await this.db.select().from(workflowStepExecutions).where(inArray(workflowStepExecutions.executionId, executionIds)).orderBy(workflowStepExecutions.stepIndex);
619
+ const stepsByExecutionId = /* @__PURE__ */ new Map();
620
+ for (const step of allSteps) {
621
+ const steps = stepsByExecutionId.get(step.executionId) ?? [];
622
+ steps.push(step);
623
+ stepsByExecutionId.set(step.executionId, steps);
624
+ }
625
+ return executions.map((execution) => ({
626
+ ...execution,
627
+ steps: stepsByExecutionId.get(execution.id) ?? []
628
+ }));
629
+ }
630
+ async retry(executionId) {
631
+ if (this.processingExecutions.has(executionId)) {
632
+ throw new Error(`Execution '${executionId}' is already being processed`);
633
+ }
634
+ const execution = await this.getExecution(executionId);
635
+ if (!execution) {
636
+ throw new Error(`Execution '${executionId}' not found`);
637
+ }
638
+ const retryableStatuses = ["failed", "compensated"];
639
+ if (!retryableStatuses.includes(execution.status)) {
640
+ throw new Error(
641
+ `Cannot retry execution '${executionId}' with status '${execution.status}'. Only 'failed' or 'compensated' executions can be retried.`
642
+ );
643
+ }
644
+ const workflow2 = this.workflows.get(execution.workflowName);
645
+ if (!workflow2) {
646
+ throw new Error(`Workflow '${execution.workflowName}' not found`);
647
+ }
648
+ this.processingExecutions.add(executionId);
649
+ try {
650
+ if (workflow2.resumable) {
651
+ await this.updateExecutionStatus(executionId, "running");
652
+ const failedSteps = execution.steps.filter((s) => s.status === "failed");
653
+ for (const step of failedSteps) {
654
+ await this.updateStepStatus(step.id, "pending");
655
+ }
656
+ } else {
657
+ await this.resetAllSteps(execution.steps);
658
+ await this.db.update(workflowExecutions).set({
659
+ status: "pending",
660
+ currentStep: 0,
661
+ error: null,
662
+ completedAt: null,
663
+ updatedAt: /* @__PURE__ */ new Date()
664
+ }).where(eq(workflowExecutions.id, executionId));
665
+ }
666
+ this.executeNextStep(
667
+ executionId,
668
+ workflow2,
669
+ execution.input
670
+ ).catch((error) => {
671
+ this.logger.error(`[Workflow:${workflow2.name}] Retry error:`, error);
672
+ }).finally(() => {
673
+ this.processingExecutions.delete(executionId);
674
+ });
675
+ return {
676
+ id: executionId,
677
+ workflowName: execution.workflowName,
678
+ status: "pending"
679
+ };
680
+ } catch (error) {
681
+ this.processingExecutions.delete(executionId);
682
+ throw error;
683
+ }
684
+ }
685
+ /**
686
+ * Reset all steps to pending state
687
+ */
688
+ async resetAllSteps(steps) {
689
+ if (steps.length === 0) return;
690
+ await this.db.update(workflowStepExecutions).set({
691
+ status: "pending",
692
+ output: null,
693
+ error: null,
694
+ startedAt: null,
695
+ completedAt: null,
696
+ updatedAt: /* @__PURE__ */ new Date()
697
+ }).where(inArray(workflowStepExecutions.id, steps.map((s) => s.id)));
698
+ }
699
+ async cancel(executionId, options) {
700
+ const execution = await this.getExecution(executionId);
701
+ if (!execution) {
702
+ throw new Error(`Execution '${executionId}' not found`);
703
+ }
704
+ const cancellableStatuses = ["pending", "running"];
705
+ if (!cancellableStatuses.includes(execution.status)) {
706
+ throw new Error(
707
+ `Cannot cancel execution '${executionId}' with status '${execution.status}'. Only 'pending' or 'running' executions can be cancelled.`
708
+ );
709
+ }
710
+ const workflow2 = this.workflows.get(execution.workflowName);
711
+ await this.updateExecutionStatus(executionId, "cancelled");
712
+ this.emitEvent({
713
+ type: "cancelled",
714
+ workflowName: execution.workflowName,
715
+ executionId,
716
+ timestamp: /* @__PURE__ */ new Date()
717
+ });
718
+ if (options?.rollback && workflow2) {
719
+ await this.executeRollback(executionId, workflow2);
720
+ }
721
+ }
722
+ subscribe(executionId, callback) {
723
+ if (!this.subscribers.has(executionId)) {
724
+ this.subscribers.set(executionId, /* @__PURE__ */ new Set());
725
+ }
726
+ this.subscribers.get(executionId).add(callback);
727
+ return () => {
728
+ this.subscribers.get(executionId)?.delete(callback);
729
+ };
730
+ }
731
+ };
732
+ function createWorkflowEngine(options) {
733
+ const { workflows, ...config } = options;
734
+ return new WorkflowEngineImpl(workflows, config);
735
+ }
736
+
737
+ // src/notification/providers.ts
738
+ var consoleProvider = {
739
+ name: "console",
740
+ async notify(event) {
741
+ const timestamp3 = event.timestamp.toISOString();
742
+ const prefix = `[Workflow:${event.workflowName}]`;
743
+ switch (event.type) {
744
+ case "started":
745
+ console.log(`${timestamp3} ${prefix} Started (id: ${event.executionId})`);
746
+ break;
747
+ case "completed":
748
+ console.log(`${timestamp3} ${prefix} Completed (id: ${event.executionId})`);
749
+ break;
750
+ case "failed":
751
+ console.error(`${timestamp3} ${prefix} Failed: ${event.error} (id: ${event.executionId})`);
752
+ break;
753
+ case "cancelled":
754
+ console.log(`${timestamp3} ${prefix} Cancelled (id: ${event.executionId})`);
755
+ break;
756
+ case "step.started":
757
+ console.log(`${timestamp3} ${prefix} Step '${event.stepName}' started`);
758
+ break;
759
+ case "step.completed":
760
+ console.log(`${timestamp3} ${prefix} Step '${event.stepName}' completed`);
761
+ break;
762
+ case "step.failed":
763
+ console.error(`${timestamp3} ${prefix} Step '${event.stepName}' failed: ${event.error}`);
764
+ break;
765
+ }
766
+ }
767
+ };
768
+ function formatEventAsText(event) {
769
+ const lines = [
770
+ `Workflow: ${event.workflowName}`,
771
+ `Event: ${event.type}`,
772
+ `Execution ID: ${event.executionId}`,
773
+ `Timestamp: ${event.timestamp.toISOString()}`
774
+ ];
775
+ if (event.stepName) {
776
+ lines.push(`Step: ${event.stepName}`);
777
+ }
778
+ if (event.error) {
779
+ lines.push(`Error: ${event.error}`);
780
+ }
781
+ return lines.join("\n");
782
+ }
783
+
784
+ // src/config/workflow-router.ts
785
+ function defineWorkflowRouter(workflows) {
786
+ const state = {
787
+ engine: null
788
+ };
789
+ return {
790
+ workflows,
791
+ _workflows: workflows,
792
+ get engine() {
793
+ if (!state.engine) {
794
+ throw new Error(
795
+ "Workflow engine not initialized. Make sure the server is started with .workflows(router) configuration."
796
+ );
797
+ }
798
+ return state.engine;
799
+ },
800
+ get isInitialized() {
801
+ return state.engine !== null;
802
+ },
803
+ _init(db, options) {
804
+ if (state.engine) {
805
+ return;
806
+ }
807
+ state.engine = createWorkflowEngine({
808
+ db,
809
+ workflows,
810
+ largeOutputThreshold: options?.largeOutputThreshold,
811
+ storage: options?.storage,
812
+ logger: options?.logger,
813
+ validateInput: options?.validateInput
814
+ });
815
+ }
816
+ };
817
+ }
818
+ function isWorkflowRouter(value) {
819
+ return typeof value === "object" && value !== null && "_workflows" in value && "_init" in value && typeof value._init === "function";
820
+ }
821
+
822
+ export { WorkflowBuilder, consoleProvider, createWorkflowEngine, defaultLogger, defineWorkflowRouter, formatEventAsText, isWorkflowRouter, workflow, workflowExecutions, workflowStepExecutions };
823
+ //# sourceMappingURL=index.js.map
824
+ //# sourceMappingURL=index.js.map