@multiplier-labs/stepflow 0.3.4 → 0.3.6

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.
Files changed (99) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-U3PCVK6O.js → chunk-D5HW7VEG.js} +4 -2
  3. package/dist/chunk-D5HW7VEG.js.map +1 -0
  4. package/dist/{chunk-5XJBAPXF.js → chunk-JUPZEBR3.js} +3 -1
  5. package/dist/{chunk-5XJBAPXF.js.map → chunk-JUPZEBR3.js.map} +1 -1
  6. package/dist/events/index.d.ts +2 -0
  7. package/dist/events/index.js +1 -1
  8. package/dist/index.js +2 -2
  9. package/dist/storage/index.js +1 -1
  10. package/package.json +23 -12
  11. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +0 -146
  12. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +0 -74
  13. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  14. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +0 -161
  15. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +0 -34
  16. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  17. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +0 -152
  18. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +0 -34
  19. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  20. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +0 -93
  21. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  22. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +0 -138
  23. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +0 -121
  24. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  25. package/.codekin/reports/security/2026-04-11_security-audit.md +0 -297
  26. package/.github/CODEOWNERS +0 -15
  27. package/.github/dependabot.yml +0 -23
  28. package/.github/workflows/ci.yml +0 -38
  29. package/.github/workflows/publish.yml +0 -42
  30. package/.github/workflows/security.yml +0 -29
  31. package/CHANGELOG.md +0 -34
  32. package/SECURITY.md +0 -40
  33. package/dist/chunk-U3PCVK6O.js.map +0 -1
  34. package/docs/github-packages-setup.md +0 -97
  35. package/docs/stepflow-api-reference.md +0 -1663
  36. package/src/core/engine/concurrency.ts +0 -229
  37. package/src/core/engine/lifecycle.ts +0 -176
  38. package/src/core/engine.test.ts +0 -1435
  39. package/src/core/engine.ts +0 -598
  40. package/src/core/index.ts +0 -7
  41. package/src/core/orchestrator/persistence.ts +0 -165
  42. package/src/core/orchestrator/plan-resolution.ts +0 -140
  43. package/src/core/orchestrator/state-transitions.ts +0 -109
  44. package/src/core/orchestrator/step-runner.ts +0 -427
  45. package/src/core/orchestrator.ts +0 -252
  46. package/src/core/types.ts +0 -220
  47. package/src/events/index.ts +0 -8
  48. package/src/events/memory.test.ts +0 -107
  49. package/src/events/memory.ts +0 -92
  50. package/src/events/socketio.test.ts +0 -378
  51. package/src/events/socketio.ts +0 -262
  52. package/src/events/types.ts +0 -118
  53. package/src/events/webhook.test.ts +0 -949
  54. package/src/events/webhook.ts +0 -807
  55. package/src/index.ts +0 -270
  56. package/src/planning/evaluate-condition.test.ts +0 -153
  57. package/src/planning/evaluate-condition.ts +0 -105
  58. package/src/planning/index.ts +0 -55
  59. package/src/planning/planner.ts +0 -494
  60. package/src/planning/planning.test.ts +0 -727
  61. package/src/planning/registry.test.ts +0 -398
  62. package/src/planning/registry.ts +0 -283
  63. package/src/planning/types.ts +0 -566
  64. package/src/scheduler/cron.test.ts +0 -479
  65. package/src/scheduler/cron.ts +0 -438
  66. package/src/scheduler/index.ts +0 -8
  67. package/src/scheduler/postgres-persistence.ts +0 -530
  68. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  69. package/src/scheduler/sqlite-persistence.ts +0 -286
  70. package/src/scheduler/types.ts +0 -73
  71. package/src/storage/index.ts +0 -27
  72. package/src/storage/memory.test.ts +0 -349
  73. package/src/storage/memory.ts +0 -227
  74. package/src/storage/postgres-core.ts +0 -550
  75. package/src/storage/postgres-migration.test.ts +0 -69
  76. package/src/storage/postgres-transaction-pagination.test.ts +0 -107
  77. package/src/storage/postgres.ts +0 -835
  78. package/src/storage/sqlite-core.ts +0 -151
  79. package/src/storage/sqlite.test.ts +0 -642
  80. package/src/storage/sqlite.ts +0 -717
  81. package/src/storage/types.ts +0 -343
  82. package/src/utils/errors.test.ts +0 -150
  83. package/src/utils/errors.ts +0 -161
  84. package/src/utils/id.ts +0 -21
  85. package/src/utils/index.ts +0 -10
  86. package/src/utils/logger.test.ts +0 -121
  87. package/src/utils/logger.ts +0 -96
  88. package/src/utils/pinned-fetch.test.ts +0 -84
  89. package/src/utils/pinned-fetch.ts +0 -118
  90. package/src/utils/postgres-deps.ts +0 -79
  91. package/src/utils/retry.test.ts +0 -177
  92. package/src/utils/retry.ts +0 -121
  93. package/src/utils/safe-json.test.ts +0 -166
  94. package/src/utils/safe-json.ts +0 -107
  95. package/src/utils/webhook-signature.test.ts +0 -111
  96. package/src/utils/webhook-signature.ts +0 -74
  97. package/tsconfig.json +0 -21
  98. package/tsup.config.ts +0 -14
  99. package/vitest.config.ts +0 -9
@@ -1,349 +0,0 @@
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({ kind: 'a', status: 'queued', input: {}, metadata: {}, context: {} });
66
- await storage.createRun({ kind: 'a', status: 'running', input: {}, metadata: {}, context: {} });
67
- await storage.createRun({ kind: 'b', status: 'succeeded', input: {}, metadata: {}, context: {} });
68
- await storage.createRun({ kind: 'a', status: 'failed', input: {}, metadata: {}, context: {}, parentRunId: 'parent-1' });
69
- });
70
-
71
- it('should list all runs with default pagination', async () => {
72
- const result = await storage.listRuns();
73
- expect(result.items).toHaveLength(4);
74
- expect(result.total).toBe(4);
75
- });
76
-
77
- it('should filter by kind', async () => {
78
- const result = await storage.listRuns({ kind: 'a' });
79
- expect(result.items).toHaveLength(3);
80
- expect(result.items.every(r => r.kind === 'a')).toBe(true);
81
- });
82
-
83
- it('should filter by single status', async () => {
84
- const result = await storage.listRuns({ status: 'queued' });
85
- expect(result.items).toHaveLength(1);
86
- expect(result.items[0].status).toBe('queued');
87
- });
88
-
89
- it('should filter by multiple statuses', async () => {
90
- const result = await storage.listRuns({ status: ['queued', 'running'] });
91
- expect(result.items).toHaveLength(2);
92
- });
93
-
94
- it('should filter by parentRunId', async () => {
95
- const result = await storage.listRuns({ parentRunId: 'parent-1' });
96
- expect(result.items).toHaveLength(1);
97
- expect(result.items[0].parentRunId).toBe('parent-1');
98
- });
99
-
100
- it('should apply limit and offset', async () => {
101
- const result = await storage.listRuns({ limit: 2, offset: 1 });
102
- expect(result.items).toHaveLength(2);
103
- expect(result.total).toBe(4);
104
- expect(result.limit).toBe(2);
105
- expect(result.offset).toBe(1);
106
- });
107
-
108
- it('should sort by createdAt ascending', async () => {
109
- const result = await storage.listRuns({ orderBy: 'createdAt', orderDirection: 'asc' });
110
- const times = result.items.map(r => r.createdAt.getTime());
111
- for (let i = 1; i < times.length; i++) {
112
- expect(times[i]).toBeGreaterThanOrEqual(times[i - 1]);
113
- }
114
- });
115
- });
116
-
117
- describe('createStep / getStep / updateStep', () => {
118
- it('should create and retrieve a step', async () => {
119
- const step = await storage.createStep({
120
- runId: 'run-1',
121
- stepKey: 'step1',
122
- stepName: 'Step 1',
123
- status: 'running',
124
- attempt: 1,
125
- startedAt: new Date(),
126
- });
127
-
128
- expect(step.id).toBeDefined();
129
- expect(step.stepKey).toBe('step1');
130
-
131
- const fetched = await storage.getStep(step.id);
132
- expect(fetched).toEqual(step);
133
- });
134
-
135
- it('should return null for non-existent step', async () => {
136
- const step = await storage.getStep('nonexistent');
137
- expect(step).toBeNull();
138
- });
139
-
140
- it('should update step fields', async () => {
141
- const step = await storage.createStep({
142
- runId: 'run-1',
143
- stepKey: 'step1',
144
- stepName: 'Step 1',
145
- status: 'running',
146
- attempt: 1,
147
- });
148
-
149
- await storage.updateStep(step.id, {
150
- status: 'succeeded',
151
- result: { output: 'done' },
152
- finishedAt: new Date(),
153
- });
154
-
155
- const updated = await storage.getStep(step.id);
156
- expect(updated?.status).toBe('succeeded');
157
- expect(updated?.result).toEqual({ output: 'done' });
158
- });
159
-
160
- it('should do nothing when updating non-existent step', async () => {
161
- await expect(
162
- storage.updateStep('nonexistent', { status: 'failed' })
163
- ).resolves.not.toThrow();
164
- });
165
- });
166
-
167
- describe('getStepsForRun', () => {
168
- it('should return steps for a specific run sorted by startedAt', async () => {
169
- const now = Date.now();
170
- await storage.createStep({
171
- runId: 'run-1',
172
- stepKey: 'b',
173
- stepName: 'B',
174
- status: 'succeeded',
175
- attempt: 1,
176
- startedAt: new Date(now + 100),
177
- });
178
- await storage.createStep({
179
- runId: 'run-1',
180
- stepKey: 'a',
181
- stepName: 'A',
182
- status: 'succeeded',
183
- attempt: 1,
184
- startedAt: new Date(now),
185
- });
186
- await storage.createStep({
187
- runId: 'run-2',
188
- stepKey: 'c',
189
- stepName: 'C',
190
- status: 'running',
191
- attempt: 1,
192
- });
193
-
194
- const steps = await storage.getStepsForRun('run-1');
195
- expect(steps).toHaveLength(2);
196
- expect(steps[0].stepKey).toBe('a');
197
- expect(steps[1].stepKey).toBe('b');
198
- });
199
- });
200
-
201
- describe('saveEvent / getEventsForRun', () => {
202
- it('should save and retrieve events', async () => {
203
- await storage.saveEvent({
204
- runId: 'run-1',
205
- eventType: 'step.started',
206
- level: 'info',
207
- timestamp: new Date(),
208
- });
209
- await storage.saveEvent({
210
- runId: 'run-1',
211
- stepKey: 'step1',
212
- eventType: 'step.completed',
213
- level: 'info',
214
- timestamp: new Date(),
215
- });
216
- await storage.saveEvent({
217
- runId: 'run-2',
218
- eventType: 'run.started',
219
- level: 'info',
220
- timestamp: new Date(),
221
- });
222
-
223
- const events = await storage.getEventsForRun('run-1');
224
- expect(events).toHaveLength(2);
225
- });
226
-
227
- it('should filter events by stepKey', async () => {
228
- await storage.saveEvent({
229
- runId: 'run-1',
230
- eventType: 'run.started',
231
- level: 'info',
232
- timestamp: new Date(),
233
- });
234
- await storage.saveEvent({
235
- runId: 'run-1',
236
- stepKey: 'step1',
237
- eventType: 'step.started',
238
- level: 'info',
239
- timestamp: new Date(),
240
- });
241
-
242
- const events = await storage.getEventsForRun('run-1', { stepKey: 'step1' });
243
- expect(events).toHaveLength(1);
244
- expect(events[0].stepKey).toBe('step1');
245
- });
246
-
247
- it('should filter events by level', async () => {
248
- await storage.saveEvent({
249
- runId: 'run-1',
250
- eventType: 'info-event',
251
- level: 'info',
252
- timestamp: new Date(),
253
- });
254
- await storage.saveEvent({
255
- runId: 'run-1',
256
- eventType: 'error-event',
257
- level: 'error',
258
- timestamp: new Date(),
259
- });
260
-
261
- const events = await storage.getEventsForRun('run-1', { level: 'error' });
262
- expect(events).toHaveLength(1);
263
- expect(events[0].level).toBe('error');
264
- });
265
-
266
- it('should paginate events', async () => {
267
- for (let i = 0; i < 5; i++) {
268
- await storage.saveEvent({
269
- runId: 'run-1',
270
- eventType: `event-${i}`,
271
- level: 'info',
272
- timestamp: new Date(Date.now() + i),
273
- });
274
- }
275
-
276
- const events = await storage.getEventsForRun('run-1', { limit: 2, offset: 1 });
277
- expect(events).toHaveLength(2);
278
- });
279
- });
280
-
281
- describe('deleteOldRuns', () => {
282
- it('should delete runs older than the cutoff date', async () => {
283
- const oldDate = new Date('2020-01-01');
284
- const newDate = new Date('2025-01-01');
285
-
286
- // Create an old run with steps and events
287
- const oldRun = await storage.createRun({
288
- kind: 'test',
289
- status: 'succeeded',
290
- input: {},
291
- metadata: {},
292
- context: {},
293
- });
294
- // Manually set createdAt to old date
295
- (await storage.getRun(oldRun.id))!.createdAt = oldDate;
296
-
297
- await storage.createStep({
298
- runId: oldRun.id,
299
- stepKey: 's1',
300
- stepName: 'S1',
301
- status: 'succeeded',
302
- attempt: 1,
303
- });
304
- await storage.saveEvent({
305
- runId: oldRun.id,
306
- eventType: 'run.completed',
307
- level: 'info',
308
- timestamp: new Date(),
309
- });
310
-
311
- // Create a recent run
312
- const newRun = await storage.createRun({
313
- kind: 'test',
314
- status: 'succeeded',
315
- input: {},
316
- metadata: {},
317
- context: {},
318
- });
319
-
320
- const deleted = await storage.deleteOldRuns(new Date('2024-01-01'));
321
-
322
- expect(deleted).toBe(1);
323
- expect(await storage.getRun(oldRun.id)).toBeNull();
324
- expect(await storage.getRun(newRun.id)).not.toBeNull();
325
-
326
- // Steps and events should also be cleaned up
327
- const steps = await storage.getStepsForRun(oldRun.id);
328
- expect(steps).toHaveLength(0);
329
- const events = await storage.getEventsForRun(oldRun.id);
330
- expect(events).toHaveLength(0);
331
- });
332
- });
333
-
334
- describe('clear / getStats', () => {
335
- it('should clear all data', async () => {
336
- await storage.createRun({ kind: 'a', status: 'queued', input: {}, metadata: {}, context: {} });
337
- await storage.createStep({ runId: 'r', stepKey: 's', stepName: 'S', status: 'running', attempt: 1 });
338
- await storage.saveEvent({ runId: 'r', eventType: 'e', level: 'info', timestamp: new Date() });
339
-
340
- expect(storage.getStats().runs).toBe(1);
341
- expect(storage.getStats().steps).toBe(1);
342
- expect(storage.getStats().events).toBe(1);
343
-
344
- storage.clear();
345
-
346
- expect(storage.getStats()).toEqual({ runs: 0, steps: 0, events: 0 });
347
- });
348
- });
349
- });
@@ -1,227 +0,0 @@
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(run: Omit<WorkflowRunRecord, 'id' | 'createdAt'>): Promise<WorkflowRunRecord> {
44
- const record: WorkflowRunRecord = {
45
- ...run,
46
- id: generateId(),
47
- createdAt: new Date(),
48
- };
49
- this.runs.set(record.id, record);
50
- return record;
51
- }
52
-
53
- /** Retrieve a workflow run by ID, or null if not found. */
54
- async getRun(runId: string): Promise<WorkflowRunRecord | null> {
55
- return this.runs.get(runId) ?? null;
56
- }
57
-
58
- /** Apply partial updates to an existing workflow run. No-op if the run does not exist. */
59
- async updateRun(runId: string, updates: Partial<WorkflowRunRecord>): Promise<void> {
60
- const run = this.runs.get(runId);
61
- if (run) {
62
- Object.assign(run, updates);
63
- }
64
- }
65
-
66
- /** List workflow runs with optional filtering, sorting, and pagination. */
67
- async listRuns(options: ListRunsOptions = {}): Promise<PaginatedResult<WorkflowRunRecord>> {
68
- let items = Array.from(this.runs.values());
69
-
70
- // Filter by kind
71
- if (options.kind) {
72
- items = items.filter(r => r.kind === options.kind);
73
- }
74
-
75
- // Filter by status
76
- if (options.status) {
77
- const statuses = Array.isArray(options.status) ? options.status : [options.status];
78
- items = items.filter(r => statuses.includes(r.status));
79
- }
80
-
81
- // Filter by parentRunId
82
- if (options.parentRunId !== undefined) {
83
- items = items.filter(r => r.parentRunId === options.parentRunId);
84
- }
85
-
86
- // Sort
87
- const orderBy = options.orderBy ?? 'createdAt';
88
- const direction = options.orderDirection ?? 'desc';
89
- items.sort((a, b) => {
90
- const aVal = a[orderBy]?.getTime() ?? 0;
91
- const bVal = b[orderBy]?.getTime() ?? 0;
92
- return direction === 'asc' ? aVal - bVal : bVal - aVal;
93
- });
94
-
95
- const total = items.length;
96
- const offset = options.offset ?? 0;
97
- const limit = options.limit ?? 50;
98
-
99
- items = items.slice(offset, offset + limit);
100
-
101
- return { items, total, limit, offset };
102
- }
103
-
104
- // ============================================================================
105
- // Step Operations
106
- // ============================================================================
107
-
108
- /** Create and persist a new step execution record. */
109
- async createStep(step: Omit<WorkflowRunStepRecord, 'id'>): Promise<WorkflowRunStepRecord> {
110
- const record: WorkflowRunStepRecord = {
111
- ...step,
112
- id: generateId(),
113
- };
114
- this.steps.set(record.id, record);
115
- return record;
116
- }
117
-
118
- /** Retrieve a step record by ID, or null if not found. */
119
- async getStep(stepId: string): Promise<WorkflowRunStepRecord | null> {
120
- return this.steps.get(stepId) ?? null;
121
- }
122
-
123
- /** Apply partial updates to an existing step record. No-op if the step does not exist. */
124
- async updateStep(stepId: string, updates: Partial<WorkflowRunStepRecord>): Promise<void> {
125
- const step = this.steps.get(stepId);
126
- if (step) {
127
- Object.assign(step, updates);
128
- }
129
- }
130
-
131
- /** Retrieve all step records for a workflow run, ordered by start time ascending. */
132
- async getStepsForRun(runId: string): Promise<WorkflowRunStepRecord[]> {
133
- return Array.from(this.steps.values())
134
- .filter(s => s.runId === runId)
135
- .sort((a, b) => (a.startedAt?.getTime() ?? 0) - (b.startedAt?.getTime() ?? 0));
136
- }
137
-
138
- // ============================================================================
139
- // Event Operations
140
- // ============================================================================
141
-
142
- /** Persist a workflow event record. */
143
- async saveEvent(event: Omit<WorkflowEventRecord, 'id'>): Promise<void> {
144
- const record: WorkflowEventRecord = {
145
- ...event,
146
- id: generateId(),
147
- };
148
- this.events.set(record.id, record);
149
- }
150
-
151
- /** Retrieve events for a workflow run with optional filtering and pagination. */
152
- async getEventsForRun(runId: string, options: ListEventsOptions = {}): Promise<WorkflowEventRecord[]> {
153
- let items = Array.from(this.events.values()).filter(e => e.runId === runId);
154
-
155
- // Filter by step
156
- if (options.stepKey) {
157
- items = items.filter(e => e.stepKey === options.stepKey);
158
- }
159
-
160
- // Filter by level
161
- if (options.level) {
162
- items = items.filter(e => e.level === options.level);
163
- }
164
-
165
- // Sort by timestamp
166
- items.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
167
-
168
- // Apply pagination
169
- const offset = options.offset ?? 0;
170
- const limit = options.limit ?? 100;
171
-
172
- return items.slice(offset, offset + limit);
173
- }
174
-
175
- // ============================================================================
176
- // Optional Operations
177
- // ============================================================================
178
-
179
- /** Delete runs (and their associated steps and events) created before the given date. Returns the number of deleted runs. */
180
- async deleteOldRuns(olderThan: Date): Promise<number> {
181
- let deleted = 0;
182
- for (const [id, run] of this.runs) {
183
- if (run.createdAt < olderThan) {
184
- // Delete associated steps
185
- for (const [stepId, step] of this.steps) {
186
- if (step.runId === id) {
187
- this.steps.delete(stepId);
188
- }
189
- }
190
- // Delete associated events
191
- for (const [eventId, event] of this.events) {
192
- if (event.runId === id) {
193
- this.events.delete(eventId);
194
- }
195
- }
196
- // Delete run
197
- this.runs.delete(id);
198
- deleted++;
199
- }
200
- }
201
- return deleted;
202
- }
203
-
204
- // ============================================================================
205
- // Testing Utilities
206
- // ============================================================================
207
-
208
- /**
209
- * Clear all stored data. Useful for testing.
210
- */
211
- clear(): void {
212
- this.runs.clear();
213
- this.steps.clear();
214
- this.events.clear();
215
- }
216
-
217
- /**
218
- * Get counts of stored records. Useful for testing.
219
- */
220
- getStats(): { runs: number; steps: number; events: number } {
221
- return {
222
- runs: this.runs.size,
223
- steps: this.steps.size,
224
- events: this.events.size,
225
- };
226
- }
227
- }