@multiplier-labs/stepflow 0.3.4 → 0.3.8

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 (109) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-5XJBAPXF.js → chunk-4IDPWWGS.js} +15 -4
  3. package/dist/chunk-4IDPWWGS.js.map +1 -0
  4. package/dist/{chunk-U3PCVK6O.js → chunk-T6GAZBTQ.js} +70 -38
  5. package/dist/chunk-T6GAZBTQ.js.map +1 -0
  6. package/dist/{chunk-FMPJBK2N.js → chunk-TVSADUYU.js} +3 -5
  7. package/dist/{chunk-FMPJBK2N.js.map → chunk-TVSADUYU.js.map} +1 -1
  8. package/dist/events/index.d.ts +43 -3
  9. package/dist/events/index.js +7 -3
  10. package/dist/{index-Cd0IZ40L.d.ts → index-CdXDv30T.d.ts} +10 -2
  11. package/dist/index.d.ts +5 -4
  12. package/dist/index.js +159 -46
  13. package/dist/index.js.map +1 -1
  14. package/dist/scheduler/index.d.ts +2 -2
  15. package/dist/scheduler/index.js +1 -1
  16. package/dist/storage/index.d.ts +6 -4
  17. package/dist/storage/index.js +1 -1
  18. package/dist/{types-DSEjiLMj.d.ts → types-BfWe5yP6.d.ts} +2 -0
  19. package/package.json +26 -14
  20. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +0 -146
  21. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +0 -74
  22. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  23. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +0 -161
  24. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +0 -34
  25. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  26. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +0 -152
  27. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +0 -34
  28. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  29. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +0 -93
  30. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  31. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +0 -138
  32. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +0 -121
  33. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  34. package/.codekin/reports/security/2026-04-11_security-audit.md +0 -297
  35. package/.github/CODEOWNERS +0 -15
  36. package/.github/dependabot.yml +0 -23
  37. package/.github/workflows/ci.yml +0 -38
  38. package/.github/workflows/publish.yml +0 -42
  39. package/.github/workflows/security.yml +0 -29
  40. package/CHANGELOG.md +0 -34
  41. package/SECURITY.md +0 -40
  42. package/dist/chunk-5XJBAPXF.js.map +0 -1
  43. package/dist/chunk-U3PCVK6O.js.map +0 -1
  44. package/docs/github-packages-setup.md +0 -97
  45. package/docs/stepflow-api-reference.md +0 -1663
  46. package/src/core/engine/concurrency.ts +0 -229
  47. package/src/core/engine/lifecycle.ts +0 -176
  48. package/src/core/engine.test.ts +0 -1435
  49. package/src/core/engine.ts +0 -598
  50. package/src/core/index.ts +0 -7
  51. package/src/core/orchestrator/persistence.ts +0 -165
  52. package/src/core/orchestrator/plan-resolution.ts +0 -140
  53. package/src/core/orchestrator/state-transitions.ts +0 -109
  54. package/src/core/orchestrator/step-runner.ts +0 -427
  55. package/src/core/orchestrator.ts +0 -252
  56. package/src/core/types.ts +0 -220
  57. package/src/events/index.ts +0 -8
  58. package/src/events/memory.test.ts +0 -107
  59. package/src/events/memory.ts +0 -92
  60. package/src/events/socketio.test.ts +0 -378
  61. package/src/events/socketio.ts +0 -262
  62. package/src/events/types.ts +0 -118
  63. package/src/events/webhook.test.ts +0 -949
  64. package/src/events/webhook.ts +0 -807
  65. package/src/index.ts +0 -270
  66. package/src/planning/evaluate-condition.test.ts +0 -153
  67. package/src/planning/evaluate-condition.ts +0 -105
  68. package/src/planning/index.ts +0 -55
  69. package/src/planning/planner.ts +0 -494
  70. package/src/planning/planning.test.ts +0 -727
  71. package/src/planning/registry.test.ts +0 -398
  72. package/src/planning/registry.ts +0 -283
  73. package/src/planning/types.ts +0 -566
  74. package/src/scheduler/cron.test.ts +0 -479
  75. package/src/scheduler/cron.ts +0 -438
  76. package/src/scheduler/index.ts +0 -8
  77. package/src/scheduler/postgres-persistence.ts +0 -530
  78. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  79. package/src/scheduler/sqlite-persistence.ts +0 -286
  80. package/src/scheduler/types.ts +0 -73
  81. package/src/storage/index.ts +0 -27
  82. package/src/storage/memory.test.ts +0 -349
  83. package/src/storage/memory.ts +0 -227
  84. package/src/storage/postgres-core.ts +0 -550
  85. package/src/storage/postgres-migration.test.ts +0 -69
  86. package/src/storage/postgres-transaction-pagination.test.ts +0 -107
  87. package/src/storage/postgres.ts +0 -835
  88. package/src/storage/sqlite-core.ts +0 -151
  89. package/src/storage/sqlite.test.ts +0 -642
  90. package/src/storage/sqlite.ts +0 -717
  91. package/src/storage/types.ts +0 -343
  92. package/src/utils/errors.test.ts +0 -150
  93. package/src/utils/errors.ts +0 -161
  94. package/src/utils/id.ts +0 -21
  95. package/src/utils/index.ts +0 -10
  96. package/src/utils/logger.test.ts +0 -121
  97. package/src/utils/logger.ts +0 -96
  98. package/src/utils/pinned-fetch.test.ts +0 -84
  99. package/src/utils/pinned-fetch.ts +0 -118
  100. package/src/utils/postgres-deps.ts +0 -79
  101. package/src/utils/retry.test.ts +0 -177
  102. package/src/utils/retry.ts +0 -121
  103. package/src/utils/safe-json.test.ts +0 -166
  104. package/src/utils/safe-json.ts +0 -107
  105. package/src/utils/webhook-signature.test.ts +0 -111
  106. package/src/utils/webhook-signature.ts +0 -74
  107. package/tsconfig.json +0 -21
  108. package/tsup.config.ts +0 -14
  109. 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
- }