@gravito/flux 3.0.0 → 3.0.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/README.md +298 -0
- package/bin/flux.js +25 -1
- package/dev/viewer/app.js +4 -4
- package/dist/bun.cjs +2 -2
- package/dist/bun.cjs.map +1 -1
- package/dist/bun.d.cts +65 -26
- package/dist/bun.d.ts +65 -26
- package/dist/bun.js +1 -1
- package/dist/chunk-4DXCQ6CL.js +3486 -0
- package/dist/chunk-4DXCQ6CL.js.map +1 -0
- package/dist/chunk-6AZNHVEO.cjs +316 -0
- package/dist/chunk-6AZNHVEO.cjs.map +1 -0
- package/dist/{chunk-ZAMVC732.js → chunk-NAIVO7RR.js} +64 -15
- package/dist/chunk-NAIVO7RR.js.map +1 -0
- package/dist/chunk-WAPZDXSX.cjs +3486 -0
- package/dist/chunk-WAPZDXSX.cjs.map +1 -0
- package/dist/chunk-WGDTB6OC.js +316 -0
- package/dist/chunk-WGDTB6OC.js.map +1 -0
- package/dist/{chunk-SJSPR4ZU.cjs → chunk-YXBEYVGY.cjs} +66 -17
- package/dist/chunk-YXBEYVGY.cjs.map +1 -0
- package/dist/cli/flux-visualize.cjs +108 -0
- package/dist/cli/flux-visualize.cjs.map +1 -0
- package/dist/cli/flux-visualize.d.cts +1 -0
- package/dist/cli/flux-visualize.d.ts +1 -0
- package/dist/cli/flux-visualize.js +108 -0
- package/dist/cli/flux-visualize.js.map +1 -0
- package/dist/index.cjs +100 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +402 -12
- package/dist/index.d.ts +402 -12
- package/dist/index.js +98 -10
- package/dist/index.js.map +1 -1
- package/dist/index.node.cjs +11 -3
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.d.cts +1114 -258
- package/dist/index.node.d.ts +1114 -258
- package/dist/index.node.js +10 -2
- package/dist/types-CRz5XdLd.d.cts +433 -0
- package/dist/types-CRz5XdLd.d.ts +433 -0
- package/package.json +17 -6
- package/dist/chunk-LULCFPIK.js +0 -1004
- package/dist/chunk-LULCFPIK.js.map +0 -1
- package/dist/chunk-SJSPR4ZU.cjs.map +0 -1
- package/dist/chunk-X3NC7HS4.cjs +0 -1004
- package/dist/chunk-X3NC7HS4.cjs.map +0 -1
- package/dist/chunk-ZAMVC732.js.map +0 -1
- package/dist/types-cnIU1O3n.d.cts +0 -250
- package/dist/types-cnIU1O3n.d.ts +0 -250
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// src/visualization/MermaidGenerator.ts
|
|
2
|
+
var MermaidGenerator = class {
|
|
3
|
+
/**
|
|
4
|
+
* Generates a Mermaid flowchart from a workflow definition.
|
|
5
|
+
*
|
|
6
|
+
* @param definition - The workflow definition to visualize
|
|
7
|
+
* @param options - Customization options
|
|
8
|
+
* @returns Mermaid diagram syntax as a string
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const generator = new MermaidGenerator()
|
|
13
|
+
* const diagram = generator.generateFromDefinition(workflow, {
|
|
14
|
+
* showDetails: true,
|
|
15
|
+
* showParallelGroups: true
|
|
16
|
+
* })
|
|
17
|
+
* console.log(diagram)
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
generateFromDefinition(definition, options = {}) {
|
|
21
|
+
const { showDetails = false, showParallelGroups = true, theme = "default" } = options;
|
|
22
|
+
const lines = [];
|
|
23
|
+
lines.push(`%%{init: {'theme':'${theme}'}}%%`);
|
|
24
|
+
lines.push("flowchart TD");
|
|
25
|
+
lines.push(` Start([Start: ${definition.name}])`);
|
|
26
|
+
let prevNode = "Start";
|
|
27
|
+
const parallelGroups = /* @__PURE__ */ new Map();
|
|
28
|
+
if (showParallelGroups) {
|
|
29
|
+
for (const step of definition.steps) {
|
|
30
|
+
if (step.parallelGroup) {
|
|
31
|
+
const group = parallelGroups.get(step.parallelGroup) || [];
|
|
32
|
+
group.push(step.name);
|
|
33
|
+
parallelGroups.set(step.parallelGroup, group);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const processedGroups = /* @__PURE__ */ new Set();
|
|
38
|
+
for (let i = 0; i < definition.steps.length; i++) {
|
|
39
|
+
const step = definition.steps[i];
|
|
40
|
+
if (step.parallelGroup && showParallelGroups) {
|
|
41
|
+
if (processedGroups.has(step.parallelGroup)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
processedGroups.add(step.parallelGroup);
|
|
45
|
+
const groupSteps = parallelGroups.get(step.parallelGroup);
|
|
46
|
+
const groupName = `ParallelGroup_${step.parallelGroup}`;
|
|
47
|
+
lines.push(` subgraph ${groupName}[" "]`);
|
|
48
|
+
lines.push(` direction LR`);
|
|
49
|
+
for (const stepName of groupSteps) {
|
|
50
|
+
const s = definition.steps.find((st) => st.name === stepName);
|
|
51
|
+
const nodeId = this.sanitizeNodeId(stepName);
|
|
52
|
+
const label = this.buildStepLabel(s, showDetails);
|
|
53
|
+
const shape = s.commit ? `[${label}]` : `(${label})`;
|
|
54
|
+
lines.push(` ${nodeId}${shape}`);
|
|
55
|
+
if (s.compensate) {
|
|
56
|
+
lines.push(` ${nodeId}_comp[\u{1F504} Compensate]`);
|
|
57
|
+
lines.push(` ${nodeId} -.->|on failure| ${nodeId}_comp`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
lines.push(` end`);
|
|
61
|
+
lines.push(` ${prevNode} --> ${groupName}`);
|
|
62
|
+
prevNode = groupName;
|
|
63
|
+
} else if (!step.parallelGroup) {
|
|
64
|
+
const nodeId = this.sanitizeNodeId(step.name);
|
|
65
|
+
const label = this.buildStepLabel(step, showDetails);
|
|
66
|
+
const shape = step.commit ? `[${label}]` : `(${label})`;
|
|
67
|
+
lines.push(` ${nodeId}${shape}`);
|
|
68
|
+
if (step.when) {
|
|
69
|
+
const condId = `${nodeId}_cond`;
|
|
70
|
+
lines.push(` ${condId}{Condition?}`);
|
|
71
|
+
lines.push(` ${prevNode} --> ${condId}`);
|
|
72
|
+
lines.push(` ${condId} -->|yes| ${nodeId}`);
|
|
73
|
+
lines.push(` ${condId} -->|no| ${this.getNextNodeId(definition.steps, i)}`);
|
|
74
|
+
prevNode = nodeId;
|
|
75
|
+
} else {
|
|
76
|
+
lines.push(` ${prevNode} --> ${nodeId}`);
|
|
77
|
+
prevNode = nodeId;
|
|
78
|
+
}
|
|
79
|
+
if (step.compensate) {
|
|
80
|
+
lines.push(` ${nodeId}_comp[\u{1F504} Compensate]`);
|
|
81
|
+
lines.push(` ${nodeId} -.->|on failure| ${nodeId}_comp`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
lines.push(` ${prevNode} --> End([End])`);
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push(" classDef commitStep fill:#e1f5e1,stroke:#4caf50,stroke-width:2px");
|
|
88
|
+
lines.push(" classDef normalStep fill:#e3f2fd,stroke:#2196f3,stroke-width:2px");
|
|
89
|
+
lines.push(
|
|
90
|
+
" classDef compensateStep fill:#fff3e0,stroke:#ff9800,stroke-width:1px,stroke-dasharray: 5 5"
|
|
91
|
+
);
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Generates a Mermaid flowchart from a workflow execution state.
|
|
96
|
+
* Overlays execution status (completed, failed, compensated) on top of the structure.
|
|
97
|
+
*
|
|
98
|
+
* @param definition - The workflow definition
|
|
99
|
+
* @param state - The current execution state
|
|
100
|
+
* @param options - Customization options
|
|
101
|
+
* @returns Mermaid diagram syntax with status overlay
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* const diagram = generator.generateFromState(workflow, executionState, {
|
|
106
|
+
* showStatus: true,
|
|
107
|
+
* showDetails: true
|
|
108
|
+
* })
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
generateFromState(definition, state, options = {}) {
|
|
112
|
+
const {
|
|
113
|
+
showDetails = true,
|
|
114
|
+
showStatus = true,
|
|
115
|
+
showParallelGroups = true,
|
|
116
|
+
theme = "default"
|
|
117
|
+
} = options;
|
|
118
|
+
const lines = [];
|
|
119
|
+
lines.push(`%%{init: {'theme':'${theme}'}}%%`);
|
|
120
|
+
lines.push("flowchart TD");
|
|
121
|
+
lines.push(` Start([Start: ${definition.name}])`);
|
|
122
|
+
lines.push(` Start:::started`);
|
|
123
|
+
let prevNode = "Start";
|
|
124
|
+
const parallelGroups = /* @__PURE__ */ new Map();
|
|
125
|
+
const executionMap = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const exec of state.history) {
|
|
127
|
+
executionMap.set(exec.name, exec);
|
|
128
|
+
}
|
|
129
|
+
if (showParallelGroups) {
|
|
130
|
+
for (const step of definition.steps) {
|
|
131
|
+
if (step.parallelGroup) {
|
|
132
|
+
const group = parallelGroups.get(step.parallelGroup) || [];
|
|
133
|
+
group.push(step.name);
|
|
134
|
+
parallelGroups.set(step.parallelGroup, group);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const processedGroups = /* @__PURE__ */ new Set();
|
|
139
|
+
for (let i = 0; i < definition.steps.length; i++) {
|
|
140
|
+
const step = definition.steps[i];
|
|
141
|
+
if (step.parallelGroup && showParallelGroups) {
|
|
142
|
+
if (processedGroups.has(step.parallelGroup)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
processedGroups.add(step.parallelGroup);
|
|
146
|
+
const groupSteps = parallelGroups.get(step.parallelGroup);
|
|
147
|
+
const groupName = `ParallelGroup_${step.parallelGroup}`;
|
|
148
|
+
lines.push(` subgraph ${groupName}[" "]`);
|
|
149
|
+
lines.push(` direction LR`);
|
|
150
|
+
for (const stepName of groupSteps) {
|
|
151
|
+
const s = definition.steps.find((st) => st.name === stepName);
|
|
152
|
+
const nodeId = this.sanitizeNodeId(stepName);
|
|
153
|
+
const exec = executionMap.get(stepName);
|
|
154
|
+
const label = this.buildStepLabelWithStatus(s, exec, showDetails, showStatus);
|
|
155
|
+
const shape = s.commit ? `[${label}]` : `(${label})`;
|
|
156
|
+
lines.push(` ${nodeId}${shape}`);
|
|
157
|
+
if (exec && showStatus) {
|
|
158
|
+
const statusClass = this.getStatusClass(exec.status);
|
|
159
|
+
lines.push(` ${nodeId}:::${statusClass}`);
|
|
160
|
+
}
|
|
161
|
+
if (s.compensate && exec?.status === "compensated") {
|
|
162
|
+
lines.push(` ${nodeId}_comp[\u{1F504} Compensated]`);
|
|
163
|
+
lines.push(` ${nodeId}_comp:::compensated`);
|
|
164
|
+
lines.push(` ${nodeId} -.->|rolled back| ${nodeId}_comp`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
lines.push(` end`);
|
|
168
|
+
lines.push(` ${prevNode} --> ${groupName}`);
|
|
169
|
+
prevNode = groupName;
|
|
170
|
+
} else if (!step.parallelGroup) {
|
|
171
|
+
const nodeId = this.sanitizeNodeId(step.name);
|
|
172
|
+
const exec = executionMap.get(step.name);
|
|
173
|
+
const label = this.buildStepLabelWithStatus(step, exec, showDetails, showStatus);
|
|
174
|
+
const shape = step.commit ? `[${label}]` : `(${label})`;
|
|
175
|
+
lines.push(` ${nodeId}${shape}`);
|
|
176
|
+
if (exec && showStatus) {
|
|
177
|
+
const statusClass = this.getStatusClass(exec.status);
|
|
178
|
+
lines.push(` ${nodeId}:::${statusClass}`);
|
|
179
|
+
}
|
|
180
|
+
if (step.when) {
|
|
181
|
+
const condId = `${nodeId}_cond`;
|
|
182
|
+
const skipped = exec?.status === "skipped";
|
|
183
|
+
lines.push(` ${condId}{Condition?}`);
|
|
184
|
+
lines.push(` ${prevNode} --> ${condId}`);
|
|
185
|
+
if (skipped) {
|
|
186
|
+
lines.push(` ${condId}:::skipped`);
|
|
187
|
+
lines.push(` ${condId} -->|no| ${nodeId}`);
|
|
188
|
+
} else {
|
|
189
|
+
lines.push(` ${condId} -->|yes| ${nodeId}`);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
lines.push(` ${prevNode} --> ${nodeId}`);
|
|
193
|
+
}
|
|
194
|
+
if (step.compensate && exec?.status === "compensated") {
|
|
195
|
+
lines.push(` ${nodeId}_comp[\u{1F504} Compensated]`);
|
|
196
|
+
lines.push(` ${nodeId}_comp:::compensated`);
|
|
197
|
+
lines.push(` ${nodeId} -.->|rolled back| ${nodeId}_comp`);
|
|
198
|
+
}
|
|
199
|
+
prevNode = nodeId;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const finalStatus = this.getWorkflowFinalStatus(state.status);
|
|
203
|
+
lines.push(` ${prevNode} --> End([End: ${finalStatus}])`);
|
|
204
|
+
lines.push(` End:::${this.getStatusClass(state.status)}`);
|
|
205
|
+
lines.push("");
|
|
206
|
+
lines.push(" classDef started fill:#e3f2fd,stroke:#2196f3,stroke-width:2px");
|
|
207
|
+
lines.push(" classDef completed fill:#e8f5e9,stroke:#4caf50,stroke-width:2px");
|
|
208
|
+
lines.push(" classDef failed fill:#ffebee,stroke:#f44336,stroke-width:2px");
|
|
209
|
+
lines.push(" classDef compensated fill:#fff3e0,stroke:#ff9800,stroke-width:2px");
|
|
210
|
+
lines.push(
|
|
211
|
+
" classDef skipped fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1px,stroke-dasharray: 5 5"
|
|
212
|
+
);
|
|
213
|
+
lines.push(" classDef pending fill:#fafafa,stroke:#bdbdbd,stroke-width:1px");
|
|
214
|
+
lines.push(
|
|
215
|
+
" classDef suspended fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px,stroke-dasharray: 3 3"
|
|
216
|
+
);
|
|
217
|
+
return lines.join("\n");
|
|
218
|
+
}
|
|
219
|
+
sanitizeNodeId(name) {
|
|
220
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
221
|
+
}
|
|
222
|
+
buildStepLabel(step, showDetails) {
|
|
223
|
+
let label = step.name;
|
|
224
|
+
if (showDetails) {
|
|
225
|
+
const meta = [];
|
|
226
|
+
if (step.commit) {
|
|
227
|
+
meta.push("\u{1F4DD}");
|
|
228
|
+
}
|
|
229
|
+
if (step.retries && step.retries > 0) {
|
|
230
|
+
meta.push(`\u21BB${step.retries}`);
|
|
231
|
+
}
|
|
232
|
+
if (step.timeout) {
|
|
233
|
+
meta.push(`\u23F1${step.timeout}ms`);
|
|
234
|
+
}
|
|
235
|
+
if (step.when) {
|
|
236
|
+
meta.push("\u2753");
|
|
237
|
+
}
|
|
238
|
+
if (meta.length > 0) {
|
|
239
|
+
label += `<br/><small>${meta.join(" ")}</small>`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return label;
|
|
243
|
+
}
|
|
244
|
+
buildStepLabelWithStatus(step, exec, showDetails, showStatus) {
|
|
245
|
+
let label = step.name;
|
|
246
|
+
if (showStatus && exec) {
|
|
247
|
+
label += `<br/><small><b>${exec.status.toUpperCase()}</b></small>`;
|
|
248
|
+
if (exec.duration) {
|
|
249
|
+
label += `<br/><small>${exec.duration}ms</small>`;
|
|
250
|
+
}
|
|
251
|
+
if (exec.retries > 0) {
|
|
252
|
+
label += `<br/><small>Retries: ${exec.retries}</small>`;
|
|
253
|
+
}
|
|
254
|
+
} else if (showDetails) {
|
|
255
|
+
const meta = [];
|
|
256
|
+
if (step.commit) {
|
|
257
|
+
meta.push("\u{1F4DD}");
|
|
258
|
+
}
|
|
259
|
+
if (step.retries && step.retries > 0) {
|
|
260
|
+
meta.push(`\u21BB${step.retries}`);
|
|
261
|
+
}
|
|
262
|
+
if (step.timeout) {
|
|
263
|
+
meta.push(`\u23F1${step.timeout}ms`);
|
|
264
|
+
}
|
|
265
|
+
if (step.when) {
|
|
266
|
+
meta.push("\u2753");
|
|
267
|
+
}
|
|
268
|
+
if (meta.length > 0) {
|
|
269
|
+
label += `<br/><small>${meta.join(" ")}</small>`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return label;
|
|
273
|
+
}
|
|
274
|
+
getStatusClass(status) {
|
|
275
|
+
switch (status) {
|
|
276
|
+
case "completed":
|
|
277
|
+
return "completed";
|
|
278
|
+
case "failed":
|
|
279
|
+
return "failed";
|
|
280
|
+
case "compensated":
|
|
281
|
+
case "compensating":
|
|
282
|
+
return "compensated";
|
|
283
|
+
case "skipped":
|
|
284
|
+
return "skipped";
|
|
285
|
+
case "suspended":
|
|
286
|
+
return "suspended";
|
|
287
|
+
case "running":
|
|
288
|
+
return "started";
|
|
289
|
+
default:
|
|
290
|
+
return "pending";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
getWorkflowFinalStatus(status) {
|
|
294
|
+
switch (status) {
|
|
295
|
+
case "completed":
|
|
296
|
+
return "\u2705 Completed";
|
|
297
|
+
case "failed":
|
|
298
|
+
return "\u274C Failed";
|
|
299
|
+
case "rolled_back":
|
|
300
|
+
return "\u{1F504} Rolled Back";
|
|
301
|
+
case "suspended":
|
|
302
|
+
return "\u23F8 Suspended";
|
|
303
|
+
default:
|
|
304
|
+
return status;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
getNextNodeId(steps, currentIndex) {
|
|
308
|
+
const nextStep = steps[currentIndex + 1];
|
|
309
|
+
return nextStep ? this.sanitizeNodeId(nextStep.name) : "End";
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export {
|
|
314
|
+
MermaidGenerator
|
|
315
|
+
};
|
|
316
|
+
//# sourceMappingURL=chunk-WGDTB6OC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/visualization/MermaidGenerator.ts"],"sourcesContent":["import type { StepExecution, WorkflowDefinition, WorkflowState } from '../types'\n\n/**\n * Options for customizing Mermaid diagram generation.\n */\nexport interface MermaidOptions {\n /** Include detailed step information (retries, timeout, conditions). */\n showDetails?: boolean\n /** Include execution history with status colors. */\n showStatus?: boolean\n /** Render parallel groups visually. */\n showParallelGroups?: boolean\n /** Theme: 'default' | 'dark' | 'forest' | 'neutral'. */\n theme?: 'default' | 'dark' | 'forest' | 'neutral'\n}\n\n/**\n * Generates Mermaid flowchart diagrams from workflow definitions and execution states.\n *\n * **Features**:\n * - Workflow structure visualization (steps, parallel groups, conditions)\n * - Execution status overlay (completed, failed, compensated)\n * - Detailed metadata (retries, timeout, commit markers)\n *\n * **Use Cases**:\n * - Documentation generation\n * - Debugging workflow execution\n * - Visual workflow design validation\n */\nexport class MermaidGenerator {\n /**\n * Generates a Mermaid flowchart from a workflow definition.\n *\n * @param definition - The workflow definition to visualize\n * @param options - Customization options\n * @returns Mermaid diagram syntax as a string\n *\n * @example\n * ```typescript\n * const generator = new MermaidGenerator()\n * const diagram = generator.generateFromDefinition(workflow, {\n * showDetails: true,\n * showParallelGroups: true\n * })\n * console.log(diagram)\n * ```\n */\n generateFromDefinition<TInput, TData>(\n definition: WorkflowDefinition<TInput, TData>,\n options: MermaidOptions = {}\n ): string {\n const { showDetails = false, showParallelGroups = true, theme = 'default' } = options\n\n const lines: string[] = []\n lines.push(`%%{init: {'theme':'${theme}'}}%%`)\n lines.push('flowchart TD')\n lines.push(` Start([Start: ${definition.name}])`)\n\n let prevNode = 'Start'\n const parallelGroups = new Map<string, string[]>()\n\n // First pass: identify parallel groups\n if (showParallelGroups) {\n for (const step of definition.steps) {\n if (step.parallelGroup) {\n const group = parallelGroups.get(step.parallelGroup) || []\n group.push(step.name)\n parallelGroups.set(step.parallelGroup, group)\n }\n }\n }\n\n const processedGroups = new Set<string>()\n\n for (let i = 0; i < definition.steps.length; i++) {\n const step = definition.steps[i]!\n\n // Handle parallel groups\n if (step.parallelGroup && showParallelGroups) {\n if (processedGroups.has(step.parallelGroup)) {\n continue // Already processed this group\n }\n processedGroups.add(step.parallelGroup)\n\n const groupSteps = parallelGroups.get(step.parallelGroup)!\n const groupName = `ParallelGroup_${step.parallelGroup}`\n\n // Create subgraph for parallel execution\n lines.push(` subgraph ${groupName}[\" \"]`)\n lines.push(` direction LR`)\n\n for (const stepName of groupSteps) {\n const s = definition.steps.find((st) => st.name === stepName)!\n const nodeId = this.sanitizeNodeId(stepName)\n const label = this.buildStepLabel(s, showDetails)\n const shape = s.commit ? `[${label}]` : `(${label})`\n lines.push(` ${nodeId}${shape}`)\n\n if (s.compensate) {\n lines.push(` ${nodeId}_comp[🔄 Compensate]`)\n lines.push(` ${nodeId} -.->|on failure| ${nodeId}_comp`)\n }\n }\n\n lines.push(` end`)\n lines.push(` ${prevNode} --> ${groupName}`)\n prevNode = groupName\n } else if (!step.parallelGroup) {\n // Regular sequential step\n const nodeId = this.sanitizeNodeId(step.name)\n const label = this.buildStepLabel(step, showDetails)\n const shape = step.commit ? `[${label}]` : `(${label})`\n\n lines.push(` ${nodeId}${shape}`)\n\n // Handle conditional steps\n if (step.when) {\n const condId = `${nodeId}_cond`\n lines.push(` ${condId}{Condition?}`)\n lines.push(` ${prevNode} --> ${condId}`)\n lines.push(` ${condId} -->|yes| ${nodeId}`)\n lines.push(` ${condId} -->|no| ${this.getNextNodeId(definition.steps, i)}`)\n prevNode = nodeId\n } else {\n lines.push(` ${prevNode} --> ${nodeId}`)\n prevNode = nodeId\n }\n\n // Show compensation\n if (step.compensate) {\n lines.push(` ${nodeId}_comp[🔄 Compensate]`)\n lines.push(` ${nodeId} -.->|on failure| ${nodeId}_comp`)\n }\n }\n }\n\n lines.push(` ${prevNode} --> End([End])`)\n\n // Add styling\n lines.push('')\n lines.push(' classDef commitStep fill:#e1f5e1,stroke:#4caf50,stroke-width:2px')\n lines.push(' classDef normalStep fill:#e3f2fd,stroke:#2196f3,stroke-width:2px')\n lines.push(\n ' classDef compensateStep fill:#fff3e0,stroke:#ff9800,stroke-width:1px,stroke-dasharray: 5 5'\n )\n\n return lines.join('\\n')\n }\n\n /**\n * Generates a Mermaid flowchart from a workflow execution state.\n * Overlays execution status (completed, failed, compensated) on top of the structure.\n *\n * @param definition - The workflow definition\n * @param state - The current execution state\n * @param options - Customization options\n * @returns Mermaid diagram syntax with status overlay\n *\n * @example\n * ```typescript\n * const diagram = generator.generateFromState(workflow, executionState, {\n * showStatus: true,\n * showDetails: true\n * })\n * ```\n */\n generateFromState<TInput, TData>(\n definition: WorkflowDefinition<TInput, TData>,\n state: WorkflowState<TInput, TData>,\n options: MermaidOptions = {}\n ): string {\n const {\n showDetails = true,\n showStatus = true,\n showParallelGroups = true,\n theme = 'default',\n } = options\n\n const lines: string[] = []\n lines.push(`%%{init: {'theme':'${theme}'}}%%`)\n lines.push('flowchart TD')\n lines.push(` Start([Start: ${definition.name}])`)\n lines.push(` Start:::started`)\n\n let prevNode = 'Start'\n const parallelGroups = new Map<string, string[]>()\n const executionMap = new Map<string, StepExecution>()\n\n // Build execution map\n for (const exec of state.history) {\n executionMap.set(exec.name, exec)\n }\n\n // Identify parallel groups\n if (showParallelGroups) {\n for (const step of definition.steps) {\n if (step.parallelGroup) {\n const group = parallelGroups.get(step.parallelGroup) || []\n group.push(step.name)\n parallelGroups.set(step.parallelGroup, group)\n }\n }\n }\n\n const processedGroups = new Set<string>()\n\n for (let i = 0; i < definition.steps.length; i++) {\n const step = definition.steps[i]!\n\n // Handle parallel groups\n if (step.parallelGroup && showParallelGroups) {\n if (processedGroups.has(step.parallelGroup)) {\n continue\n }\n processedGroups.add(step.parallelGroup)\n\n const groupSteps = parallelGroups.get(step.parallelGroup)!\n const groupName = `ParallelGroup_${step.parallelGroup}`\n\n lines.push(` subgraph ${groupName}[\" \"]`)\n lines.push(` direction LR`)\n\n for (const stepName of groupSteps) {\n const s = definition.steps.find((st) => st.name === stepName)!\n const nodeId = this.sanitizeNodeId(stepName)\n const exec = executionMap.get(stepName)\n const label = this.buildStepLabelWithStatus(s, exec, showDetails, showStatus)\n const shape = s.commit ? `[${label}]` : `(${label})`\n\n lines.push(` ${nodeId}${shape}`)\n\n if (exec && showStatus) {\n const statusClass = this.getStatusClass(exec.status)\n lines.push(` ${nodeId}:::${statusClass}`)\n }\n\n if (s.compensate && exec?.status === 'compensated') {\n lines.push(` ${nodeId}_comp[🔄 Compensated]`)\n lines.push(` ${nodeId}_comp:::compensated`)\n lines.push(` ${nodeId} -.->|rolled back| ${nodeId}_comp`)\n }\n }\n\n lines.push(` end`)\n lines.push(` ${prevNode} --> ${groupName}`)\n prevNode = groupName\n } else if (!step.parallelGroup) {\n const nodeId = this.sanitizeNodeId(step.name)\n const exec = executionMap.get(step.name)\n const label = this.buildStepLabelWithStatus(step, exec, showDetails, showStatus)\n const shape = step.commit ? `[${label}]` : `(${label})`\n\n lines.push(` ${nodeId}${shape}`)\n\n if (exec && showStatus) {\n const statusClass = this.getStatusClass(exec.status)\n lines.push(` ${nodeId}:::${statusClass}`)\n }\n\n if (step.when) {\n const condId = `${nodeId}_cond`\n const skipped = exec?.status === 'skipped'\n lines.push(` ${condId}{Condition?}`)\n lines.push(` ${prevNode} --> ${condId}`)\n\n if (skipped) {\n lines.push(` ${condId}:::skipped`)\n lines.push(` ${condId} -->|no| ${nodeId}`)\n } else {\n lines.push(` ${condId} -->|yes| ${nodeId}`)\n }\n } else {\n lines.push(` ${prevNode} --> ${nodeId}`)\n }\n\n if (step.compensate && exec?.status === 'compensated') {\n lines.push(` ${nodeId}_comp[🔄 Compensated]`)\n lines.push(` ${nodeId}_comp:::compensated`)\n lines.push(` ${nodeId} -.->|rolled back| ${nodeId}_comp`)\n }\n\n prevNode = nodeId\n }\n }\n\n const finalStatus = this.getWorkflowFinalStatus(state.status)\n lines.push(` ${prevNode} --> End([End: ${finalStatus}])`)\n lines.push(` End:::${this.getStatusClass(state.status)}`)\n\n // Add status-based styling\n lines.push('')\n lines.push(' classDef started fill:#e3f2fd,stroke:#2196f3,stroke-width:2px')\n lines.push(' classDef completed fill:#e8f5e9,stroke:#4caf50,stroke-width:2px')\n lines.push(' classDef failed fill:#ffebee,stroke:#f44336,stroke-width:2px')\n lines.push(' classDef compensated fill:#fff3e0,stroke:#ff9800,stroke-width:2px')\n lines.push(\n ' classDef skipped fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1px,stroke-dasharray: 5 5'\n )\n lines.push(' classDef pending fill:#fafafa,stroke:#bdbdbd,stroke-width:1px')\n lines.push(\n ' classDef suspended fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px,stroke-dasharray: 3 3'\n )\n\n return lines.join('\\n')\n }\n\n private sanitizeNodeId(name: string): string {\n return name.replace(/[^a-zA-Z0-9_]/g, '_')\n }\n\n private buildStepLabel(step: any, showDetails: boolean): string {\n let label = step.name\n\n if (showDetails) {\n const meta: string[] = []\n if (step.commit) {\n meta.push('📝')\n }\n if (step.retries && step.retries > 0) {\n meta.push(`↻${step.retries}`)\n }\n if (step.timeout) {\n meta.push(`⏱${step.timeout}ms`)\n }\n if (step.when) {\n meta.push('❓')\n }\n\n if (meta.length > 0) {\n label += `<br/><small>${meta.join(' ')}</small>`\n }\n }\n\n return label\n }\n\n private buildStepLabelWithStatus(\n step: any,\n exec: StepExecution | undefined,\n showDetails: boolean,\n showStatus: boolean\n ): string {\n let label = step.name\n\n if (showStatus && exec) {\n label += `<br/><small><b>${exec.status.toUpperCase()}</b></small>`\n\n if (exec.duration) {\n label += `<br/><small>${exec.duration}ms</small>`\n }\n\n if (exec.retries > 0) {\n label += `<br/><small>Retries: ${exec.retries}</small>`\n }\n } else if (showDetails) {\n const meta: string[] = []\n if (step.commit) {\n meta.push('📝')\n }\n if (step.retries && step.retries > 0) {\n meta.push(`↻${step.retries}`)\n }\n if (step.timeout) {\n meta.push(`⏱${step.timeout}ms`)\n }\n if (step.when) {\n meta.push('❓')\n }\n\n if (meta.length > 0) {\n label += `<br/><small>${meta.join(' ')}</small>`\n }\n }\n\n return label\n }\n\n private getStatusClass(status: string): string {\n switch (status) {\n case 'completed':\n return 'completed'\n case 'failed':\n return 'failed'\n case 'compensated':\n case 'compensating':\n return 'compensated'\n case 'skipped':\n return 'skipped'\n case 'suspended':\n return 'suspended'\n case 'running':\n return 'started'\n default:\n return 'pending'\n }\n }\n\n private getWorkflowFinalStatus(status: string): string {\n switch (status) {\n case 'completed':\n return '✅ Completed'\n case 'failed':\n return '❌ Failed'\n case 'rolled_back':\n return '🔄 Rolled Back'\n case 'suspended':\n return '⏸ Suspended'\n default:\n return status\n }\n }\n\n private getNextNodeId(steps: any[], currentIndex: number): string {\n const nextStep = steps[currentIndex + 1]\n return nextStep ? this.sanitizeNodeId(nextStep.name) : 'End'\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,12 +4,21 @@ var BunSQLiteStorage = (_class = class {
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
__init() {this.initialized = false}
|
|
7
|
+
/**
|
|
8
|
+
* Creates a new instance of BunSQLiteStorage.
|
|
9
|
+
*
|
|
10
|
+
* @param options - Configuration for the database connection and table naming.
|
|
11
|
+
*/
|
|
7
12
|
constructor(options = {}) {;_class.prototype.__init.call(this);
|
|
8
13
|
this.db = new (0, _bunsqlite.Database)(_nullishCoalesce(options.path, () => ( ":memory:")));
|
|
9
14
|
this.tableName = _nullishCoalesce(options.tableName, () => ( "flux_workflows"));
|
|
10
15
|
}
|
|
11
16
|
/**
|
|
12
|
-
*
|
|
17
|
+
* Initializes the database schema and required indexes.
|
|
18
|
+
*
|
|
19
|
+
* This method is idempotent and will be called automatically by other operations if not invoked manually.
|
|
20
|
+
*
|
|
21
|
+
* @throws {Error} If the database schema cannot be created or indexes fail to initialize.
|
|
13
22
|
*/
|
|
14
23
|
async init() {
|
|
15
24
|
if (this.initialized) {
|
|
@@ -27,7 +36,9 @@ var BunSQLiteStorage = (_class = class {
|
|
|
27
36
|
error TEXT,
|
|
28
37
|
created_at TEXT NOT NULL,
|
|
29
38
|
updated_at TEXT NOT NULL,
|
|
30
|
-
completed_at TEXT
|
|
39
|
+
completed_at TEXT,
|
|
40
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
41
|
+
definition_version TEXT
|
|
31
42
|
)
|
|
32
43
|
`);
|
|
33
44
|
this.db.run(`
|
|
@@ -45,14 +56,19 @@ var BunSQLiteStorage = (_class = class {
|
|
|
45
56
|
this.initialized = true;
|
|
46
57
|
}
|
|
47
58
|
/**
|
|
48
|
-
*
|
|
59
|
+
* Persists or updates a workflow state in the database.
|
|
60
|
+
*
|
|
61
|
+
* Uses an "INSERT OR REPLACE" strategy to ensure the latest state is always stored for a given ID.
|
|
62
|
+
*
|
|
63
|
+
* @param state - The current state of the workflow to be saved.
|
|
64
|
+
* @throws {Error} If the database write operation fails or serialization errors occur.
|
|
49
65
|
*/
|
|
50
66
|
async save(state) {
|
|
51
67
|
await this.init();
|
|
52
68
|
const stmt = this.db.prepare(`
|
|
53
69
|
INSERT OR REPLACE INTO ${this.tableName}
|
|
54
|
-
(id, name, status, input, data, current_step, history, error, created_at, updated_at, completed_at)
|
|
55
|
-
VALUES ($id, $name, $status, $input, $data, $currentStep, $history, $error, $createdAt, $updatedAt, $completedAt)
|
|
70
|
+
(id, name, status, input, data, current_step, history, error, created_at, updated_at, completed_at, version, definition_version)
|
|
71
|
+
VALUES ($id, $name, $status, $input, $data, $currentStep, $history, $error, $createdAt, $updatedAt, $completedAt, $version, $definitionVersion)
|
|
56
72
|
`);
|
|
57
73
|
stmt.run({
|
|
58
74
|
$id: state.id,
|
|
@@ -65,11 +81,17 @@ var BunSQLiteStorage = (_class = class {
|
|
|
65
81
|
$error: _nullishCoalesce(state.error, () => ( null)),
|
|
66
82
|
$createdAt: state.createdAt.toISOString(),
|
|
67
83
|
$updatedAt: state.updatedAt.toISOString(),
|
|
68
|
-
$completedAt: _nullishCoalesce(_optionalChain([state, 'access', _2 => _2.completedAt, 'optionalAccess', _3 => _3.toISOString, 'call', _4 => _4()]), () => ( null))
|
|
84
|
+
$completedAt: _nullishCoalesce(_optionalChain([state, 'access', _2 => _2.completedAt, 'optionalAccess', _3 => _3.toISOString, 'call', _4 => _4()]), () => ( null)),
|
|
85
|
+
$version: state.version,
|
|
86
|
+
$definitionVersion: _nullishCoalesce(state.definitionVersion, () => ( null))
|
|
69
87
|
});
|
|
70
88
|
}
|
|
71
89
|
/**
|
|
72
|
-
*
|
|
90
|
+
* Retrieves a workflow state by its unique identifier.
|
|
91
|
+
*
|
|
92
|
+
* @param id - The unique ID of the workflow to load.
|
|
93
|
+
* @returns The reconstructed workflow state, or null if no record is found.
|
|
94
|
+
* @throws {Error} If the database query fails or deserialization of stored JSON fails.
|
|
73
95
|
*/
|
|
74
96
|
async load(id) {
|
|
75
97
|
await this.init();
|
|
@@ -83,7 +105,13 @@ var BunSQLiteStorage = (_class = class {
|
|
|
83
105
|
return this.rowToState(row);
|
|
84
106
|
}
|
|
85
107
|
/**
|
|
86
|
-
*
|
|
108
|
+
* Lists workflow states based on the provided filtering criteria.
|
|
109
|
+
*
|
|
110
|
+
* Results are returned in descending order of creation time.
|
|
111
|
+
*
|
|
112
|
+
* @param filter - Criteria for filtering and paginating the results.
|
|
113
|
+
* @returns An array of workflow states matching the filter.
|
|
114
|
+
* @throws {Error} If the database query fails.
|
|
87
115
|
*/
|
|
88
116
|
async list(filter) {
|
|
89
117
|
await this.init();
|
|
@@ -105,12 +133,16 @@ var BunSQLiteStorage = (_class = class {
|
|
|
105
133
|
params.$status = filter.status;
|
|
106
134
|
}
|
|
107
135
|
}
|
|
136
|
+
if (_optionalChain([filter, 'optionalAccess', _7 => _7.version])) {
|
|
137
|
+
query += " AND definition_version = $version";
|
|
138
|
+
params.$version = filter.version;
|
|
139
|
+
}
|
|
108
140
|
query += " ORDER BY created_at DESC";
|
|
109
|
-
if (_optionalChain([filter, 'optionalAccess',
|
|
141
|
+
if (_optionalChain([filter, 'optionalAccess', _8 => _8.limit])) {
|
|
110
142
|
query += " LIMIT $limit";
|
|
111
143
|
params.$limit = filter.limit;
|
|
112
144
|
}
|
|
113
|
-
if (_optionalChain([filter, 'optionalAccess',
|
|
145
|
+
if (_optionalChain([filter, 'optionalAccess', _9 => _9.offset])) {
|
|
114
146
|
query += " OFFSET $offset";
|
|
115
147
|
params.$offset = filter.offset;
|
|
116
148
|
}
|
|
@@ -119,7 +151,10 @@ var BunSQLiteStorage = (_class = class {
|
|
|
119
151
|
return rows.map((row) => this.rowToState(row));
|
|
120
152
|
}
|
|
121
153
|
/**
|
|
122
|
-
*
|
|
154
|
+
* Deletes a workflow state from the database.
|
|
155
|
+
*
|
|
156
|
+
* @param id - The unique ID of the workflow to delete.
|
|
157
|
+
* @throws {Error} If the database deletion fails.
|
|
123
158
|
*/
|
|
124
159
|
async delete(id) {
|
|
125
160
|
await this.init();
|
|
@@ -129,14 +164,20 @@ var BunSQLiteStorage = (_class = class {
|
|
|
129
164
|
stmt.run({ $id: id });
|
|
130
165
|
}
|
|
131
166
|
/**
|
|
132
|
-
*
|
|
167
|
+
* Closes the database connection and resets the initialization state.
|
|
168
|
+
*
|
|
169
|
+
* @throws {Error} If the database connection cannot be closed cleanly.
|
|
133
170
|
*/
|
|
134
171
|
async close() {
|
|
135
172
|
this.db.close();
|
|
136
173
|
this.initialized = false;
|
|
137
174
|
}
|
|
138
175
|
/**
|
|
139
|
-
*
|
|
176
|
+
* Converts a raw database row into a structured WorkflowState object.
|
|
177
|
+
*
|
|
178
|
+
* @param row - The raw SQLite row data.
|
|
179
|
+
* @returns The parsed workflow state.
|
|
180
|
+
* @private
|
|
140
181
|
*/
|
|
141
182
|
rowToState(row) {
|
|
142
183
|
return {
|
|
@@ -150,17 +191,25 @@ var BunSQLiteStorage = (_class = class {
|
|
|
150
191
|
error: _nullishCoalesce(row.error, () => ( void 0)),
|
|
151
192
|
createdAt: new Date(row.created_at),
|
|
152
193
|
updatedAt: new Date(row.updated_at),
|
|
153
|
-
completedAt: row.completed_at ? new Date(row.completed_at) : void 0
|
|
194
|
+
completedAt: row.completed_at ? new Date(row.completed_at) : void 0,
|
|
195
|
+
version: row.version,
|
|
196
|
+
definitionVersion: _nullishCoalesce(row.definition_version, () => ( void 0))
|
|
154
197
|
};
|
|
155
198
|
}
|
|
156
199
|
/**
|
|
157
|
-
*
|
|
200
|
+
* Provides direct access to the underlying Bun SQLite Database instance.
|
|
201
|
+
*
|
|
202
|
+
* Useful for performing custom queries or maintenance tasks.
|
|
203
|
+
*
|
|
204
|
+
* @returns The raw Database instance.
|
|
158
205
|
*/
|
|
159
206
|
getDatabase() {
|
|
160
207
|
return this.db;
|
|
161
208
|
}
|
|
162
209
|
/**
|
|
163
|
-
*
|
|
210
|
+
* Performs a VACUUM operation to reclaim unused space and defragment the database.
|
|
211
|
+
*
|
|
212
|
+
* @throws {Error} If the VACUUM operation fails.
|
|
164
213
|
*/
|
|
165
214
|
vacuum() {
|
|
166
215
|
this.db.run("VACUUM");
|
|
@@ -170,4 +219,4 @@ var BunSQLiteStorage = (_class = class {
|
|
|
170
219
|
|
|
171
220
|
|
|
172
221
|
exports.BunSQLiteStorage = BunSQLiteStorage;
|
|
173
|
-
//# sourceMappingURL=chunk-
|
|
222
|
+
//# sourceMappingURL=chunk-YXBEYVGY.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/carl/Dev/Carl/gravito-core-ci-fix/packages/flux/dist/chunk-YXBEYVGY.cjs","../src/storage/BunSQLiteStorage.ts"],"names":[],"mappings":"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","file":"/Users/carl/Dev/Carl/gravito-core-ci-fix/packages/flux/dist/chunk-YXBEYVGY.cjs","sourcesContent":[null,"import { Database } from 'bun:sqlite'\nimport type { WorkflowFilter, WorkflowState, WorkflowStorage } from '../types'\n\n/**\n * Configuration options for the Bun SQLite storage adapter.\n */\nexport interface BunSQLiteStorageOptions {\n /**\n * Path to the SQLite database file.\n * Use ':memory:' for an ephemeral in-memory database.\n */\n path?: string\n /**\n * Name of the table used to store workflow states.\n */\n tableName?: string\n}\n\n/**\n * BunSQLiteStorage provides a persistent storage backend for Flux workflows using Bun's native SQLite module.\n *\n * It handles automatic table creation, indexing for performance, and serialization of workflow state\n * into a relational format.\n *\n * @example\n * ```typescript\n * const storage = new BunSQLiteStorage({\n * path: './workflows.db',\n * tableName: 'my_workflows'\n * });\n * await storage.init();\n * ```\n */\nexport class BunSQLiteStorage implements WorkflowStorage {\n private db: Database\n private tableName: string\n private initialized = false\n\n /**\n * Creates a new instance of BunSQLiteStorage.\n *\n * @param options - Configuration for the database connection and table naming.\n */\n constructor(options: BunSQLiteStorageOptions = {}) {\n this.db = new Database(options.path ?? ':memory:')\n this.tableName = options.tableName ?? 'flux_workflows'\n }\n\n /**\n * Initializes the database schema and required indexes.\n *\n * This method is idempotent and will be called automatically by other operations if not invoked manually.\n *\n * @throws {Error} If the database schema cannot be created or indexes fail to initialize.\n */\n async init(): Promise<void> {\n if (this.initialized) {\n return\n }\n\n this.db.run(`\n CREATE TABLE IF NOT EXISTS ${this.tableName} (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n status TEXT NOT NULL,\n input TEXT NOT NULL,\n data TEXT NOT NULL,\n current_step INTEGER NOT NULL,\n history TEXT NOT NULL,\n error TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n completed_at TEXT,\n version INTEGER NOT NULL DEFAULT 1,\n definition_version TEXT\n )\n `)\n\n this.db.run(`\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_name \n ON ${this.tableName}(name)\n `)\n this.db.run(`\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_status \n ON ${this.tableName}(status)\n `)\n this.db.run(`\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_created \n ON ${this.tableName}(created_at DESC)\n `)\n\n this.initialized = true\n }\n\n /**\n * Persists or updates a workflow state in the database.\n *\n * Uses an \"INSERT OR REPLACE\" strategy to ensure the latest state is always stored for a given ID.\n *\n * @param state - The current state of the workflow to be saved.\n * @throws {Error} If the database write operation fails or serialization errors occur.\n */\n async save(state: WorkflowState): Promise<void> {\n await this.init()\n\n const stmt = this.db.prepare(`\n INSERT OR REPLACE INTO ${this.tableName} \n (id, name, status, input, data, current_step, history, error, created_at, updated_at, completed_at, version, definition_version)\n VALUES ($id, $name, $status, $input, $data, $currentStep, $history, $error, $createdAt, $updatedAt, $completedAt, $version, $definitionVersion)\n `)\n\n stmt.run({\n $id: state.id,\n $name: state.name,\n $status: state.status,\n $input: JSON.stringify(state.input),\n $data: JSON.stringify(state.data),\n $currentStep: state.currentStep,\n $history: JSON.stringify(state.history),\n $error: state.error ?? null,\n $createdAt: state.createdAt.toISOString(),\n $updatedAt: state.updatedAt.toISOString(),\n $completedAt: state.completedAt?.toISOString() ?? null,\n $version: state.version,\n $definitionVersion: state.definitionVersion ?? null,\n })\n }\n\n /**\n * Retrieves a workflow state by its unique identifier.\n *\n * @param id - The unique ID of the workflow to load.\n * @returns The reconstructed workflow state, or null if no record is found.\n * @throws {Error} If the database query fails or deserialization of stored JSON fails.\n */\n async load(id: string): Promise<WorkflowState | null> {\n await this.init()\n\n const stmt = this.db.prepare(`\n SELECT * FROM ${this.tableName} WHERE id = $id\n `)\n\n const row = stmt.get({ $id: id }) as SQLiteRow | null\n\n if (!row) {\n return null\n }\n\n return this.rowToState(row)\n }\n\n /**\n * Lists workflow states based on the provided filtering criteria.\n *\n * Results are returned in descending order of creation time.\n *\n * @param filter - Criteria for filtering and paginating the results.\n * @returns An array of workflow states matching the filter.\n * @throws {Error} If the database query fails.\n */\n async list(filter?: WorkflowFilter): Promise<WorkflowState[]> {\n await this.init()\n\n let query = `SELECT * FROM ${this.tableName} WHERE 1=1`\n const params: Record<string, unknown> = {}\n\n if (filter?.name) {\n query += ' AND name = $name'\n params.$name = filter.name\n }\n\n if (filter?.status) {\n if (Array.isArray(filter.status)) {\n const placeholders = filter.status.map((_, i) => `$status${i}`).join(', ')\n query += ` AND status IN (${placeholders})`\n filter.status.forEach((s, i) => {\n params[`$status${i}`] = s\n })\n } else {\n query += ' AND status = $status'\n params.$status = filter.status\n }\n }\n\n if (filter?.version) {\n query += ' AND definition_version = $version'\n params.$version = filter.version\n }\n\n query += ' ORDER BY created_at DESC'\n\n if (filter?.limit) {\n query += ' LIMIT $limit'\n params.$limit = filter.limit\n }\n\n if (filter?.offset) {\n query += ' OFFSET $offset'\n params.$offset = filter.offset\n }\n\n const stmt = this.db.prepare(query)\n const rows = stmt.all(params as Record<string, any>) as SQLiteRow[]\n\n return rows.map((row) => this.rowToState(row))\n }\n\n /**\n * Deletes a workflow state from the database.\n *\n * @param id - The unique ID of the workflow to delete.\n * @throws {Error} If the database deletion fails.\n */\n async delete(id: string): Promise<void> {\n await this.init()\n\n const stmt = this.db.prepare(`\n DELETE FROM ${this.tableName} WHERE id = $id\n `)\n\n stmt.run({ $id: id })\n }\n\n /**\n * Closes the database connection and resets the initialization state.\n *\n * @throws {Error} If the database connection cannot be closed cleanly.\n */\n async close(): Promise<void> {\n this.db.close()\n this.initialized = false\n }\n\n /**\n * Converts a raw database row into a structured WorkflowState object.\n *\n * @param row - The raw SQLite row data.\n * @returns The parsed workflow state.\n * @private\n */\n private rowToState(row: SQLiteRow): WorkflowState {\n return {\n id: row.id,\n name: row.name,\n status: row.status as WorkflowState['status'],\n input: JSON.parse(row.input),\n data: JSON.parse(row.data),\n currentStep: row.current_step,\n history: JSON.parse(row.history),\n error: row.error ?? undefined,\n createdAt: new Date(row.created_at),\n updatedAt: new Date(row.updated_at),\n completedAt: row.completed_at ? new Date(row.completed_at) : undefined,\n version: row.version,\n definitionVersion: row.definition_version ?? undefined,\n }\n }\n\n /**\n * Provides direct access to the underlying Bun SQLite Database instance.\n *\n * Useful for performing custom queries or maintenance tasks.\n *\n * @returns The raw Database instance.\n */\n getDatabase(): Database {\n return this.db\n }\n\n /**\n * Performs a VACUUM operation to reclaim unused space and defragment the database.\n *\n * @throws {Error} If the VACUUM operation fails.\n */\n vacuum(): void {\n this.db.run('VACUUM')\n }\n}\n\n/**\n * Internal representation of a workflow record in the SQLite database.\n */\ninterface SQLiteRow {\n id: string\n name: string\n status: string\n input: string\n data: string\n current_step: number\n history: string\n error: string | null\n created_at: string\n updated_at: string\n completed_at: string | null\n version: number\n definition_version: string | null\n}\n"]}
|