@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.
- package/README.md +9 -0
- package/dist/{chunk-GYPNJBDP.js → chunk-D5HW7VEG.js} +269 -48
- package/dist/chunk-D5HW7VEG.js.map +1 -0
- package/dist/{chunk-KHTUWLVZ.js → chunk-FMPJBK2N.js} +55 -10
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-ML35PIHX.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-4VOKMGP6.js → chunk-JUPZEBR3.js} +519 -522
- package/dist/chunk-JUPZEBR3.js.map +1 -0
- package/dist/events/index.d.ts +58 -3
- package/dist/events/index.js +1 -1
- package/dist/{index-Dzr437iW.d.ts → index-Cd0IZ40L.d.ts} +50 -11
- package/dist/index.d.ts +2 -12
- package/dist/index.js +764 -485
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +313 -47
- package/dist/storage/index.js +4 -2
- package/package.json +27 -13
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
- package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
- package/.github/CODEOWNERS +0 -15
- package/.github/dependabot.yml +0 -23
- package/.github/workflows/ci.yml +0 -38
- package/.github/workflows/publish.yml +0 -36
- package/.github/workflows/security.yml +0 -29
- package/CHANGELOG.md +0 -34
- package/SECURITY.md +0 -40
- package/dist/chunk-4VOKMGP6.js.map +0 -1
- package/dist/chunk-GYPNJBDP.js.map +0 -1
- package/dist/chunk-KHTUWLVZ.js.map +0 -1
- package/dist/chunk-ML35PIHX.js.map +0 -1
- package/docs/github-packages-setup.md +0 -97
- package/docs/stepflow-api-reference.md +0 -1661
- package/src/core/engine.test.ts +0 -1365
- package/src/core/engine.ts +0 -692
- package/src/core/index.ts +0 -7
- package/src/core/orchestrator.ts +0 -571
- package/src/core/types.ts +0 -220
- package/src/events/index.ts +0 -8
- package/src/events/memory.test.ts +0 -107
- package/src/events/memory.ts +0 -92
- package/src/events/socketio.test.ts +0 -378
- package/src/events/socketio.ts +0 -262
- package/src/events/types.ts +0 -118
- package/src/events/webhook.test.ts +0 -657
- package/src/events/webhook.ts +0 -564
- package/src/index.ts +0 -270
- package/src/planning/index.ts +0 -55
- package/src/planning/planner.ts +0 -576
- package/src/planning/planning.test.ts +0 -727
- package/src/planning/registry.test.ts +0 -398
- package/src/planning/registry.ts +0 -365
- package/src/planning/types.ts +0 -566
- package/src/scheduler/cron.test.ts +0 -471
- package/src/scheduler/cron.ts +0 -414
- package/src/scheduler/index.ts +0 -8
- package/src/scheduler/postgres-persistence.ts +0 -518
- package/src/scheduler/sqlite-persistence.test.ts +0 -235
- package/src/scheduler/sqlite-persistence.ts +0 -261
- package/src/scheduler/types.ts +0 -73
- package/src/storage/index.ts +0 -27
- package/src/storage/memory.test.ts +0 -349
- package/src/storage/memory.ts +0 -227
- package/src/storage/postgres.ts +0 -1354
- package/src/storage/sqlite.test.ts +0 -466
- package/src/storage/sqlite.ts +0 -627
- package/src/storage/types.ts +0 -343
- package/src/utils/errors.test.ts +0 -150
- package/src/utils/errors.ts +0 -161
- package/src/utils/id.ts +0 -21
- package/src/utils/index.ts +0 -8
- package/src/utils/logger.test.ts +0 -121
- package/src/utils/logger.ts +0 -96
- package/src/utils/postgres-deps.ts +0 -63
- package/src/utils/retry.test.ts +0 -177
- package/src/utils/retry.ts +0 -121
- package/tsconfig.json +0 -21
- package/tsup.config.ts +0 -14
- package/vitest.config.ts +0 -9
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { SocketIOEventTransport, SocketIOServer, SocketIOSocket } from './socketio';
|
|
3
|
-
import type { WorkflowEvent } from './types';
|
|
4
|
-
|
|
5
|
-
// Mock Socket.IO server
|
|
6
|
-
function createMockServer(): SocketIOServer & {
|
|
7
|
-
_rooms: Map<string, { emit: ReturnType<typeof vi.fn> }>;
|
|
8
|
-
_emitCalls: Array<{ event: string; args: unknown[] }>;
|
|
9
|
-
} {
|
|
10
|
-
const rooms = new Map<string, { emit: ReturnType<typeof vi.fn> }>();
|
|
11
|
-
const emitCalls: Array<{ event: string; args: unknown[] }> = [];
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
_rooms: rooms,
|
|
15
|
-
_emitCalls: emitCalls,
|
|
16
|
-
to(room: string) {
|
|
17
|
-
if (!rooms.has(room)) {
|
|
18
|
-
rooms.set(room, { emit: vi.fn() });
|
|
19
|
-
}
|
|
20
|
-
return rooms.get(room)!;
|
|
21
|
-
},
|
|
22
|
-
emit(event: string, ...args: unknown[]) {
|
|
23
|
-
emitCalls.push({ event, args });
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Mock Socket.IO socket
|
|
29
|
-
function createMockSocket(): SocketIOSocket & {
|
|
30
|
-
_joinedRooms: Set<string>;
|
|
31
|
-
_handlers: Map<string, (...args: unknown[]) => void>;
|
|
32
|
-
} {
|
|
33
|
-
const joinedRooms = new Set<string>();
|
|
34
|
-
const handlers = new Map<string, (...args: unknown[]) => void>();
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
_joinedRooms: joinedRooms,
|
|
38
|
-
_handlers: handlers,
|
|
39
|
-
join(room: string) {
|
|
40
|
-
joinedRooms.add(room);
|
|
41
|
-
},
|
|
42
|
-
leave(room: string) {
|
|
43
|
-
joinedRooms.delete(room);
|
|
44
|
-
},
|
|
45
|
-
on(event: string, callback: (...args: unknown[]) => void) {
|
|
46
|
-
handlers.set(event, callback);
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function createTestEvent(overrides: Partial<WorkflowEvent> = {}): WorkflowEvent {
|
|
52
|
-
return {
|
|
53
|
-
eventType: 'run.started',
|
|
54
|
-
runId: 'run-123',
|
|
55
|
-
kind: 'test.workflow',
|
|
56
|
-
timestamp: new Date('2024-01-01T12:00:00Z'),
|
|
57
|
-
...overrides,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
describe('SocketIOEventTransport', () => {
|
|
62
|
-
let io: ReturnType<typeof createMockServer>;
|
|
63
|
-
let transport: SocketIOEventTransport;
|
|
64
|
-
|
|
65
|
-
beforeEach(() => {
|
|
66
|
-
io = createMockServer();
|
|
67
|
-
transport = new SocketIOEventTransport({ io });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe('emit', () => {
|
|
71
|
-
it('should emit event to run-specific room', () => {
|
|
72
|
-
const event = createTestEvent({ runId: 'run-456' });
|
|
73
|
-
transport.emit(event);
|
|
74
|
-
|
|
75
|
-
const roomEmit = io._rooms.get('run:run-456');
|
|
76
|
-
expect(roomEmit).toBeDefined();
|
|
77
|
-
expect(roomEmit!.emit).toHaveBeenCalledWith('workflow:event', {
|
|
78
|
-
...event,
|
|
79
|
-
timestamp: '2024-01-01T12:00:00.000Z',
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should emit event to global room by default', () => {
|
|
84
|
-
const event = createTestEvent();
|
|
85
|
-
transport.emit(event);
|
|
86
|
-
|
|
87
|
-
const globalRoomEmit = io._rooms.get('workflow:all');
|
|
88
|
-
expect(globalRoomEmit).toBeDefined();
|
|
89
|
-
expect(globalRoomEmit!.emit).toHaveBeenCalled();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should not emit to global room when broadcastGlobal is false', () => {
|
|
93
|
-
transport = new SocketIOEventTransport({ io, broadcastGlobal: false });
|
|
94
|
-
const event = createTestEvent();
|
|
95
|
-
transport.emit(event);
|
|
96
|
-
|
|
97
|
-
// Should only have run-specific room, not global
|
|
98
|
-
expect(io._rooms.size).toBe(1);
|
|
99
|
-
expect(io._rooms.has('run:run-123')).toBe(true);
|
|
100
|
-
expect(io._rooms.has('workflow:all')).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should use custom event name', () => {
|
|
104
|
-
transport = new SocketIOEventTransport({ io, eventName: 'custom:event' });
|
|
105
|
-
const event = createTestEvent();
|
|
106
|
-
transport.emit(event);
|
|
107
|
-
|
|
108
|
-
const roomEmit = io._rooms.get('run:run-123');
|
|
109
|
-
expect(roomEmit!.emit).toHaveBeenCalledWith('custom:event', expect.any(Object));
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should use custom room prefix', () => {
|
|
113
|
-
transport = new SocketIOEventTransport({ io, roomPrefix: 'workflow:' });
|
|
114
|
-
const event = createTestEvent({ runId: 'xyz' });
|
|
115
|
-
transport.emit(event);
|
|
116
|
-
|
|
117
|
-
expect(io._rooms.has('workflow:xyz')).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should serialize timestamp to ISO string', () => {
|
|
121
|
-
const event = createTestEvent({
|
|
122
|
-
timestamp: new Date('2024-06-15T10:30:00Z'),
|
|
123
|
-
});
|
|
124
|
-
transport.emit(event);
|
|
125
|
-
|
|
126
|
-
const roomEmit = io._rooms.get('run:run-123');
|
|
127
|
-
const emittedEvent = roomEmit!.emit.mock.calls[0][1] as Record<string, unknown>;
|
|
128
|
-
expect(emittedEvent.timestamp).toBe('2024-06-15T10:30:00.000Z');
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe('subscribe', () => {
|
|
133
|
-
it('should call callback when event matches runId', () => {
|
|
134
|
-
const callback = vi.fn();
|
|
135
|
-
transport.subscribe('run-123', callback);
|
|
136
|
-
|
|
137
|
-
const event = createTestEvent({ runId: 'run-123' });
|
|
138
|
-
transport.emit(event);
|
|
139
|
-
|
|
140
|
-
expect(callback).toHaveBeenCalledWith(event);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should not call callback for different runId', () => {
|
|
144
|
-
const callback = vi.fn();
|
|
145
|
-
transport.subscribe('run-123', callback);
|
|
146
|
-
|
|
147
|
-
const event = createTestEvent({ runId: 'run-456' });
|
|
148
|
-
transport.emit(event);
|
|
149
|
-
|
|
150
|
-
expect(callback).not.toHaveBeenCalled();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should allow multiple subscribers for same runId', () => {
|
|
154
|
-
const callback1 = vi.fn();
|
|
155
|
-
const callback2 = vi.fn();
|
|
156
|
-
transport.subscribe('run-123', callback1);
|
|
157
|
-
transport.subscribe('run-123', callback2);
|
|
158
|
-
|
|
159
|
-
const event = createTestEvent({ runId: 'run-123' });
|
|
160
|
-
transport.emit(event);
|
|
161
|
-
|
|
162
|
-
expect(callback1).toHaveBeenCalledWith(event);
|
|
163
|
-
expect(callback2).toHaveBeenCalledWith(event);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('should unsubscribe when calling returned function', () => {
|
|
167
|
-
const callback = vi.fn();
|
|
168
|
-
const unsubscribe = transport.subscribe('run-123', callback);
|
|
169
|
-
|
|
170
|
-
unsubscribe();
|
|
171
|
-
|
|
172
|
-
const event = createTestEvent({ runId: 'run-123' });
|
|
173
|
-
transport.emit(event);
|
|
174
|
-
|
|
175
|
-
expect(callback).not.toHaveBeenCalled();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('should handle callback errors gracefully', () => {
|
|
179
|
-
const errorCallback = vi.fn().mockImplementation(() => {
|
|
180
|
-
throw new Error('Callback error');
|
|
181
|
-
});
|
|
182
|
-
const normalCallback = vi.fn();
|
|
183
|
-
|
|
184
|
-
transport.subscribe('run-123', errorCallback);
|
|
185
|
-
transport.subscribe('run-123', normalCallback);
|
|
186
|
-
|
|
187
|
-
const event = createTestEvent({ runId: 'run-123' });
|
|
188
|
-
transport.emit(event);
|
|
189
|
-
|
|
190
|
-
// Both callbacks should have been called despite error
|
|
191
|
-
expect(errorCallback).toHaveBeenCalled();
|
|
192
|
-
expect(normalCallback).toHaveBeenCalled();
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe('subscribeAll', () => {
|
|
197
|
-
it('should call callback for all events', () => {
|
|
198
|
-
const callback = vi.fn();
|
|
199
|
-
transport.subscribeAll(callback);
|
|
200
|
-
|
|
201
|
-
const event1 = createTestEvent({ runId: 'run-1' });
|
|
202
|
-
const event2 = createTestEvent({ runId: 'run-2' });
|
|
203
|
-
transport.emit(event1);
|
|
204
|
-
transport.emit(event2);
|
|
205
|
-
|
|
206
|
-
expect(callback).toHaveBeenCalledTimes(2);
|
|
207
|
-
expect(callback).toHaveBeenCalledWith(event1);
|
|
208
|
-
expect(callback).toHaveBeenCalledWith(event2);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('should unsubscribe when calling returned function', () => {
|
|
212
|
-
const callback = vi.fn();
|
|
213
|
-
const unsubscribe = transport.subscribeAll(callback);
|
|
214
|
-
|
|
215
|
-
unsubscribe();
|
|
216
|
-
|
|
217
|
-
const event = createTestEvent();
|
|
218
|
-
transport.emit(event);
|
|
219
|
-
|
|
220
|
-
expect(callback).not.toHaveBeenCalled();
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
describe('setupClientHandlers', () => {
|
|
225
|
-
const allowAll = vi.fn().mockResolvedValue(true);
|
|
226
|
-
|
|
227
|
-
it('should register subscription handlers on socket', () => {
|
|
228
|
-
const socket = createMockSocket();
|
|
229
|
-
transport.setupClientHandlers(socket, allowAll);
|
|
230
|
-
|
|
231
|
-
expect(socket._handlers.has('workflow:subscribe')).toBe(true);
|
|
232
|
-
expect(socket._handlers.has('workflow:unsubscribe')).toBe(true);
|
|
233
|
-
expect(socket._handlers.has('workflow:subscribe:all')).toBe(true);
|
|
234
|
-
expect(socket._handlers.has('workflow:unsubscribe:all')).toBe(true);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('should join run-specific room on subscribe when authorized', async () => {
|
|
238
|
-
const socket = createMockSocket();
|
|
239
|
-
transport.setupClientHandlers(socket, allowAll);
|
|
240
|
-
|
|
241
|
-
const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
|
|
242
|
-
await subscribeHandler('run-123');
|
|
243
|
-
|
|
244
|
-
expect(socket._joinedRooms.has('run:run-123')).toBe(true);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('should leave run-specific room on unsubscribe', async () => {
|
|
248
|
-
const socket = createMockSocket();
|
|
249
|
-
transport.setupClientHandlers(socket, allowAll);
|
|
250
|
-
|
|
251
|
-
// First subscribe
|
|
252
|
-
const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
|
|
253
|
-
await subscribeHandler('run-123');
|
|
254
|
-
|
|
255
|
-
// Then unsubscribe
|
|
256
|
-
const unsubscribeHandler = socket._handlers.get('workflow:unsubscribe')!;
|
|
257
|
-
unsubscribeHandler('run-123');
|
|
258
|
-
|
|
259
|
-
expect(socket._joinedRooms.has('run:run-123')).toBe(false);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('should join global room on subscribe:all when authorized', async () => {
|
|
263
|
-
const socket = createMockSocket();
|
|
264
|
-
transport.setupClientHandlers(socket, allowAll);
|
|
265
|
-
|
|
266
|
-
const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
|
|
267
|
-
await subscribeAllHandler();
|
|
268
|
-
|
|
269
|
-
expect(socket._joinedRooms.has('workflow:all')).toBe(true);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('should leave global room on unsubscribe:all', async () => {
|
|
273
|
-
const socket = createMockSocket();
|
|
274
|
-
transport.setupClientHandlers(socket, allowAll);
|
|
275
|
-
|
|
276
|
-
// First subscribe
|
|
277
|
-
const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
|
|
278
|
-
await subscribeAllHandler();
|
|
279
|
-
|
|
280
|
-
// Then unsubscribe
|
|
281
|
-
const unsubscribeAllHandler = socket._handlers.get('workflow:unsubscribe:all')!;
|
|
282
|
-
unsubscribeAllHandler();
|
|
283
|
-
|
|
284
|
-
expect(socket._joinedRooms.has('workflow:all')).toBe(false);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('should ignore non-string runId on subscribe', async () => {
|
|
288
|
-
const socket = createMockSocket();
|
|
289
|
-
transport.setupClientHandlers(socket, allowAll);
|
|
290
|
-
|
|
291
|
-
const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
|
|
292
|
-
await subscribeHandler(123); // Not a string
|
|
293
|
-
await subscribeHandler(null);
|
|
294
|
-
await subscribeHandler(undefined);
|
|
295
|
-
|
|
296
|
-
expect(socket._joinedRooms.size).toBe(0);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('should deny run subscription when authorize returns false', async () => {
|
|
300
|
-
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
301
|
-
transport = new SocketIOEventTransport({ io, logger });
|
|
302
|
-
|
|
303
|
-
const socket = createMockSocket();
|
|
304
|
-
const authorize = vi.fn().mockResolvedValue(false);
|
|
305
|
-
transport.setupClientHandlers(socket, authorize);
|
|
306
|
-
|
|
307
|
-
const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
|
|
308
|
-
await subscribeHandler('run-secret');
|
|
309
|
-
|
|
310
|
-
expect(authorize).toHaveBeenCalledWith('run-secret', socket);
|
|
311
|
-
expect(socket._joinedRooms.has('run:run-secret')).toBe(false);
|
|
312
|
-
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('run-secret'));
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('should deny run subscription when authorize throws', async () => {
|
|
316
|
-
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
317
|
-
transport = new SocketIOEventTransport({ io, logger });
|
|
318
|
-
|
|
319
|
-
const socket = createMockSocket();
|
|
320
|
-
const authorize = vi.fn().mockRejectedValue(new Error('auth error'));
|
|
321
|
-
transport.setupClientHandlers(socket, authorize);
|
|
322
|
-
|
|
323
|
-
const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
|
|
324
|
-
await subscribeHandler('run-123');
|
|
325
|
-
|
|
326
|
-
expect(socket._joinedRooms.has('run:run-123')).toBe(false);
|
|
327
|
-
expect(logger.error).toHaveBeenCalledWith('Authorization check failed:', expect.any(Error));
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it('should deny global subscription when authorize returns false', async () => {
|
|
331
|
-
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
332
|
-
transport = new SocketIOEventTransport({ io, logger });
|
|
333
|
-
|
|
334
|
-
const socket = createMockSocket();
|
|
335
|
-
const authorize = vi.fn().mockResolvedValue(false);
|
|
336
|
-
transport.setupClientHandlers(socket, authorize);
|
|
337
|
-
|
|
338
|
-
const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
|
|
339
|
-
await subscribeAllHandler();
|
|
340
|
-
|
|
341
|
-
expect(authorize).toHaveBeenCalledWith('*', socket);
|
|
342
|
-
expect(socket._joinedRooms.has('workflow:all')).toBe(false);
|
|
343
|
-
expect(logger.warn).toHaveBeenCalledWith('Global subscription denied');
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('should deny global subscription when authorize throws', async () => {
|
|
347
|
-
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
348
|
-
transport = new SocketIOEventTransport({ io, logger });
|
|
349
|
-
|
|
350
|
-
const socket = createMockSocket();
|
|
351
|
-
const authorize = vi.fn().mockRejectedValue(new Error('auth down'));
|
|
352
|
-
transport.setupClientHandlers(socket, authorize);
|
|
353
|
-
|
|
354
|
-
const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
|
|
355
|
-
await subscribeAllHandler();
|
|
356
|
-
|
|
357
|
-
expect(socket._joinedRooms.has('workflow:all')).toBe(false);
|
|
358
|
-
expect(logger.error).toHaveBeenCalledWith('Authorization check failed:', expect.any(Error));
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
describe('close', () => {
|
|
363
|
-
it('should clear all subscribers', () => {
|
|
364
|
-
const runCallback = vi.fn();
|
|
365
|
-
const globalCallback = vi.fn();
|
|
366
|
-
transport.subscribe('run-123', runCallback);
|
|
367
|
-
transport.subscribeAll(globalCallback);
|
|
368
|
-
|
|
369
|
-
transport.close();
|
|
370
|
-
|
|
371
|
-
const event = createTestEvent({ runId: 'run-123' });
|
|
372
|
-
transport.emit(event);
|
|
373
|
-
|
|
374
|
-
expect(runCallback).not.toHaveBeenCalled();
|
|
375
|
-
expect(globalCallback).not.toHaveBeenCalled();
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
});
|
package/src/events/socketio.ts
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Socket.IO Event Transport
|
|
3
|
-
*
|
|
4
|
-
* Broadcasts workflow events to connected clients via Socket.IO.
|
|
5
|
-
* Clients can subscribe to specific run IDs or receive all events.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { EventTransport, EventCallback, Unsubscribe, WorkflowEvent } from './types';
|
|
9
|
-
import type { Logger } from '../core/types';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Minimal Socket.IO Server interface.
|
|
13
|
-
* This allows using the adapter without a direct dependency on socket.io.
|
|
14
|
-
*/
|
|
15
|
-
export interface SocketIOServer {
|
|
16
|
-
to(room: string): {
|
|
17
|
-
emit(event: string, ...args: unknown[]): void;
|
|
18
|
-
};
|
|
19
|
-
emit(event: string, ...args: unknown[]): void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Minimal Socket.IO Socket interface.
|
|
24
|
-
*/
|
|
25
|
-
export interface SocketIOSocket {
|
|
26
|
-
join(room: string): void;
|
|
27
|
-
leave(room: string): void;
|
|
28
|
-
on(event: string, callback: (...args: unknown[]) => void): void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Authorization callback for Socket.IO run subscriptions.
|
|
33
|
-
* Return true to allow the subscription, false to deny it.
|
|
34
|
-
*/
|
|
35
|
-
export type SocketIOAuthorizeFn = (
|
|
36
|
-
runId: string,
|
|
37
|
-
socket: SocketIOSocket
|
|
38
|
-
) => boolean | Promise<boolean>;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Configuration for SocketIOEventTransport.
|
|
42
|
-
*/
|
|
43
|
-
export interface SocketIOEventTransportConfig {
|
|
44
|
-
/** Socket.IO server instance */
|
|
45
|
-
io: SocketIOServer;
|
|
46
|
-
|
|
47
|
-
/** Event name to use for workflow events (default: 'workflow:event') */
|
|
48
|
-
eventName?: string;
|
|
49
|
-
|
|
50
|
-
/** Room prefix for run-specific subscriptions (default: 'run:') */
|
|
51
|
-
roomPrefix?: string;
|
|
52
|
-
|
|
53
|
-
/** Whether to also broadcast to a global room (default: true) */
|
|
54
|
-
broadcastGlobal?: boolean;
|
|
55
|
-
|
|
56
|
-
/** Global room name (default: 'workflow:all') */
|
|
57
|
-
globalRoom?: string;
|
|
58
|
-
|
|
59
|
-
/** Logger for transport errors (default: console-based) */
|
|
60
|
-
logger?: Logger;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Socket.IO-based event transport for real-time workflow events.
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* ```typescript
|
|
68
|
-
* import { Server } from 'socket.io';
|
|
69
|
-
* import { SocketIOEventTransport } from 'stepflow';
|
|
70
|
-
*
|
|
71
|
-
* const io = new Server(httpServer);
|
|
72
|
-
* const eventTransport = new SocketIOEventTransport({ io });
|
|
73
|
-
*
|
|
74
|
-
* const engine = new WorkflowEngine({
|
|
75
|
-
* storage,
|
|
76
|
-
* events: eventTransport,
|
|
77
|
-
* });
|
|
78
|
-
*
|
|
79
|
-
* // Client-side:
|
|
80
|
-
* // socket.emit('workflow:subscribe', runId);
|
|
81
|
-
* // socket.on('workflow:event', (event) => console.log(event));
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
export class SocketIOEventTransport implements EventTransport {
|
|
85
|
-
private io: SocketIOServer;
|
|
86
|
-
private eventName: string;
|
|
87
|
-
private roomPrefix: string;
|
|
88
|
-
private broadcastGlobal: boolean;
|
|
89
|
-
private globalRoom: string;
|
|
90
|
-
private logger: Logger;
|
|
91
|
-
|
|
92
|
-
// Local subscribers for server-side subscriptions
|
|
93
|
-
private runSubscribers = new Map<string, Set<EventCallback>>();
|
|
94
|
-
private globalSubscribers = new Set<EventCallback>();
|
|
95
|
-
|
|
96
|
-
constructor(config: SocketIOEventTransportConfig) {
|
|
97
|
-
this.io = config.io;
|
|
98
|
-
this.eventName = config.eventName ?? 'workflow:event';
|
|
99
|
-
this.roomPrefix = config.roomPrefix ?? 'run:';
|
|
100
|
-
this.broadcastGlobal = config.broadcastGlobal ?? true;
|
|
101
|
-
this.globalRoom = config.globalRoom ?? 'workflow:all';
|
|
102
|
-
this.logger = config.logger ?? {
|
|
103
|
-
debug() {},
|
|
104
|
-
info() {},
|
|
105
|
-
warn() {},
|
|
106
|
-
error(message: string, ...args: unknown[]) { console.error(message, ...args); },
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Emit an event to Socket.IO clients and local subscribers.
|
|
112
|
-
*/
|
|
113
|
-
emit(event: WorkflowEvent): void {
|
|
114
|
-
// Serialize the event (convert Date to ISO string for transport)
|
|
115
|
-
const serializedEvent = {
|
|
116
|
-
...event,
|
|
117
|
-
timestamp: event.timestamp.toISOString(),
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// Emit to run-specific room
|
|
121
|
-
const room = `${this.roomPrefix}${event.runId}`;
|
|
122
|
-
this.io.to(room).emit(this.eventName, serializedEvent);
|
|
123
|
-
|
|
124
|
-
// Emit to global room
|
|
125
|
-
if (this.broadcastGlobal) {
|
|
126
|
-
this.io.to(this.globalRoom).emit(this.eventName, serializedEvent);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Notify local subscribers
|
|
130
|
-
const runSubs = this.runSubscribers.get(event.runId);
|
|
131
|
-
if (runSubs) {
|
|
132
|
-
for (const callback of runSubs) {
|
|
133
|
-
try {
|
|
134
|
-
callback(event);
|
|
135
|
-
} catch (error) {
|
|
136
|
-
this.logger.error('Event callback error:', error);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
for (const callback of this.globalSubscribers) {
|
|
142
|
-
try {
|
|
143
|
-
callback(event);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
this.logger.error('Event callback error:', error);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Subscribe to events for a specific run (server-side).
|
|
152
|
-
*/
|
|
153
|
-
subscribe(runId: string, callback: EventCallback): Unsubscribe {
|
|
154
|
-
let subs = this.runSubscribers.get(runId);
|
|
155
|
-
if (!subs) {
|
|
156
|
-
subs = new Set();
|
|
157
|
-
this.runSubscribers.set(runId, subs);
|
|
158
|
-
}
|
|
159
|
-
subs.add(callback);
|
|
160
|
-
|
|
161
|
-
return () => {
|
|
162
|
-
subs!.delete(callback);
|
|
163
|
-
if (subs!.size === 0) {
|
|
164
|
-
this.runSubscribers.delete(runId);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Subscribe to all events (server-side).
|
|
171
|
-
*/
|
|
172
|
-
subscribeAll(callback: EventCallback): Unsubscribe {
|
|
173
|
-
this.globalSubscribers.add(callback);
|
|
174
|
-
return () => {
|
|
175
|
-
this.globalSubscribers.delete(callback);
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Set up client subscription handlers on a socket.
|
|
181
|
-
* Call this when a client connects to enable subscription commands.
|
|
182
|
-
*
|
|
183
|
-
* An `authorize` callback is required to prevent unauthorized access to run events.
|
|
184
|
-
* The callback receives the run ID (or `'*'` for global subscriptions) and the socket,
|
|
185
|
-
* and must return `true` to allow or `false` to deny the subscription.
|
|
186
|
-
*
|
|
187
|
-
* @param socket - The Socket.IO socket to set up handlers on
|
|
188
|
-
* @param authorize - Callback to check if a socket can access a run.
|
|
189
|
-
*
|
|
190
|
-
* @example
|
|
191
|
-
* ```typescript
|
|
192
|
-
* io.on('connection', (socket) => {
|
|
193
|
-
* eventTransport.setupClientHandlers(socket, async (runId, sock) => {
|
|
194
|
-
* // Check if the authenticated user owns this run
|
|
195
|
-
* const userId = sock.data?.userId;
|
|
196
|
-
* return userId ? await canUserAccessRun(userId, runId) : false;
|
|
197
|
-
* });
|
|
198
|
-
* });
|
|
199
|
-
* ```
|
|
200
|
-
*/
|
|
201
|
-
setupClientHandlers(
|
|
202
|
-
socket: SocketIOSocket,
|
|
203
|
-
authorize: SocketIOAuthorizeFn
|
|
204
|
-
): void {
|
|
205
|
-
// Subscribe to a specific run
|
|
206
|
-
socket.on('workflow:subscribe', async (...args: unknown[]) => {
|
|
207
|
-
const runId = args[0];
|
|
208
|
-
if (typeof runId !== 'string') return;
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
const allowed = await authorize(runId, socket);
|
|
212
|
-
if (!allowed) {
|
|
213
|
-
this.logger.warn(`Subscription denied for run ${runId}`);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
} catch (error) {
|
|
217
|
-
this.logger.error('Authorization check failed:', error);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
socket.join(`${this.roomPrefix}${runId}`);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// Unsubscribe from a specific run
|
|
225
|
-
socket.on('workflow:unsubscribe', (...args: unknown[]) => {
|
|
226
|
-
const runId = args[0];
|
|
227
|
-
if (typeof runId === 'string') {
|
|
228
|
-
socket.leave(`${this.roomPrefix}${runId}`);
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// Subscribe to all events (requires authorization to pass with '*' as runId)
|
|
233
|
-
socket.on('workflow:subscribe:all', async () => {
|
|
234
|
-
try {
|
|
235
|
-
const allowed = await authorize('*', socket);
|
|
236
|
-
if (!allowed) {
|
|
237
|
-
this.logger.warn('Global subscription denied');
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
} catch (error) {
|
|
241
|
-
this.logger.error('Authorization check failed:', error);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
socket.join(this.globalRoom);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Unsubscribe from all events
|
|
249
|
-
socket.on('workflow:unsubscribe:all', () => {
|
|
250
|
-
socket.leave(this.globalRoom);
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Clear all in-process server-side subscribers.
|
|
256
|
-
* The underlying Socket.IO server socket is not closed here — manage its lifecycle externally.
|
|
257
|
-
*/
|
|
258
|
-
close(): void {
|
|
259
|
-
this.runSubscribers.clear();
|
|
260
|
-
this.globalSubscribers.clear();
|
|
261
|
-
}
|
|
262
|
-
}
|