@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,479 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { CronScheduler, type CronSchedulerConfig, type SchedulePersistence } from './cron';
3
- import { WorkflowEngine } from '../core/engine';
4
- import { MemoryStorageAdapter } from '../storage/memory';
5
- import { MemoryEventTransport } from '../events/memory';
6
- import { SilentLogger } from '../utils/logger';
7
- import type { WorkflowSchedule } from './types';
8
-
9
- describe('CronScheduler', () => {
10
- let engine: WorkflowEngine;
11
- let scheduler: CronScheduler;
12
-
13
- beforeEach(() => {
14
- // Create a workflow engine with silent logger
15
- engine = new WorkflowEngine({
16
- storage: new MemoryStorageAdapter(),
17
- events: new MemoryEventTransport(),
18
- logger: new SilentLogger(),
19
- });
20
-
21
- // Register a test workflow
22
- engine.registerWorkflow({
23
- kind: 'test.workflow',
24
- name: 'Test Workflow',
25
- steps: [
26
- {
27
- key: 'step1',
28
- name: 'Step 1',
29
- handler: async (ctx) => ({ value: ctx.input.value ?? 'default' }),
30
- },
31
- ],
32
- });
33
-
34
- // Create scheduler
35
- scheduler = new CronScheduler({
36
- engine,
37
- logger: new SilentLogger(),
38
- pollInterval: 100, // Fast polling for tests
39
- });
40
- });
41
-
42
- afterEach(async () => {
43
- await scheduler.stop();
44
- await engine.shutdown();
45
- });
46
-
47
- describe('addSchedule', () => {
48
- it('should add a cron schedule', async () => {
49
- const schedule = await scheduler.addSchedule({
50
- workflowKind: 'test.workflow',
51
- triggerType: 'cron',
52
- cronExpression: '0 * * * *', // Every hour
53
- enabled: true,
54
- });
55
-
56
- expect(schedule.id).toBeDefined();
57
- expect(schedule.workflowKind).toBe('test.workflow');
58
- expect(schedule.triggerType).toBe('cron');
59
- expect(schedule.cronExpression).toBe('0 * * * *');
60
- expect(schedule.nextRunAt).toBeInstanceOf(Date);
61
- });
62
-
63
- it('should add a workflow completion trigger', async () => {
64
- const schedule = await scheduler.addSchedule({
65
- workflowKind: 'notification.send',
66
- triggerType: 'workflow_completed',
67
- triggerOnWorkflowKind: 'order.process',
68
- triggerOnStatus: ['succeeded'],
69
- enabled: true,
70
- });
71
-
72
- expect(schedule.id).toBeDefined();
73
- expect(schedule.triggerType).toBe('workflow_completed');
74
- expect(schedule.triggerOnWorkflowKind).toBe('order.process');
75
- });
76
-
77
- it('should reject invalid cron expression', async () => {
78
- await expect(
79
- scheduler.addSchedule({
80
- workflowKind: 'test.workflow',
81
- triggerType: 'cron',
82
- cronExpression: 'invalid cron',
83
- enabled: true,
84
- })
85
- ).rejects.toThrow('Invalid cron expression');
86
- });
87
-
88
- it('should require triggerOnWorkflowKind for workflow_completed triggers', async () => {
89
- await expect(
90
- scheduler.addSchedule({
91
- workflowKind: 'test.workflow',
92
- triggerType: 'workflow_completed',
93
- enabled: true,
94
- })
95
- ).rejects.toThrow('triggerOnWorkflowKind is required');
96
- });
97
- });
98
-
99
- describe('removeSchedule', () => {
100
- it('should remove a schedule', async () => {
101
- const schedule = await scheduler.addSchedule({
102
- workflowKind: 'test.workflow',
103
- triggerType: 'cron',
104
- cronExpression: '0 * * * *',
105
- enabled: true,
106
- });
107
-
108
- await scheduler.removeSchedule(schedule.id);
109
-
110
- const schedules = await scheduler.getSchedules();
111
- expect(schedules).toHaveLength(0);
112
- });
113
-
114
- it('should throw for non-existent schedule', async () => {
115
- await expect(scheduler.removeSchedule('nonexistent')).rejects.toThrow(
116
- 'Schedule not found'
117
- );
118
- });
119
- });
120
-
121
- describe('updateSchedule', () => {
122
- it('should update schedule properties', async () => {
123
- const schedule = await scheduler.addSchedule({
124
- workflowKind: 'test.workflow',
125
- triggerType: 'cron',
126
- cronExpression: '0 * * * *',
127
- enabled: true,
128
- });
129
-
130
- await scheduler.updateSchedule(schedule.id, { enabled: false });
131
-
132
- const updated = scheduler.getSchedule(schedule.id);
133
- expect(updated?.enabled).toBe(false);
134
- });
135
-
136
- it('should update cron expression and recalculate next run', async () => {
137
- const schedule = await scheduler.addSchedule({
138
- workflowKind: 'test.workflow',
139
- triggerType: 'cron',
140
- cronExpression: '0 * * * *',
141
- enabled: true,
142
- });
143
-
144
- const originalNextRun = schedule.nextRunAt;
145
-
146
- await scheduler.updateSchedule(schedule.id, {
147
- cronExpression: '30 * * * *',
148
- });
149
-
150
- const updated = scheduler.getSchedule(schedule.id);
151
- expect(updated?.cronExpression).toBe('30 * * * *');
152
- expect(updated?.nextRunAt).not.toEqual(originalNextRun);
153
- });
154
- });
155
-
156
- describe('getSchedules', () => {
157
- it('should return all schedules', async () => {
158
- await scheduler.addSchedule({
159
- workflowKind: 'test.workflow',
160
- triggerType: 'cron',
161
- cronExpression: '0 * * * *',
162
- enabled: true,
163
- });
164
-
165
- await scheduler.addSchedule({
166
- workflowKind: 'test.workflow',
167
- triggerType: 'manual',
168
- enabled: true,
169
- });
170
-
171
- const schedules = await scheduler.getSchedules();
172
- expect(schedules).toHaveLength(2);
173
- });
174
- });
175
-
176
- describe('triggerNow', () => {
177
- it('should manually trigger a schedule', async () => {
178
- const schedule = await scheduler.addSchedule({
179
- workflowKind: 'test.workflow',
180
- triggerType: 'cron',
181
- cronExpression: '0 0 1 1 *', // Once a year (won't trigger naturally)
182
- input: { value: 'manual' },
183
- enabled: true,
184
- });
185
-
186
- const runId = await scheduler.triggerNow(schedule.id);
187
-
188
- expect(runId).toBeDefined();
189
-
190
- // Wait for workflow to complete
191
- const run = await engine.waitForRun(runId);
192
- expect(run.status).toBe('succeeded');
193
- });
194
-
195
- it('should throw for non-existent schedule', async () => {
196
- await expect(scheduler.triggerNow('nonexistent')).rejects.toThrow(
197
- 'Schedule not found'
198
- );
199
- });
200
- });
201
-
202
- describe('cron execution', () => {
203
- it('should execute scheduled workflow when cron time is reached', async () => {
204
- // Use fake timers
205
- vi.useFakeTimers();
206
-
207
- // Create a schedule that runs every minute
208
- await scheduler.addSchedule({
209
- workflowKind: 'test.workflow',
210
- triggerType: 'cron',
211
- cronExpression: '* * * * *', // Every minute
212
- input: { value: 'scheduled' },
213
- enabled: true,
214
- });
215
-
216
- await scheduler.start();
217
-
218
- // Advance time past the next minute boundary
219
- const now = new Date();
220
- const msUntilNextMinute = (60 - now.getSeconds()) * 1000 + 1000;
221
- vi.advanceTimersByTime(msUntilNextMinute);
222
-
223
- // Run pending timers for poll interval
224
- await vi.runOnlyPendingTimersAsync();
225
-
226
- // Stop the scheduler and shut down the engine while fake timers are
227
- // still active so that any setImmediate/setTimeout handles created
228
- // under fake timers are cleared with the fake clear* functions.
229
- // Clearing fake-timer handles with real clearImmediate corrupts
230
- // Node 20/22's internal timer state and breaks subsequent tests.
231
- await scheduler.stop();
232
- await engine.shutdown();
233
-
234
- vi.useRealTimers();
235
-
236
- // Note: With fake timers, the actual workflow may not complete
237
- // This test verifies the scheduler logic, not full execution
238
- });
239
- });
240
-
241
- describe('workflow completion triggers', () => {
242
- it('should trigger workflow on completion of another workflow', async () => {
243
- // Register a trigger workflow
244
- engine.registerWorkflow({
245
- kind: 'trigger.workflow',
246
- name: 'Trigger Workflow',
247
- steps: [
248
- {
249
- key: 'trigger',
250
- name: 'Trigger',
251
- handler: async () => ({ triggered: true }),
252
- },
253
- ],
254
- });
255
-
256
- // Register a dependent workflow
257
- engine.registerWorkflow({
258
- kind: 'dependent.workflow',
259
- name: 'Dependent Workflow',
260
- steps: [
261
- {
262
- key: 'dependent',
263
- name: 'Dependent',
264
- handler: async (ctx) => ({
265
- parentRunId: ctx.metadata.triggerRunId
266
- }),
267
- },
268
- ],
269
- });
270
-
271
- // Add a completion trigger
272
- await scheduler.addSchedule({
273
- workflowKind: 'dependent.workflow',
274
- triggerType: 'workflow_completed',
275
- triggerOnWorkflowKind: 'trigger.workflow',
276
- triggerOnStatus: ['succeeded'],
277
- enabled: true,
278
- });
279
-
280
- await scheduler.start();
281
-
282
- // Start the trigger workflow
283
- const triggerRunId = await engine.startRun({ kind: 'trigger.workflow' });
284
-
285
- // Wait for trigger workflow to complete
286
- await engine.waitForRun(triggerRunId);
287
-
288
- // Give time for the completion trigger to fire and dependent workflow to start
289
- await new Promise(resolve => setTimeout(resolve, 200));
290
-
291
- // Check that the dependent workflow was started
292
- const storage = engine.getStorage();
293
- const { items } = await storage.listRuns({ kind: 'dependent.workflow' });
294
-
295
- expect(items.length).toBeGreaterThanOrEqual(1);
296
- });
297
-
298
- it('should not trigger on non-matching status', async () => {
299
- // Register a failing trigger workflow
300
- engine.registerWorkflow({
301
- kind: 'failing.workflow',
302
- name: 'Failing Workflow',
303
- steps: [
304
- {
305
- key: 'fail',
306
- name: 'Fail',
307
- handler: async () => {
308
- throw new Error('Intentional failure');
309
- },
310
- },
311
- ],
312
- });
313
-
314
- // Add a completion trigger for success only
315
- await scheduler.addSchedule({
316
- workflowKind: 'test.workflow',
317
- triggerType: 'workflow_completed',
318
- triggerOnWorkflowKind: 'failing.workflow',
319
- triggerOnStatus: ['succeeded'], // Only on success
320
- enabled: true,
321
- });
322
-
323
- await scheduler.start();
324
-
325
- // Start the failing workflow
326
- const runId = await engine.startRun({ kind: 'failing.workflow' });
327
-
328
- // Wait for it to fail
329
- await engine.waitForRun(runId);
330
-
331
- // Give time for any triggers
332
- await new Promise(resolve => setTimeout(resolve, 200));
333
-
334
- // Check that test.workflow was NOT started
335
- const storage = engine.getStorage();
336
- const { items } = await storage.listRuns({ kind: 'test.workflow' });
337
-
338
- expect(items).toHaveLength(0);
339
- });
340
- });
341
-
342
- describe('start/stop', () => {
343
- it('should start and stop without error', async () => {
344
- await scheduler.start();
345
- await scheduler.stop();
346
- });
347
-
348
- it('should warn when starting twice', async () => {
349
- await scheduler.start();
350
- await scheduler.start(); // Should just warn
351
- await scheduler.stop();
352
- });
353
-
354
- it('should handle stopping when not started', async () => {
355
- await scheduler.stop(); // Should do nothing
356
- });
357
- });
358
-
359
- describe('persistence', () => {
360
- it('should load schedules from persistence on start', async () => {
361
- const mockSchedule: WorkflowSchedule = {
362
- id: 'persisted-1',
363
- workflowKind: 'test.workflow',
364
- triggerType: 'cron',
365
- cronExpression: '0 * * * *',
366
- enabled: true,
367
- };
368
-
369
- const mockPersistence: SchedulePersistence = {
370
- loadSchedules: vi.fn().mockResolvedValue([mockSchedule]),
371
- saveSchedule: vi.fn().mockResolvedValue(undefined),
372
- updateSchedule: vi.fn().mockResolvedValue(undefined),
373
- deleteSchedule: vi.fn().mockResolvedValue(undefined),
374
- };
375
-
376
- const schedulerWithPersistence = new CronScheduler({
377
- engine,
378
- logger: new SilentLogger(),
379
- persistence: mockPersistence,
380
- });
381
-
382
- await schedulerWithPersistence.start();
383
-
384
- expect(mockPersistence.loadSchedules).toHaveBeenCalled();
385
-
386
- const schedules = await schedulerWithPersistence.getSchedules();
387
- expect(schedules).toHaveLength(1);
388
- expect(schedules[0].id).toBe('persisted-1');
389
-
390
- await schedulerWithPersistence.stop();
391
- });
392
-
393
- it('should save schedule to persistence when added', async () => {
394
- const mockPersistence: SchedulePersistence = {
395
- loadSchedules: vi.fn().mockResolvedValue([]),
396
- saveSchedule: vi.fn().mockResolvedValue(undefined),
397
- updateSchedule: vi.fn().mockResolvedValue(undefined),
398
- deleteSchedule: vi.fn().mockResolvedValue(undefined),
399
- };
400
-
401
- const schedulerWithPersistence = new CronScheduler({
402
- engine,
403
- logger: new SilentLogger(),
404
- persistence: mockPersistence,
405
- });
406
-
407
- await schedulerWithPersistence.addSchedule({
408
- workflowKind: 'test.workflow',
409
- triggerType: 'cron',
410
- cronExpression: '0 * * * *',
411
- enabled: true,
412
- });
413
-
414
- expect(mockPersistence.saveSchedule).toHaveBeenCalled();
415
- });
416
- });
417
-
418
- describe('updateNextRunTime parse-error path', () => {
419
- it('should set nextRunAt to undefined and log error for malformed cron expression update', async () => {
420
- const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
421
- const errorScheduler = new CronScheduler({
422
- engine,
423
- logger,
424
- pollInterval: 100,
425
- });
426
-
427
- // Add a valid schedule first
428
- const schedule = await errorScheduler.addSchedule({
429
- workflowKind: 'test.workflow',
430
- triggerType: 'cron',
431
- cronExpression: '0 * * * *',
432
- enabled: true,
433
- });
434
-
435
- expect(schedule.nextRunAt).toBeInstanceOf(Date);
436
-
437
- // Now directly update the internal schedule with a malformed expression
438
- // to trigger the error path in updateNextRunTime
439
- // We need to bypass the validation in updateSchedule by manipulating the internal state.
440
- // The updateSchedule method validates and throws, but updateNextRunTime is called
441
- // during start() when loading from persistence — so we test via start() loading bad data.
442
- const mockPersistence: SchedulePersistence = {
443
- loadSchedules: vi.fn().mockResolvedValue([
444
- {
445
- id: 'bad-cron',
446
- workflowKind: 'test.workflow',
447
- triggerType: 'cron' as const,
448
- cronExpression: '99 99 99 99 99',
449
- enabled: true,
450
- },
451
- ]),
452
- saveSchedule: vi.fn().mockResolvedValue(undefined),
453
- updateSchedule: vi.fn().mockResolvedValue(undefined),
454
- deleteSchedule: vi.fn().mockResolvedValue(undefined),
455
- };
456
-
457
- const badScheduler = new CronScheduler({
458
- engine,
459
- logger,
460
- persistence: mockPersistence,
461
- pollInterval: 100,
462
- });
463
-
464
- await badScheduler.start();
465
-
466
- // The schedule should have been loaded with nextRunAt = undefined due to parse error
467
- const loaded = badScheduler.getSchedule('bad-cron');
468
- expect(loaded).toBeDefined();
469
- expect(loaded!.nextRunAt).toBeUndefined();
470
- expect(logger.error).toHaveBeenCalledWith(
471
- expect.stringContaining('Invalid cron expression'),
472
- expect.anything()
473
- );
474
-
475
- await badScheduler.stop();
476
- await errorScheduler.stop();
477
- });
478
- });
479
- });