@multiplier-labs/stepflow 0.3.1
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/.github/workflows/publish.yml +30 -0
- package/README.md +408 -0
- package/dist/chunk-FQCT3I2A.js +1512 -0
- package/dist/chunk-FQCT3I2A.js.map +1 -0
- package/dist/chunk-GXY66V7Q.js +111 -0
- package/dist/chunk-GXY66V7Q.js.map +1 -0
- package/dist/chunk-U5QGN3RT.js +715 -0
- package/dist/chunk-U5QGN3RT.js.map +1 -0
- package/dist/chunk-XCF4PLN7.js +575 -0
- package/dist/chunk-XCF4PLN7.js.map +1 -0
- package/dist/events/index.d.ts +346 -0
- package/dist/events/index.js +11 -0
- package/dist/events/index.js.map +1 -0
- package/dist/index-CPfGsmrp.d.ts +567 -0
- package/dist/index.d.ts +748 -0
- package/dist/index.js +1707 -0
- package/dist/index.js.map +1 -0
- package/dist/scheduler/index.d.ts +6 -0
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/storage/index.d.ts +402 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/types-BVShq7zT.d.ts +150 -0
- package/dist/types-BlwFEGzj.d.ts +276 -0
- package/dist/types-DzkEqnQJ.d.ts +75 -0
- package/docs/github-packages-setup.md +97 -0
- package/docs/stepflow-api-reference.md +1662 -0
- package/package.json +85 -0
- package/src/core/engine.test.ts +1421 -0
- package/src/core/engine.ts +717 -0
- package/src/core/index.ts +7 -0
- package/src/core/orchestrator.ts +590 -0
- package/src/core/types.ts +242 -0
- package/src/events/index.ts +8 -0
- package/src/events/memory.test.ts +109 -0
- package/src/events/memory.ts +95 -0
- package/src/events/socketio.test.ts +428 -0
- package/src/events/socketio.ts +269 -0
- package/src/events/types.ts +121 -0
- package/src/events/webhook.test.ts +695 -0
- package/src/events/webhook.ts +591 -0
- package/src/index.ts +270 -0
- package/src/planning/index.ts +52 -0
- package/src/planning/planner.ts +601 -0
- package/src/planning/planning.test.ts +790 -0
- package/src/planning/registry.test.ts +445 -0
- package/src/planning/registry.ts +369 -0
- package/src/planning/types.ts +570 -0
- package/src/scheduler/cron.test.ts +480 -0
- package/src/scheduler/cron.ts +449 -0
- package/src/scheduler/index.ts +18 -0
- package/src/scheduler/postgres-persistence.test.ts +906 -0
- package/src/scheduler/postgres-persistence.ts +546 -0
- package/src/scheduler/sqlite-persistence.test.ts +235 -0
- package/src/scheduler/sqlite-persistence.ts +268 -0
- package/src/scheduler/types.ts +78 -0
- package/src/storage/index.ts +27 -0
- package/src/storage/memory.test.ts +399 -0
- package/src/storage/memory.ts +248 -0
- package/src/storage/postgres.ts +1550 -0
- package/src/storage/sqlite.test.ts +593 -0
- package/src/storage/sqlite.ts +662 -0
- package/src/storage/types.ts +368 -0
- package/src/utils/errors.test.ts +152 -0
- package/src/utils/errors.ts +191 -0
- package/src/utils/id.ts +21 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.test.ts +127 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/postgres-deps.ts +63 -0
- package/src/utils/retry.test.ts +197 -0
- package/src/utils/retry.ts +121 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { MemoryStorageAdapter } from "./memory";
|
|
3
|
+
import type { WorkflowRunRecord, WorkflowRunStepRecord } from "./types";
|
|
4
|
+
|
|
5
|
+
describe("MemoryStorageAdapter", () => {
|
|
6
|
+
let storage: MemoryStorageAdapter;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
storage = new MemoryStorageAdapter();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("createRun / getRun", () => {
|
|
13
|
+
it("should create and retrieve a run", async () => {
|
|
14
|
+
const run = await storage.createRun({
|
|
15
|
+
kind: "test.workflow",
|
|
16
|
+
status: "queued",
|
|
17
|
+
input: { x: 1 },
|
|
18
|
+
metadata: { userId: "u1" },
|
|
19
|
+
context: {},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(run.id).toBeDefined();
|
|
23
|
+
expect(run.kind).toBe("test.workflow");
|
|
24
|
+
expect(run.createdAt).toBeInstanceOf(Date);
|
|
25
|
+
|
|
26
|
+
const fetched = await storage.getRun(run.id);
|
|
27
|
+
expect(fetched).toEqual(run);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should return null for non-existent run", async () => {
|
|
31
|
+
const run = await storage.getRun("nonexistent");
|
|
32
|
+
expect(run).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("updateRun", () => {
|
|
37
|
+
it("should update run fields", async () => {
|
|
38
|
+
const run = await storage.createRun({
|
|
39
|
+
kind: "test",
|
|
40
|
+
status: "queued",
|
|
41
|
+
input: {},
|
|
42
|
+
metadata: {},
|
|
43
|
+
context: {},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await storage.updateRun(run.id, {
|
|
47
|
+
status: "running",
|
|
48
|
+
startedAt: new Date(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const updated = await storage.getRun(run.id);
|
|
52
|
+
expect(updated?.status).toBe("running");
|
|
53
|
+
expect(updated?.startedAt).toBeInstanceOf(Date);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should do nothing for non-existent run", async () => {
|
|
57
|
+
await expect(
|
|
58
|
+
storage.updateRun("nonexistent", { status: "failed" }),
|
|
59
|
+
).resolves.not.toThrow();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("listRuns", () => {
|
|
64
|
+
beforeEach(async () => {
|
|
65
|
+
await storage.createRun({
|
|
66
|
+
kind: "a",
|
|
67
|
+
status: "queued",
|
|
68
|
+
input: {},
|
|
69
|
+
metadata: {},
|
|
70
|
+
context: {},
|
|
71
|
+
});
|
|
72
|
+
await storage.createRun({
|
|
73
|
+
kind: "a",
|
|
74
|
+
status: "running",
|
|
75
|
+
input: {},
|
|
76
|
+
metadata: {},
|
|
77
|
+
context: {},
|
|
78
|
+
});
|
|
79
|
+
await storage.createRun({
|
|
80
|
+
kind: "b",
|
|
81
|
+
status: "succeeded",
|
|
82
|
+
input: {},
|
|
83
|
+
metadata: {},
|
|
84
|
+
context: {},
|
|
85
|
+
});
|
|
86
|
+
await storage.createRun({
|
|
87
|
+
kind: "a",
|
|
88
|
+
status: "failed",
|
|
89
|
+
input: {},
|
|
90
|
+
metadata: {},
|
|
91
|
+
context: {},
|
|
92
|
+
parentRunId: "parent-1",
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should list all runs with default pagination", async () => {
|
|
97
|
+
const result = await storage.listRuns();
|
|
98
|
+
expect(result.items).toHaveLength(4);
|
|
99
|
+
expect(result.total).toBe(4);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should filter by kind", async () => {
|
|
103
|
+
const result = await storage.listRuns({ kind: "a" });
|
|
104
|
+
expect(result.items).toHaveLength(3);
|
|
105
|
+
expect(result.items.every((r) => r.kind === "a")).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should filter by single status", async () => {
|
|
109
|
+
const result = await storage.listRuns({ status: "queued" });
|
|
110
|
+
expect(result.items).toHaveLength(1);
|
|
111
|
+
expect(result.items[0].status).toBe("queued");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should filter by multiple statuses", async () => {
|
|
115
|
+
const result = await storage.listRuns({ status: ["queued", "running"] });
|
|
116
|
+
expect(result.items).toHaveLength(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should filter by parentRunId", async () => {
|
|
120
|
+
const result = await storage.listRuns({ parentRunId: "parent-1" });
|
|
121
|
+
expect(result.items).toHaveLength(1);
|
|
122
|
+
expect(result.items[0].parentRunId).toBe("parent-1");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should apply limit and offset", async () => {
|
|
126
|
+
const result = await storage.listRuns({ limit: 2, offset: 1 });
|
|
127
|
+
expect(result.items).toHaveLength(2);
|
|
128
|
+
expect(result.total).toBe(4);
|
|
129
|
+
expect(result.limit).toBe(2);
|
|
130
|
+
expect(result.offset).toBe(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should sort by createdAt ascending", async () => {
|
|
134
|
+
const result = await storage.listRuns({
|
|
135
|
+
orderBy: "createdAt",
|
|
136
|
+
orderDirection: "asc",
|
|
137
|
+
});
|
|
138
|
+
const times = result.items.map((r) => r.createdAt.getTime());
|
|
139
|
+
for (let i = 1; i < times.length; i++) {
|
|
140
|
+
expect(times[i]).toBeGreaterThanOrEqual(times[i - 1]);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("createStep / getStep / updateStep", () => {
|
|
146
|
+
it("should create and retrieve a step", async () => {
|
|
147
|
+
const step = await storage.createStep({
|
|
148
|
+
runId: "run-1",
|
|
149
|
+
stepKey: "step1",
|
|
150
|
+
stepName: "Step 1",
|
|
151
|
+
status: "running",
|
|
152
|
+
attempt: 1,
|
|
153
|
+
startedAt: new Date(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(step.id).toBeDefined();
|
|
157
|
+
expect(step.stepKey).toBe("step1");
|
|
158
|
+
|
|
159
|
+
const fetched = await storage.getStep(step.id);
|
|
160
|
+
expect(fetched).toEqual(step);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should return null for non-existent step", async () => {
|
|
164
|
+
const step = await storage.getStep("nonexistent");
|
|
165
|
+
expect(step).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should update step fields", async () => {
|
|
169
|
+
const step = await storage.createStep({
|
|
170
|
+
runId: "run-1",
|
|
171
|
+
stepKey: "step1",
|
|
172
|
+
stepName: "Step 1",
|
|
173
|
+
status: "running",
|
|
174
|
+
attempt: 1,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await storage.updateStep(step.id, {
|
|
178
|
+
status: "succeeded",
|
|
179
|
+
result: { output: "done" },
|
|
180
|
+
finishedAt: new Date(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const updated = await storage.getStep(step.id);
|
|
184
|
+
expect(updated?.status).toBe("succeeded");
|
|
185
|
+
expect(updated?.result).toEqual({ output: "done" });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should do nothing when updating non-existent step", async () => {
|
|
189
|
+
await expect(
|
|
190
|
+
storage.updateStep("nonexistent", { status: "failed" }),
|
|
191
|
+
).resolves.not.toThrow();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("getStepsForRun", () => {
|
|
196
|
+
it("should return steps for a specific run sorted by startedAt", async () => {
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
await storage.createStep({
|
|
199
|
+
runId: "run-1",
|
|
200
|
+
stepKey: "b",
|
|
201
|
+
stepName: "B",
|
|
202
|
+
status: "succeeded",
|
|
203
|
+
attempt: 1,
|
|
204
|
+
startedAt: new Date(now + 100),
|
|
205
|
+
});
|
|
206
|
+
await storage.createStep({
|
|
207
|
+
runId: "run-1",
|
|
208
|
+
stepKey: "a",
|
|
209
|
+
stepName: "A",
|
|
210
|
+
status: "succeeded",
|
|
211
|
+
attempt: 1,
|
|
212
|
+
startedAt: new Date(now),
|
|
213
|
+
});
|
|
214
|
+
await storage.createStep({
|
|
215
|
+
runId: "run-2",
|
|
216
|
+
stepKey: "c",
|
|
217
|
+
stepName: "C",
|
|
218
|
+
status: "running",
|
|
219
|
+
attempt: 1,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const steps = await storage.getStepsForRun("run-1");
|
|
223
|
+
expect(steps).toHaveLength(2);
|
|
224
|
+
expect(steps[0].stepKey).toBe("a");
|
|
225
|
+
expect(steps[1].stepKey).toBe("b");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("saveEvent / getEventsForRun", () => {
|
|
230
|
+
it("should save and retrieve events", async () => {
|
|
231
|
+
await storage.saveEvent({
|
|
232
|
+
runId: "run-1",
|
|
233
|
+
eventType: "step.started",
|
|
234
|
+
level: "info",
|
|
235
|
+
timestamp: new Date(),
|
|
236
|
+
});
|
|
237
|
+
await storage.saveEvent({
|
|
238
|
+
runId: "run-1",
|
|
239
|
+
stepKey: "step1",
|
|
240
|
+
eventType: "step.completed",
|
|
241
|
+
level: "info",
|
|
242
|
+
timestamp: new Date(),
|
|
243
|
+
});
|
|
244
|
+
await storage.saveEvent({
|
|
245
|
+
runId: "run-2",
|
|
246
|
+
eventType: "run.started",
|
|
247
|
+
level: "info",
|
|
248
|
+
timestamp: new Date(),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const events = await storage.getEventsForRun("run-1");
|
|
252
|
+
expect(events).toHaveLength(2);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should filter events by stepKey", async () => {
|
|
256
|
+
await storage.saveEvent({
|
|
257
|
+
runId: "run-1",
|
|
258
|
+
eventType: "run.started",
|
|
259
|
+
level: "info",
|
|
260
|
+
timestamp: new Date(),
|
|
261
|
+
});
|
|
262
|
+
await storage.saveEvent({
|
|
263
|
+
runId: "run-1",
|
|
264
|
+
stepKey: "step1",
|
|
265
|
+
eventType: "step.started",
|
|
266
|
+
level: "info",
|
|
267
|
+
timestamp: new Date(),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const events = await storage.getEventsForRun("run-1", {
|
|
271
|
+
stepKey: "step1",
|
|
272
|
+
});
|
|
273
|
+
expect(events).toHaveLength(1);
|
|
274
|
+
expect(events[0].stepKey).toBe("step1");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should filter events by level", async () => {
|
|
278
|
+
await storage.saveEvent({
|
|
279
|
+
runId: "run-1",
|
|
280
|
+
eventType: "info-event",
|
|
281
|
+
level: "info",
|
|
282
|
+
timestamp: new Date(),
|
|
283
|
+
});
|
|
284
|
+
await storage.saveEvent({
|
|
285
|
+
runId: "run-1",
|
|
286
|
+
eventType: "error-event",
|
|
287
|
+
level: "error",
|
|
288
|
+
timestamp: new Date(),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const events = await storage.getEventsForRun("run-1", { level: "error" });
|
|
292
|
+
expect(events).toHaveLength(1);
|
|
293
|
+
expect(events[0].level).toBe("error");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("should paginate events", async () => {
|
|
297
|
+
for (let i = 0; i < 5; i++) {
|
|
298
|
+
await storage.saveEvent({
|
|
299
|
+
runId: "run-1",
|
|
300
|
+
eventType: `event-${i}`,
|
|
301
|
+
level: "info",
|
|
302
|
+
timestamp: new Date(Date.now() + i),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const events = await storage.getEventsForRun("run-1", {
|
|
307
|
+
limit: 2,
|
|
308
|
+
offset: 1,
|
|
309
|
+
});
|
|
310
|
+
expect(events).toHaveLength(2);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("deleteOldRuns", () => {
|
|
315
|
+
it("should delete runs older than the cutoff date", async () => {
|
|
316
|
+
const oldDate = new Date("2020-01-01");
|
|
317
|
+
const newDate = new Date("2025-01-01");
|
|
318
|
+
|
|
319
|
+
// Create an old run with steps and events
|
|
320
|
+
const oldRun = await storage.createRun({
|
|
321
|
+
kind: "test",
|
|
322
|
+
status: "succeeded",
|
|
323
|
+
input: {},
|
|
324
|
+
metadata: {},
|
|
325
|
+
context: {},
|
|
326
|
+
});
|
|
327
|
+
// Manually set createdAt to old date
|
|
328
|
+
(await storage.getRun(oldRun.id))!.createdAt = oldDate;
|
|
329
|
+
|
|
330
|
+
await storage.createStep({
|
|
331
|
+
runId: oldRun.id,
|
|
332
|
+
stepKey: "s1",
|
|
333
|
+
stepName: "S1",
|
|
334
|
+
status: "succeeded",
|
|
335
|
+
attempt: 1,
|
|
336
|
+
});
|
|
337
|
+
await storage.saveEvent({
|
|
338
|
+
runId: oldRun.id,
|
|
339
|
+
eventType: "run.completed",
|
|
340
|
+
level: "info",
|
|
341
|
+
timestamp: new Date(),
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Create a recent run
|
|
345
|
+
const newRun = await storage.createRun({
|
|
346
|
+
kind: "test",
|
|
347
|
+
status: "succeeded",
|
|
348
|
+
input: {},
|
|
349
|
+
metadata: {},
|
|
350
|
+
context: {},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const deleted = await storage.deleteOldRuns(new Date("2024-01-01"));
|
|
354
|
+
|
|
355
|
+
expect(deleted).toBe(1);
|
|
356
|
+
expect(await storage.getRun(oldRun.id)).toBeNull();
|
|
357
|
+
expect(await storage.getRun(newRun.id)).not.toBeNull();
|
|
358
|
+
|
|
359
|
+
// Steps and events should also be cleaned up
|
|
360
|
+
const steps = await storage.getStepsForRun(oldRun.id);
|
|
361
|
+
expect(steps).toHaveLength(0);
|
|
362
|
+
const events = await storage.getEventsForRun(oldRun.id);
|
|
363
|
+
expect(events).toHaveLength(0);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("clear / getStats", () => {
|
|
368
|
+
it("should clear all data", async () => {
|
|
369
|
+
await storage.createRun({
|
|
370
|
+
kind: "a",
|
|
371
|
+
status: "queued",
|
|
372
|
+
input: {},
|
|
373
|
+
metadata: {},
|
|
374
|
+
context: {},
|
|
375
|
+
});
|
|
376
|
+
await storage.createStep({
|
|
377
|
+
runId: "r",
|
|
378
|
+
stepKey: "s",
|
|
379
|
+
stepName: "S",
|
|
380
|
+
status: "running",
|
|
381
|
+
attempt: 1,
|
|
382
|
+
});
|
|
383
|
+
await storage.saveEvent({
|
|
384
|
+
runId: "r",
|
|
385
|
+
eventType: "e",
|
|
386
|
+
level: "info",
|
|
387
|
+
timestamp: new Date(),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(storage.getStats().runs).toBe(1);
|
|
391
|
+
expect(storage.getStats().steps).toBe(1);
|
|
392
|
+
expect(storage.getStats().events).toBe(1);
|
|
393
|
+
|
|
394
|
+
storage.clear();
|
|
395
|
+
|
|
396
|
+
expect(storage.getStats()).toEqual({ runs: 0, steps: 0, events: 0 });
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory storage adapter for development and testing.
|
|
3
|
+
* All data is lost when the process exits.
|
|
4
|
+
*
|
|
5
|
+
* NOTE: This adapter is intended for development and testing only. It stores
|
|
6
|
+
* all state in-process with no persistence or multi-process safety guarantees.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { generateId } from "../utils/id";
|
|
10
|
+
import type {
|
|
11
|
+
StorageAdapter,
|
|
12
|
+
WorkflowRunRecord,
|
|
13
|
+
WorkflowRunStepRecord,
|
|
14
|
+
WorkflowEventRecord,
|
|
15
|
+
ListRunsOptions,
|
|
16
|
+
ListEventsOptions,
|
|
17
|
+
PaginatedResult,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* In-memory implementation of StorageAdapter.
|
|
22
|
+
* Useful for development, testing, and lightweight deployments.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { WorkflowEngine } from 'stepflow';
|
|
27
|
+
* import { MemoryStorageAdapter } from 'stepflow/storage';
|
|
28
|
+
*
|
|
29
|
+
* const storage = new MemoryStorageAdapter();
|
|
30
|
+
* const engine = new WorkflowEngine({ storage });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class MemoryStorageAdapter implements StorageAdapter {
|
|
34
|
+
private runs = new Map<string, WorkflowRunRecord>();
|
|
35
|
+
private steps = new Map<string, WorkflowRunStepRecord>();
|
|
36
|
+
private events = new Map<string, WorkflowEventRecord>();
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Run Operations
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/** Create and persist a new workflow run record. */
|
|
43
|
+
async createRun(
|
|
44
|
+
run: Omit<WorkflowRunRecord, "id" | "createdAt">,
|
|
45
|
+
): Promise<WorkflowRunRecord> {
|
|
46
|
+
const record: WorkflowRunRecord = {
|
|
47
|
+
...run,
|
|
48
|
+
id: generateId(),
|
|
49
|
+
createdAt: new Date(),
|
|
50
|
+
};
|
|
51
|
+
this.runs.set(record.id, record);
|
|
52
|
+
return record;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Retrieve a workflow run by ID, or null if not found. */
|
|
56
|
+
async getRun(runId: string): Promise<WorkflowRunRecord | null> {
|
|
57
|
+
return this.runs.get(runId) ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Apply partial updates to an existing workflow run. No-op if the run does not exist. */
|
|
61
|
+
async updateRun(
|
|
62
|
+
runId: string,
|
|
63
|
+
updates: Partial<WorkflowRunRecord>,
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const run = this.runs.get(runId);
|
|
66
|
+
if (run) {
|
|
67
|
+
Object.assign(run, updates);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** List workflow runs with optional filtering, sorting, and pagination. */
|
|
72
|
+
async listRuns(
|
|
73
|
+
options: ListRunsOptions = {},
|
|
74
|
+
): Promise<PaginatedResult<WorkflowRunRecord>> {
|
|
75
|
+
let items = Array.from(this.runs.values());
|
|
76
|
+
|
|
77
|
+
// Filter by kind
|
|
78
|
+
if (options.kind) {
|
|
79
|
+
items = items.filter((r) => r.kind === options.kind);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Filter by status
|
|
83
|
+
if (options.status) {
|
|
84
|
+
const statuses = Array.isArray(options.status)
|
|
85
|
+
? options.status
|
|
86
|
+
: [options.status];
|
|
87
|
+
items = items.filter((r) => statuses.includes(r.status));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Filter by parentRunId
|
|
91
|
+
if (options.parentRunId !== undefined) {
|
|
92
|
+
items = items.filter((r) => r.parentRunId === options.parentRunId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Sort
|
|
96
|
+
const orderBy = options.orderBy ?? "createdAt";
|
|
97
|
+
const direction = options.orderDirection ?? "desc";
|
|
98
|
+
items.sort((a, b) => {
|
|
99
|
+
const aVal = a[orderBy]?.getTime() ?? 0;
|
|
100
|
+
const bVal = b[orderBy]?.getTime() ?? 0;
|
|
101
|
+
return direction === "asc" ? aVal - bVal : bVal - aVal;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const total = items.length;
|
|
105
|
+
const offset = options.offset ?? 0;
|
|
106
|
+
const limit = options.limit ?? 50;
|
|
107
|
+
|
|
108
|
+
items = items.slice(offset, offset + limit);
|
|
109
|
+
|
|
110
|
+
return { items, total, limit, offset };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Step Operations
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
/** Create and persist a new step execution record. */
|
|
118
|
+
async createStep(
|
|
119
|
+
step: Omit<WorkflowRunStepRecord, "id">,
|
|
120
|
+
): Promise<WorkflowRunStepRecord> {
|
|
121
|
+
const record: WorkflowRunStepRecord = {
|
|
122
|
+
...step,
|
|
123
|
+
id: generateId(),
|
|
124
|
+
};
|
|
125
|
+
this.steps.set(record.id, record);
|
|
126
|
+
return record;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Retrieve a step record by ID, or null if not found. */
|
|
130
|
+
async getStep(stepId: string): Promise<WorkflowRunStepRecord | null> {
|
|
131
|
+
return this.steps.get(stepId) ?? null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Apply partial updates to an existing step record. No-op if the step does not exist. */
|
|
135
|
+
async updateStep(
|
|
136
|
+
stepId: string,
|
|
137
|
+
updates: Partial<WorkflowRunStepRecord>,
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
const step = this.steps.get(stepId);
|
|
140
|
+
if (step) {
|
|
141
|
+
Object.assign(step, updates);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Retrieve all step records for a workflow run, ordered by start time ascending. */
|
|
146
|
+
async getStepsForRun(runId: string): Promise<WorkflowRunStepRecord[]> {
|
|
147
|
+
return Array.from(this.steps.values())
|
|
148
|
+
.filter((s) => s.runId === runId)
|
|
149
|
+
.sort(
|
|
150
|
+
(a, b) => (a.startedAt?.getTime() ?? 0) - (b.startedAt?.getTime() ?? 0),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Event Operations
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
/** Persist a workflow event record. */
|
|
159
|
+
async saveEvent(event: Omit<WorkflowEventRecord, "id">): Promise<void> {
|
|
160
|
+
const record: WorkflowEventRecord = {
|
|
161
|
+
...event,
|
|
162
|
+
id: generateId(),
|
|
163
|
+
};
|
|
164
|
+
this.events.set(record.id, record);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Retrieve events for a workflow run with optional filtering and pagination. */
|
|
168
|
+
async getEventsForRun(
|
|
169
|
+
runId: string,
|
|
170
|
+
options: ListEventsOptions = {},
|
|
171
|
+
): Promise<WorkflowEventRecord[]> {
|
|
172
|
+
let items = Array.from(this.events.values()).filter(
|
|
173
|
+
(e) => e.runId === runId,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Filter by step
|
|
177
|
+
if (options.stepKey) {
|
|
178
|
+
items = items.filter((e) => e.stepKey === options.stepKey);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Filter by level
|
|
182
|
+
if (options.level) {
|
|
183
|
+
items = items.filter((e) => e.level === options.level);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Sort by timestamp
|
|
187
|
+
items.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
188
|
+
|
|
189
|
+
// Apply pagination
|
|
190
|
+
const offset = options.offset ?? 0;
|
|
191
|
+
const limit = options.limit ?? 100;
|
|
192
|
+
|
|
193
|
+
return items.slice(offset, offset + limit);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Optional Operations
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
/** Delete runs (and their associated steps and events) created before the given date. Returns the number of deleted runs. */
|
|
201
|
+
async deleteOldRuns(olderThan: Date): Promise<number> {
|
|
202
|
+
let deleted = 0;
|
|
203
|
+
for (const [id, run] of this.runs) {
|
|
204
|
+
if (run.createdAt < olderThan) {
|
|
205
|
+
// Delete associated steps
|
|
206
|
+
for (const [stepId, step] of this.steps) {
|
|
207
|
+
if (step.runId === id) {
|
|
208
|
+
this.steps.delete(stepId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Delete associated events
|
|
212
|
+
for (const [eventId, event] of this.events) {
|
|
213
|
+
if (event.runId === id) {
|
|
214
|
+
this.events.delete(eventId);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Delete run
|
|
218
|
+
this.runs.delete(id);
|
|
219
|
+
deleted++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return deleted;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// Testing Utilities
|
|
227
|
+
// ============================================================================
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Clear all stored data. Useful for testing.
|
|
231
|
+
*/
|
|
232
|
+
clear(): void {
|
|
233
|
+
this.runs.clear();
|
|
234
|
+
this.steps.clear();
|
|
235
|
+
this.events.clear();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get counts of stored records. Useful for testing.
|
|
240
|
+
*/
|
|
241
|
+
getStats(): { runs: number; steps: number; events: number } {
|
|
242
|
+
return {
|
|
243
|
+
runs: this.runs.size,
|
|
244
|
+
steps: this.steps.size,
|
|
245
|
+
events: this.events.size,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|