@multiplier-labs/stepflow 0.3.3 → 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 (84) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-GYPNJBDP.js → chunk-D5HW7VEG.js} +269 -48
  3. package/dist/chunk-D5HW7VEG.js.map +1 -0
  4. package/dist/{chunk-KHTUWLVZ.js → chunk-FMPJBK2N.js} +55 -10
  5. package/dist/chunk-FMPJBK2N.js.map +1 -0
  6. package/dist/{chunk-ML35PIHX.js → chunk-JPXPB7U5.js} +52 -2
  7. package/dist/chunk-JPXPB7U5.js.map +1 -0
  8. package/dist/{chunk-4VOKMGP6.js → chunk-JUPZEBR3.js} +519 -522
  9. package/dist/chunk-JUPZEBR3.js.map +1 -0
  10. package/dist/events/index.d.ts +58 -3
  11. package/dist/events/index.js +1 -1
  12. package/dist/{index-Dzr437iW.d.ts → index-Cd0IZ40L.d.ts} +50 -11
  13. package/dist/index.d.ts +2 -12
  14. package/dist/index.js +764 -485
  15. package/dist/index.js.map +1 -1
  16. package/dist/scheduler/index.d.ts +1 -1
  17. package/dist/scheduler/index.js +2 -2
  18. package/dist/storage/index.d.ts +313 -47
  19. package/dist/storage/index.js +4 -2
  20. package/package.json +27 -13
  21. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  22. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  23. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  24. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  25. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  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 -36
  30. package/.github/workflows/security.yml +0 -29
  31. package/CHANGELOG.md +0 -34
  32. package/SECURITY.md +0 -40
  33. package/dist/chunk-4VOKMGP6.js.map +0 -1
  34. package/dist/chunk-GYPNJBDP.js.map +0 -1
  35. package/dist/chunk-KHTUWLVZ.js.map +0 -1
  36. package/dist/chunk-ML35PIHX.js.map +0 -1
  37. package/docs/github-packages-setup.md +0 -97
  38. package/docs/stepflow-api-reference.md +0 -1661
  39. package/src/core/engine.test.ts +0 -1365
  40. package/src/core/engine.ts +0 -692
  41. package/src/core/index.ts +0 -7
  42. package/src/core/orchestrator.ts +0 -571
  43. package/src/core/types.ts +0 -220
  44. package/src/events/index.ts +0 -8
  45. package/src/events/memory.test.ts +0 -107
  46. package/src/events/memory.ts +0 -92
  47. package/src/events/socketio.test.ts +0 -378
  48. package/src/events/socketio.ts +0 -262
  49. package/src/events/types.ts +0 -118
  50. package/src/events/webhook.test.ts +0 -657
  51. package/src/events/webhook.ts +0 -564
  52. package/src/index.ts +0 -270
  53. package/src/planning/index.ts +0 -55
  54. package/src/planning/planner.ts +0 -576
  55. package/src/planning/planning.test.ts +0 -727
  56. package/src/planning/registry.test.ts +0 -398
  57. package/src/planning/registry.ts +0 -365
  58. package/src/planning/types.ts +0 -566
  59. package/src/scheduler/cron.test.ts +0 -471
  60. package/src/scheduler/cron.ts +0 -414
  61. package/src/scheduler/index.ts +0 -8
  62. package/src/scheduler/postgres-persistence.ts +0 -518
  63. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  64. package/src/scheduler/sqlite-persistence.ts +0 -261
  65. package/src/scheduler/types.ts +0 -73
  66. package/src/storage/index.ts +0 -27
  67. package/src/storage/memory.test.ts +0 -349
  68. package/src/storage/memory.ts +0 -227
  69. package/src/storage/postgres.ts +0 -1354
  70. package/src/storage/sqlite.test.ts +0 -466
  71. package/src/storage/sqlite.ts +0 -627
  72. package/src/storage/types.ts +0 -343
  73. package/src/utils/errors.test.ts +0 -150
  74. package/src/utils/errors.ts +0 -161
  75. package/src/utils/id.ts +0 -21
  76. package/src/utils/index.ts +0 -8
  77. package/src/utils/logger.test.ts +0 -121
  78. package/src/utils/logger.ts +0 -96
  79. package/src/utils/postgres-deps.ts +0 -63
  80. package/src/utils/retry.test.ts +0 -177
  81. package/src/utils/retry.ts +0 -121
  82. package/tsconfig.json +0 -21
  83. package/tsup.config.ts +0 -14
  84. package/vitest.config.ts +0 -9
@@ -1,471 +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
- vi.useRealTimers();
227
-
228
- // Note: With fake timers, the actual workflow may not complete
229
- // This test verifies the scheduler logic, not full execution
230
- });
231
- });
232
-
233
- describe('workflow completion triggers', () => {
234
- it('should trigger workflow on completion of another workflow', async () => {
235
- // Register a trigger workflow
236
- engine.registerWorkflow({
237
- kind: 'trigger.workflow',
238
- name: 'Trigger Workflow',
239
- steps: [
240
- {
241
- key: 'trigger',
242
- name: 'Trigger',
243
- handler: async () => ({ triggered: true }),
244
- },
245
- ],
246
- });
247
-
248
- // Register a dependent workflow
249
- engine.registerWorkflow({
250
- kind: 'dependent.workflow',
251
- name: 'Dependent Workflow',
252
- steps: [
253
- {
254
- key: 'dependent',
255
- name: 'Dependent',
256
- handler: async (ctx) => ({
257
- parentRunId: ctx.metadata.triggerRunId
258
- }),
259
- },
260
- ],
261
- });
262
-
263
- // Add a completion trigger
264
- await scheduler.addSchedule({
265
- workflowKind: 'dependent.workflow',
266
- triggerType: 'workflow_completed',
267
- triggerOnWorkflowKind: 'trigger.workflow',
268
- triggerOnStatus: ['succeeded'],
269
- enabled: true,
270
- });
271
-
272
- await scheduler.start();
273
-
274
- // Start the trigger workflow
275
- const triggerRunId = await engine.startRun({ kind: 'trigger.workflow' });
276
-
277
- // Wait for trigger workflow to complete
278
- await engine.waitForRun(triggerRunId);
279
-
280
- // Give time for the completion trigger to fire and dependent workflow to start
281
- await new Promise(resolve => setTimeout(resolve, 200));
282
-
283
- // Check that the dependent workflow was started
284
- const storage = engine.getStorage();
285
- const { items } = await storage.listRuns({ kind: 'dependent.workflow' });
286
-
287
- expect(items.length).toBeGreaterThanOrEqual(1);
288
- });
289
-
290
- it('should not trigger on non-matching status', async () => {
291
- // Register a failing trigger workflow
292
- engine.registerWorkflow({
293
- kind: 'failing.workflow',
294
- name: 'Failing Workflow',
295
- steps: [
296
- {
297
- key: 'fail',
298
- name: 'Fail',
299
- handler: async () => {
300
- throw new Error('Intentional failure');
301
- },
302
- },
303
- ],
304
- });
305
-
306
- // Add a completion trigger for success only
307
- await scheduler.addSchedule({
308
- workflowKind: 'test.workflow',
309
- triggerType: 'workflow_completed',
310
- triggerOnWorkflowKind: 'failing.workflow',
311
- triggerOnStatus: ['succeeded'], // Only on success
312
- enabled: true,
313
- });
314
-
315
- await scheduler.start();
316
-
317
- // Start the failing workflow
318
- const runId = await engine.startRun({ kind: 'failing.workflow' });
319
-
320
- // Wait for it to fail
321
- await engine.waitForRun(runId);
322
-
323
- // Give time for any triggers
324
- await new Promise(resolve => setTimeout(resolve, 200));
325
-
326
- // Check that test.workflow was NOT started
327
- const storage = engine.getStorage();
328
- const { items } = await storage.listRuns({ kind: 'test.workflow' });
329
-
330
- expect(items).toHaveLength(0);
331
- });
332
- });
333
-
334
- describe('start/stop', () => {
335
- it('should start and stop without error', async () => {
336
- await scheduler.start();
337
- await scheduler.stop();
338
- });
339
-
340
- it('should warn when starting twice', async () => {
341
- await scheduler.start();
342
- await scheduler.start(); // Should just warn
343
- await scheduler.stop();
344
- });
345
-
346
- it('should handle stopping when not started', async () => {
347
- await scheduler.stop(); // Should do nothing
348
- });
349
- });
350
-
351
- describe('persistence', () => {
352
- it('should load schedules from persistence on start', async () => {
353
- const mockSchedule: WorkflowSchedule = {
354
- id: 'persisted-1',
355
- workflowKind: 'test.workflow',
356
- triggerType: 'cron',
357
- cronExpression: '0 * * * *',
358
- enabled: true,
359
- };
360
-
361
- const mockPersistence: SchedulePersistence = {
362
- loadSchedules: vi.fn().mockResolvedValue([mockSchedule]),
363
- saveSchedule: vi.fn().mockResolvedValue(undefined),
364
- updateSchedule: vi.fn().mockResolvedValue(undefined),
365
- deleteSchedule: vi.fn().mockResolvedValue(undefined),
366
- };
367
-
368
- const schedulerWithPersistence = new CronScheduler({
369
- engine,
370
- logger: new SilentLogger(),
371
- persistence: mockPersistence,
372
- });
373
-
374
- await schedulerWithPersistence.start();
375
-
376
- expect(mockPersistence.loadSchedules).toHaveBeenCalled();
377
-
378
- const schedules = await schedulerWithPersistence.getSchedules();
379
- expect(schedules).toHaveLength(1);
380
- expect(schedules[0].id).toBe('persisted-1');
381
-
382
- await schedulerWithPersistence.stop();
383
- });
384
-
385
- it('should save schedule to persistence when added', async () => {
386
- const mockPersistence: SchedulePersistence = {
387
- loadSchedules: vi.fn().mockResolvedValue([]),
388
- saveSchedule: vi.fn().mockResolvedValue(undefined),
389
- updateSchedule: vi.fn().mockResolvedValue(undefined),
390
- deleteSchedule: vi.fn().mockResolvedValue(undefined),
391
- };
392
-
393
- const schedulerWithPersistence = new CronScheduler({
394
- engine,
395
- logger: new SilentLogger(),
396
- persistence: mockPersistence,
397
- });
398
-
399
- await schedulerWithPersistence.addSchedule({
400
- workflowKind: 'test.workflow',
401
- triggerType: 'cron',
402
- cronExpression: '0 * * * *',
403
- enabled: true,
404
- });
405
-
406
- expect(mockPersistence.saveSchedule).toHaveBeenCalled();
407
- });
408
- });
409
-
410
- describe('updateNextRunTime parse-error path', () => {
411
- it('should set nextRunAt to undefined and log error for malformed cron expression update', async () => {
412
- const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
413
- const errorScheduler = new CronScheduler({
414
- engine,
415
- logger,
416
- pollInterval: 100,
417
- });
418
-
419
- // Add a valid schedule first
420
- const schedule = await errorScheduler.addSchedule({
421
- workflowKind: 'test.workflow',
422
- triggerType: 'cron',
423
- cronExpression: '0 * * * *',
424
- enabled: true,
425
- });
426
-
427
- expect(schedule.nextRunAt).toBeInstanceOf(Date);
428
-
429
- // Now directly update the internal schedule with a malformed expression
430
- // to trigger the error path in updateNextRunTime
431
- // We need to bypass the validation in updateSchedule by manipulating the internal state.
432
- // The updateSchedule method validates and throws, but updateNextRunTime is called
433
- // during start() when loading from persistence — so we test via start() loading bad data.
434
- const mockPersistence: SchedulePersistence = {
435
- loadSchedules: vi.fn().mockResolvedValue([
436
- {
437
- id: 'bad-cron',
438
- workflowKind: 'test.workflow',
439
- triggerType: 'cron' as const,
440
- cronExpression: '99 99 99 99 99',
441
- enabled: true,
442
- },
443
- ]),
444
- saveSchedule: vi.fn().mockResolvedValue(undefined),
445
- updateSchedule: vi.fn().mockResolvedValue(undefined),
446
- deleteSchedule: vi.fn().mockResolvedValue(undefined),
447
- };
448
-
449
- const badScheduler = new CronScheduler({
450
- engine,
451
- logger,
452
- persistence: mockPersistence,
453
- pollInterval: 100,
454
- });
455
-
456
- await badScheduler.start();
457
-
458
- // The schedule should have been loaded with nextRunAt = undefined due to parse error
459
- const loaded = badScheduler.getSchedule('bad-cron');
460
- expect(loaded).toBeDefined();
461
- expect(loaded!.nextRunAt).toBeUndefined();
462
- expect(logger.error).toHaveBeenCalledWith(
463
- expect.stringContaining('Failed to parse cron expression'),
464
- expect.anything()
465
- );
466
-
467
- await badScheduler.stop();
468
- await errorScheduler.stop();
469
- });
470
- });
471
- });