@multiplier-labs/stepflow 0.3.1 → 0.3.4
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/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
- package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
- package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +17 -5
- package/.github/workflows/security.yml +29 -0
- package/CHANGELOG.md +34 -0
- package/README.md +24 -0
- package/SECURITY.md +40 -0
- package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
- package/dist/chunk-5XJBAPXF.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
- package/dist/chunk-U3PCVK6O.js.map +1 -0
- package/dist/events/index.d.ts +60 -7
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
- package/dist/index.d.ts +15 -23
- package/dist/index.js +784 -547
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +4 -4
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +322 -56
- package/dist/storage/index.js +4 -2
- package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
- package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
- package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
- package/docs/stepflow-api-reference.md +5 -4
- package/package.json +12 -8
- package/src/core/engine/concurrency.ts +229 -0
- package/src/core/engine/lifecycle.ts +176 -0
- package/src/core/engine.test.ts +536 -522
- package/src/core/engine.ts +116 -235
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator/persistence.ts +165 -0
- package/src/core/orchestrator/plan-resolution.ts +140 -0
- package/src/core/orchestrator/state-transitions.ts +109 -0
- package/src/core/orchestrator/step-runner.ts +427 -0
- package/src/core/orchestrator.ts +57 -395
- package/src/core/types.ts +7 -29
- package/src/events/index.ts +4 -4
- package/src/events/memory.test.ts +39 -41
- package/src/events/memory.ts +7 -10
- package/src/events/socketio.test.ts +112 -162
- package/src/events/socketio.ts +20 -27
- package/src/events/types.ts +16 -19
- package/src/events/webhook.test.ts +489 -235
- package/src/events/webhook.ts +355 -139
- package/src/index.ts +19 -19
- package/src/planning/evaluate-condition.test.ts +153 -0
- package/src/planning/evaluate-condition.ts +105 -0
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +45 -152
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +20 -106
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +149 -150
- package/src/scheduler/cron.ts +58 -69
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +89 -105
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +40 -22
- package/src/scheduler/types.ts +4 -9
- package/src/storage/index.ts +5 -5
- package/src/storage/memory.test.ts +133 -183
- package/src/storage/memory.ts +21 -42
- package/src/storage/postgres-core.ts +550 -0
- package/src/storage/postgres-migration.test.ts +69 -0
- package/src/storage/postgres-transaction-pagination.test.ts +107 -0
- package/src/storage/postgres.ts +222 -937
- package/src/storage/sqlite-core.ts +151 -0
- package/src/storage/sqlite.test.ts +330 -281
- package/src/storage/sqlite.ts +225 -170
- package/src/storage/types.ts +33 -58
- package/src/utils/errors.test.ts +78 -80
- package/src/utils/errors.ts +27 -57
- package/src/utils/id.ts +3 -3
- package/src/utils/index.ts +6 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/pinned-fetch.test.ts +84 -0
- package/src/utils/pinned-fetch.ts +118 -0
- package/src/utils/postgres-deps.ts +20 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- package/src/utils/safe-json.test.ts +166 -0
- package/src/utils/safe-json.ts +107 -0
- package/src/utils/webhook-signature.test.ts +111 -0
- package/src/utils/webhook-signature.ts +74 -0
- package/dist/chunk-FQCT3I2A.js.map +0 -1
- package/dist/chunk-GXY66V7Q.js.map +0 -1
- package/dist/chunk-U5QGN3RT.js.map +0 -1
- package/dist/chunk-XCF4PLN7.js.map +0 -1
- package/src/scheduler/postgres-persistence.test.ts +0 -906
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
SocketIOServer,
|
|
5
|
-
SocketIOSocket,
|
|
6
|
-
} from "./socketio";
|
|
7
|
-
import type { WorkflowEvent } from "./types";
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { SocketIOEventTransport, SocketIOServer, SocketIOSocket } from './socketio';
|
|
3
|
+
import type { WorkflowEvent } from './types';
|
|
8
4
|
|
|
9
5
|
// Mock Socket.IO server
|
|
10
6
|
function createMockServer(): SocketIOServer & {
|
|
@@ -52,19 +48,17 @@ function createMockSocket(): SocketIOSocket & {
|
|
|
52
48
|
};
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
function createTestEvent(
|
|
56
|
-
overrides: Partial<WorkflowEvent> = {},
|
|
57
|
-
): WorkflowEvent {
|
|
51
|
+
function createTestEvent(overrides: Partial<WorkflowEvent> = {}): WorkflowEvent {
|
|
58
52
|
return {
|
|
59
|
-
eventType:
|
|
60
|
-
runId:
|
|
61
|
-
kind:
|
|
62
|
-
timestamp: new Date(
|
|
53
|
+
eventType: 'run.started',
|
|
54
|
+
runId: 'run-123',
|
|
55
|
+
kind: 'test.workflow',
|
|
56
|
+
timestamp: new Date('2024-01-01T12:00:00Z'),
|
|
63
57
|
...overrides,
|
|
64
58
|
};
|
|
65
59
|
}
|
|
66
60
|
|
|
67
|
-
describe(
|
|
61
|
+
describe('SocketIOEventTransport', () => {
|
|
68
62
|
let io: ReturnType<typeof createMockServer>;
|
|
69
63
|
let transport: SocketIOEventTransport;
|
|
70
64
|
|
|
@@ -73,130 +67,124 @@ describe("SocketIOEventTransport", () => {
|
|
|
73
67
|
transport = new SocketIOEventTransport({ io });
|
|
74
68
|
});
|
|
75
69
|
|
|
76
|
-
describe(
|
|
77
|
-
it(
|
|
78
|
-
const event = createTestEvent({ runId:
|
|
70
|
+
describe('emit', () => {
|
|
71
|
+
it('should emit event to run-specific room', () => {
|
|
72
|
+
const event = createTestEvent({ runId: 'run-456' });
|
|
79
73
|
transport.emit(event);
|
|
80
74
|
|
|
81
|
-
const roomEmit = io._rooms.get(
|
|
75
|
+
const roomEmit = io._rooms.get('run:run-456');
|
|
82
76
|
expect(roomEmit).toBeDefined();
|
|
83
|
-
expect(roomEmit!.emit).toHaveBeenCalledWith(
|
|
77
|
+
expect(roomEmit!.emit).toHaveBeenCalledWith('workflow:event', {
|
|
84
78
|
...event,
|
|
85
|
-
timestamp:
|
|
79
|
+
timestamp: '2024-01-01T12:00:00.000Z',
|
|
86
80
|
});
|
|
87
81
|
});
|
|
88
82
|
|
|
89
|
-
it(
|
|
83
|
+
it('should emit event to global room by default', () => {
|
|
90
84
|
const event = createTestEvent();
|
|
91
85
|
transport.emit(event);
|
|
92
86
|
|
|
93
|
-
const globalRoomEmit = io._rooms.get(
|
|
87
|
+
const globalRoomEmit = io._rooms.get('workflow:all');
|
|
94
88
|
expect(globalRoomEmit).toBeDefined();
|
|
95
89
|
expect(globalRoomEmit!.emit).toHaveBeenCalled();
|
|
96
90
|
});
|
|
97
91
|
|
|
98
|
-
it(
|
|
92
|
+
it('should not emit to global room when broadcastGlobal is false', () => {
|
|
99
93
|
transport = new SocketIOEventTransport({ io, broadcastGlobal: false });
|
|
100
94
|
const event = createTestEvent();
|
|
101
95
|
transport.emit(event);
|
|
102
96
|
|
|
103
97
|
// Should only have run-specific room, not global
|
|
104
98
|
expect(io._rooms.size).toBe(1);
|
|
105
|
-
expect(io._rooms.has(
|
|
106
|
-
expect(io._rooms.has(
|
|
99
|
+
expect(io._rooms.has('run:run-123')).toBe(true);
|
|
100
|
+
expect(io._rooms.has('workflow:all')).toBe(false);
|
|
107
101
|
});
|
|
108
102
|
|
|
109
|
-
it(
|
|
110
|
-
transport = new SocketIOEventTransport({ io, eventName:
|
|
103
|
+
it('should use custom event name', () => {
|
|
104
|
+
transport = new SocketIOEventTransport({ io, eventName: 'custom:event' });
|
|
111
105
|
const event = createTestEvent();
|
|
112
106
|
transport.emit(event);
|
|
113
107
|
|
|
114
|
-
const roomEmit = io._rooms.get(
|
|
115
|
-
expect(roomEmit!.emit).toHaveBeenCalledWith(
|
|
116
|
-
"custom:event",
|
|
117
|
-
expect.any(Object),
|
|
118
|
-
);
|
|
108
|
+
const roomEmit = io._rooms.get('run:run-123');
|
|
109
|
+
expect(roomEmit!.emit).toHaveBeenCalledWith('custom:event', expect.any(Object));
|
|
119
110
|
});
|
|
120
111
|
|
|
121
|
-
it(
|
|
122
|
-
transport = new SocketIOEventTransport({ io, roomPrefix:
|
|
123
|
-
const event = createTestEvent({ runId:
|
|
112
|
+
it('should use custom room prefix', () => {
|
|
113
|
+
transport = new SocketIOEventTransport({ io, roomPrefix: 'workflow:' });
|
|
114
|
+
const event = createTestEvent({ runId: 'xyz' });
|
|
124
115
|
transport.emit(event);
|
|
125
116
|
|
|
126
|
-
expect(io._rooms.has(
|
|
117
|
+
expect(io._rooms.has('workflow:xyz')).toBe(true);
|
|
127
118
|
});
|
|
128
119
|
|
|
129
|
-
it(
|
|
120
|
+
it('should serialize timestamp to ISO string', () => {
|
|
130
121
|
const event = createTestEvent({
|
|
131
|
-
timestamp: new Date(
|
|
122
|
+
timestamp: new Date('2024-06-15T10:30:00Z'),
|
|
132
123
|
});
|
|
133
124
|
transport.emit(event);
|
|
134
125
|
|
|
135
|
-
const roomEmit = io._rooms.get(
|
|
136
|
-
const emittedEvent = roomEmit!.emit.mock.calls[0][1] as Record<
|
|
137
|
-
|
|
138
|
-
unknown
|
|
139
|
-
>;
|
|
140
|
-
expect(emittedEvent.timestamp).toBe("2024-06-15T10:30:00.000Z");
|
|
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');
|
|
141
129
|
});
|
|
142
130
|
});
|
|
143
131
|
|
|
144
|
-
describe(
|
|
145
|
-
it(
|
|
132
|
+
describe('subscribe', () => {
|
|
133
|
+
it('should call callback when event matches runId', () => {
|
|
146
134
|
const callback = vi.fn();
|
|
147
|
-
transport.subscribe(
|
|
135
|
+
transport.subscribe('run-123', callback);
|
|
148
136
|
|
|
149
|
-
const event = createTestEvent({ runId:
|
|
137
|
+
const event = createTestEvent({ runId: 'run-123' });
|
|
150
138
|
transport.emit(event);
|
|
151
139
|
|
|
152
140
|
expect(callback).toHaveBeenCalledWith(event);
|
|
153
141
|
});
|
|
154
142
|
|
|
155
|
-
it(
|
|
143
|
+
it('should not call callback for different runId', () => {
|
|
156
144
|
const callback = vi.fn();
|
|
157
|
-
transport.subscribe(
|
|
145
|
+
transport.subscribe('run-123', callback);
|
|
158
146
|
|
|
159
|
-
const event = createTestEvent({ runId:
|
|
147
|
+
const event = createTestEvent({ runId: 'run-456' });
|
|
160
148
|
transport.emit(event);
|
|
161
149
|
|
|
162
150
|
expect(callback).not.toHaveBeenCalled();
|
|
163
151
|
});
|
|
164
152
|
|
|
165
|
-
it(
|
|
153
|
+
it('should allow multiple subscribers for same runId', () => {
|
|
166
154
|
const callback1 = vi.fn();
|
|
167
155
|
const callback2 = vi.fn();
|
|
168
|
-
transport.subscribe(
|
|
169
|
-
transport.subscribe(
|
|
156
|
+
transport.subscribe('run-123', callback1);
|
|
157
|
+
transport.subscribe('run-123', callback2);
|
|
170
158
|
|
|
171
|
-
const event = createTestEvent({ runId:
|
|
159
|
+
const event = createTestEvent({ runId: 'run-123' });
|
|
172
160
|
transport.emit(event);
|
|
173
161
|
|
|
174
162
|
expect(callback1).toHaveBeenCalledWith(event);
|
|
175
163
|
expect(callback2).toHaveBeenCalledWith(event);
|
|
176
164
|
});
|
|
177
165
|
|
|
178
|
-
it(
|
|
166
|
+
it('should unsubscribe when calling returned function', () => {
|
|
179
167
|
const callback = vi.fn();
|
|
180
|
-
const unsubscribe = transport.subscribe(
|
|
168
|
+
const unsubscribe = transport.subscribe('run-123', callback);
|
|
181
169
|
|
|
182
170
|
unsubscribe();
|
|
183
171
|
|
|
184
|
-
const event = createTestEvent({ runId:
|
|
172
|
+
const event = createTestEvent({ runId: 'run-123' });
|
|
185
173
|
transport.emit(event);
|
|
186
174
|
|
|
187
175
|
expect(callback).not.toHaveBeenCalled();
|
|
188
176
|
});
|
|
189
177
|
|
|
190
|
-
it(
|
|
178
|
+
it('should handle callback errors gracefully', () => {
|
|
191
179
|
const errorCallback = vi.fn().mockImplementation(() => {
|
|
192
|
-
throw new Error(
|
|
180
|
+
throw new Error('Callback error');
|
|
193
181
|
});
|
|
194
182
|
const normalCallback = vi.fn();
|
|
195
183
|
|
|
196
|
-
transport.subscribe(
|
|
197
|
-
transport.subscribe(
|
|
184
|
+
transport.subscribe('run-123', errorCallback);
|
|
185
|
+
transport.subscribe('run-123', normalCallback);
|
|
198
186
|
|
|
199
|
-
const event = createTestEvent({ runId:
|
|
187
|
+
const event = createTestEvent({ runId: 'run-123' });
|
|
200
188
|
transport.emit(event);
|
|
201
189
|
|
|
202
190
|
// Both callbacks should have been called despite error
|
|
@@ -205,13 +193,13 @@ describe("SocketIOEventTransport", () => {
|
|
|
205
193
|
});
|
|
206
194
|
});
|
|
207
195
|
|
|
208
|
-
describe(
|
|
209
|
-
it(
|
|
196
|
+
describe('subscribeAll', () => {
|
|
197
|
+
it('should call callback for all events', () => {
|
|
210
198
|
const callback = vi.fn();
|
|
211
199
|
transport.subscribeAll(callback);
|
|
212
200
|
|
|
213
|
-
const event1 = createTestEvent({ runId:
|
|
214
|
-
const event2 = createTestEvent({ runId:
|
|
201
|
+
const event1 = createTestEvent({ runId: 'run-1' });
|
|
202
|
+
const event2 = createTestEvent({ runId: 'run-2' });
|
|
215
203
|
transport.emit(event1);
|
|
216
204
|
transport.emit(event2);
|
|
217
205
|
|
|
@@ -220,7 +208,7 @@ describe("SocketIOEventTransport", () => {
|
|
|
220
208
|
expect(callback).toHaveBeenCalledWith(event2);
|
|
221
209
|
});
|
|
222
210
|
|
|
223
|
-
it(
|
|
211
|
+
it('should unsubscribe when calling returned function', () => {
|
|
224
212
|
const callback = vi.fn();
|
|
225
213
|
const unsubscribe = transport.subscribeAll(callback);
|
|
226
214
|
|
|
@@ -233,80 +221,74 @@ describe("SocketIOEventTransport", () => {
|
|
|
233
221
|
});
|
|
234
222
|
});
|
|
235
223
|
|
|
236
|
-
describe(
|
|
224
|
+
describe('setupClientHandlers', () => {
|
|
237
225
|
const allowAll = vi.fn().mockResolvedValue(true);
|
|
238
226
|
|
|
239
|
-
it(
|
|
227
|
+
it('should register subscription handlers on socket', () => {
|
|
240
228
|
const socket = createMockSocket();
|
|
241
229
|
transport.setupClientHandlers(socket, allowAll);
|
|
242
230
|
|
|
243
|
-
expect(socket._handlers.has(
|
|
244
|
-
expect(socket._handlers.has(
|
|
245
|
-
expect(socket._handlers.has(
|
|
246
|
-
expect(socket._handlers.has(
|
|
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);
|
|
247
235
|
});
|
|
248
236
|
|
|
249
|
-
it(
|
|
237
|
+
it('should join run-specific room on subscribe when authorized', async () => {
|
|
250
238
|
const socket = createMockSocket();
|
|
251
239
|
transport.setupClientHandlers(socket, allowAll);
|
|
252
240
|
|
|
253
|
-
const subscribeHandler = socket._handlers.get(
|
|
254
|
-
await subscribeHandler(
|
|
241
|
+
const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
|
|
242
|
+
await subscribeHandler('run-123');
|
|
255
243
|
|
|
256
|
-
expect(socket._joinedRooms.has(
|
|
244
|
+
expect(socket._joinedRooms.has('run:run-123')).toBe(true);
|
|
257
245
|
});
|
|
258
246
|
|
|
259
|
-
it(
|
|
247
|
+
it('should leave run-specific room on unsubscribe', async () => {
|
|
260
248
|
const socket = createMockSocket();
|
|
261
249
|
transport.setupClientHandlers(socket, allowAll);
|
|
262
250
|
|
|
263
251
|
// First subscribe
|
|
264
|
-
const subscribeHandler = socket._handlers.get(
|
|
265
|
-
await subscribeHandler(
|
|
252
|
+
const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
|
|
253
|
+
await subscribeHandler('run-123');
|
|
266
254
|
|
|
267
255
|
// Then unsubscribe
|
|
268
|
-
const unsubscribeHandler = socket._handlers.get(
|
|
269
|
-
unsubscribeHandler(
|
|
256
|
+
const unsubscribeHandler = socket._handlers.get('workflow:unsubscribe')!;
|
|
257
|
+
unsubscribeHandler('run-123');
|
|
270
258
|
|
|
271
|
-
expect(socket._joinedRooms.has(
|
|
259
|
+
expect(socket._joinedRooms.has('run:run-123')).toBe(false);
|
|
272
260
|
});
|
|
273
261
|
|
|
274
|
-
it(
|
|
262
|
+
it('should join global room on subscribe:all when authorized', async () => {
|
|
275
263
|
const socket = createMockSocket();
|
|
276
264
|
transport.setupClientHandlers(socket, allowAll);
|
|
277
265
|
|
|
278
|
-
const subscribeAllHandler = socket._handlers.get(
|
|
279
|
-
"workflow:subscribe:all",
|
|
280
|
-
)!;
|
|
266
|
+
const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
|
|
281
267
|
await subscribeAllHandler();
|
|
282
268
|
|
|
283
|
-
expect(socket._joinedRooms.has(
|
|
269
|
+
expect(socket._joinedRooms.has('workflow:all')).toBe(true);
|
|
284
270
|
});
|
|
285
271
|
|
|
286
|
-
it(
|
|
272
|
+
it('should leave global room on unsubscribe:all', async () => {
|
|
287
273
|
const socket = createMockSocket();
|
|
288
274
|
transport.setupClientHandlers(socket, allowAll);
|
|
289
275
|
|
|
290
276
|
// First subscribe
|
|
291
|
-
const subscribeAllHandler = socket._handlers.get(
|
|
292
|
-
"workflow:subscribe:all",
|
|
293
|
-
)!;
|
|
277
|
+
const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
|
|
294
278
|
await subscribeAllHandler();
|
|
295
279
|
|
|
296
280
|
// Then unsubscribe
|
|
297
|
-
const unsubscribeAllHandler = socket._handlers.get(
|
|
298
|
-
"workflow:unsubscribe:all",
|
|
299
|
-
)!;
|
|
281
|
+
const unsubscribeAllHandler = socket._handlers.get('workflow:unsubscribe:all')!;
|
|
300
282
|
unsubscribeAllHandler();
|
|
301
283
|
|
|
302
|
-
expect(socket._joinedRooms.has(
|
|
284
|
+
expect(socket._joinedRooms.has('workflow:all')).toBe(false);
|
|
303
285
|
});
|
|
304
286
|
|
|
305
|
-
it(
|
|
287
|
+
it('should ignore non-string runId on subscribe', async () => {
|
|
306
288
|
const socket = createMockSocket();
|
|
307
289
|
transport.setupClientHandlers(socket, allowAll);
|
|
308
290
|
|
|
309
|
-
const subscribeHandler = socket._handlers.get(
|
|
291
|
+
const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
|
|
310
292
|
await subscribeHandler(123); // Not a string
|
|
311
293
|
await subscribeHandler(null);
|
|
312
294
|
await subscribeHandler(undefined);
|
|
@@ -314,111 +296,79 @@ describe("SocketIOEventTransport", () => {
|
|
|
314
296
|
expect(socket._joinedRooms.size).toBe(0);
|
|
315
297
|
});
|
|
316
298
|
|
|
317
|
-
it(
|
|
318
|
-
const logger = {
|
|
319
|
-
debug: vi.fn(),
|
|
320
|
-
info: vi.fn(),
|
|
321
|
-
warn: vi.fn(),
|
|
322
|
-
error: vi.fn(),
|
|
323
|
-
};
|
|
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() };
|
|
324
301
|
transport = new SocketIOEventTransport({ io, logger });
|
|
325
302
|
|
|
326
303
|
const socket = createMockSocket();
|
|
327
304
|
const authorize = vi.fn().mockResolvedValue(false);
|
|
328
305
|
transport.setupClientHandlers(socket, authorize);
|
|
329
306
|
|
|
330
|
-
const subscribeHandler = socket._handlers.get(
|
|
331
|
-
await subscribeHandler(
|
|
307
|
+
const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
|
|
308
|
+
await subscribeHandler('run-secret');
|
|
332
309
|
|
|
333
|
-
expect(authorize).toHaveBeenCalledWith(
|
|
334
|
-
expect(socket._joinedRooms.has(
|
|
335
|
-
expect(logger.warn).toHaveBeenCalledWith(
|
|
336
|
-
expect.stringContaining("run-secret"),
|
|
337
|
-
);
|
|
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'));
|
|
338
313
|
});
|
|
339
314
|
|
|
340
|
-
it(
|
|
341
|
-
const logger = {
|
|
342
|
-
debug: vi.fn(),
|
|
343
|
-
info: vi.fn(),
|
|
344
|
-
warn: vi.fn(),
|
|
345
|
-
error: vi.fn(),
|
|
346
|
-
};
|
|
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() };
|
|
347
317
|
transport = new SocketIOEventTransport({ io, logger });
|
|
348
318
|
|
|
349
319
|
const socket = createMockSocket();
|
|
350
|
-
const authorize = vi.fn().mockRejectedValue(new Error(
|
|
320
|
+
const authorize = vi.fn().mockRejectedValue(new Error('auth error'));
|
|
351
321
|
transport.setupClientHandlers(socket, authorize);
|
|
352
322
|
|
|
353
|
-
const subscribeHandler = socket._handlers.get(
|
|
354
|
-
await subscribeHandler(
|
|
323
|
+
const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
|
|
324
|
+
await subscribeHandler('run-123');
|
|
355
325
|
|
|
356
|
-
expect(socket._joinedRooms.has(
|
|
357
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
358
|
-
"Authorization check failed:",
|
|
359
|
-
expect.any(Error),
|
|
360
|
-
);
|
|
326
|
+
expect(socket._joinedRooms.has('run:run-123')).toBe(false);
|
|
327
|
+
expect(logger.error).toHaveBeenCalledWith('Authorization check failed:', expect.any(Error));
|
|
361
328
|
});
|
|
362
329
|
|
|
363
|
-
it(
|
|
364
|
-
const logger = {
|
|
365
|
-
debug: vi.fn(),
|
|
366
|
-
info: vi.fn(),
|
|
367
|
-
warn: vi.fn(),
|
|
368
|
-
error: vi.fn(),
|
|
369
|
-
};
|
|
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() };
|
|
370
332
|
transport = new SocketIOEventTransport({ io, logger });
|
|
371
333
|
|
|
372
334
|
const socket = createMockSocket();
|
|
373
335
|
const authorize = vi.fn().mockResolvedValue(false);
|
|
374
336
|
transport.setupClientHandlers(socket, authorize);
|
|
375
337
|
|
|
376
|
-
const subscribeAllHandler = socket._handlers.get(
|
|
377
|
-
"workflow:subscribe:all",
|
|
378
|
-
)!;
|
|
338
|
+
const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
|
|
379
339
|
await subscribeAllHandler();
|
|
380
340
|
|
|
381
|
-
expect(authorize).toHaveBeenCalledWith(
|
|
382
|
-
expect(socket._joinedRooms.has(
|
|
383
|
-
expect(logger.warn).toHaveBeenCalledWith(
|
|
341
|
+
expect(authorize).toHaveBeenCalledWith('*', socket);
|
|
342
|
+
expect(socket._joinedRooms.has('workflow:all')).toBe(false);
|
|
343
|
+
expect(logger.warn).toHaveBeenCalledWith('Global subscription denied');
|
|
384
344
|
});
|
|
385
345
|
|
|
386
|
-
it(
|
|
387
|
-
const logger = {
|
|
388
|
-
debug: vi.fn(),
|
|
389
|
-
info: vi.fn(),
|
|
390
|
-
warn: vi.fn(),
|
|
391
|
-
error: vi.fn(),
|
|
392
|
-
};
|
|
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() };
|
|
393
348
|
transport = new SocketIOEventTransport({ io, logger });
|
|
394
349
|
|
|
395
350
|
const socket = createMockSocket();
|
|
396
|
-
const authorize = vi.fn().mockRejectedValue(new Error(
|
|
351
|
+
const authorize = vi.fn().mockRejectedValue(new Error('auth down'));
|
|
397
352
|
transport.setupClientHandlers(socket, authorize);
|
|
398
353
|
|
|
399
|
-
const subscribeAllHandler = socket._handlers.get(
|
|
400
|
-
"workflow:subscribe:all",
|
|
401
|
-
)!;
|
|
354
|
+
const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
|
|
402
355
|
await subscribeAllHandler();
|
|
403
356
|
|
|
404
|
-
expect(socket._joinedRooms.has(
|
|
405
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
406
|
-
"Authorization check failed:",
|
|
407
|
-
expect.any(Error),
|
|
408
|
-
);
|
|
357
|
+
expect(socket._joinedRooms.has('workflow:all')).toBe(false);
|
|
358
|
+
expect(logger.error).toHaveBeenCalledWith('Authorization check failed:', expect.any(Error));
|
|
409
359
|
});
|
|
410
360
|
});
|
|
411
361
|
|
|
412
|
-
describe(
|
|
413
|
-
it(
|
|
362
|
+
describe('close', () => {
|
|
363
|
+
it('should clear all subscribers', () => {
|
|
414
364
|
const runCallback = vi.fn();
|
|
415
365
|
const globalCallback = vi.fn();
|
|
416
|
-
transport.subscribe(
|
|
366
|
+
transport.subscribe('run-123', runCallback);
|
|
417
367
|
transport.subscribeAll(globalCallback);
|
|
418
368
|
|
|
419
369
|
transport.close();
|
|
420
370
|
|
|
421
|
-
const event = createTestEvent({ runId:
|
|
371
|
+
const event = createTestEvent({ runId: 'run-123' });
|
|
422
372
|
transport.emit(event);
|
|
423
373
|
|
|
424
374
|
expect(runCallback).not.toHaveBeenCalled();
|
package/src/events/socketio.ts
CHANGED
|
@@ -5,13 +5,8 @@
|
|
|
5
5
|
* Clients can subscribe to specific run IDs or receive all events.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
-
EventCallback,
|
|
11
|
-
Unsubscribe,
|
|
12
|
-
WorkflowEvent,
|
|
13
|
-
} from "./types";
|
|
14
|
-
import type { Logger } from "../core/types";
|
|
8
|
+
import type { EventTransport, EventCallback, Unsubscribe, WorkflowEvent } from './types';
|
|
9
|
+
import type { Logger } from '../core/types';
|
|
15
10
|
|
|
16
11
|
/**
|
|
17
12
|
* Minimal Socket.IO Server interface.
|
|
@@ -39,7 +34,7 @@ export interface SocketIOSocket {
|
|
|
39
34
|
*/
|
|
40
35
|
export type SocketIOAuthorizeFn = (
|
|
41
36
|
runId: string,
|
|
42
|
-
socket: SocketIOSocket
|
|
37
|
+
socket: SocketIOSocket
|
|
43
38
|
) => boolean | Promise<boolean>;
|
|
44
39
|
|
|
45
40
|
/**
|
|
@@ -100,17 +95,15 @@ export class SocketIOEventTransport implements EventTransport {
|
|
|
100
95
|
|
|
101
96
|
constructor(config: SocketIOEventTransportConfig) {
|
|
102
97
|
this.io = config.io;
|
|
103
|
-
this.eventName = config.eventName ??
|
|
104
|
-
this.roomPrefix = config.roomPrefix ??
|
|
98
|
+
this.eventName = config.eventName ?? 'workflow:event';
|
|
99
|
+
this.roomPrefix = config.roomPrefix ?? 'run:';
|
|
105
100
|
this.broadcastGlobal = config.broadcastGlobal ?? true;
|
|
106
|
-
this.globalRoom = config.globalRoom ??
|
|
101
|
+
this.globalRoom = config.globalRoom ?? 'workflow:all';
|
|
107
102
|
this.logger = config.logger ?? {
|
|
108
103
|
debug() {},
|
|
109
104
|
info() {},
|
|
110
105
|
warn() {},
|
|
111
|
-
error(message: string, ...args: unknown[]) {
|
|
112
|
-
console.error(message, ...args);
|
|
113
|
-
},
|
|
106
|
+
error(message: string, ...args: unknown[]) { console.error(message, ...args); },
|
|
114
107
|
};
|
|
115
108
|
}
|
|
116
109
|
|
|
@@ -140,7 +133,7 @@ export class SocketIOEventTransport implements EventTransport {
|
|
|
140
133
|
try {
|
|
141
134
|
callback(event);
|
|
142
135
|
} catch (error) {
|
|
143
|
-
this.logger.error(
|
|
136
|
+
this.logger.error('Event callback error:', error);
|
|
144
137
|
}
|
|
145
138
|
}
|
|
146
139
|
}
|
|
@@ -149,7 +142,7 @@ export class SocketIOEventTransport implements EventTransport {
|
|
|
149
142
|
try {
|
|
150
143
|
callback(event);
|
|
151
144
|
} catch (error) {
|
|
152
|
-
this.logger.error(
|
|
145
|
+
this.logger.error('Event callback error:', error);
|
|
153
146
|
}
|
|
154
147
|
}
|
|
155
148
|
}
|
|
@@ -207,12 +200,12 @@ export class SocketIOEventTransport implements EventTransport {
|
|
|
207
200
|
*/
|
|
208
201
|
setupClientHandlers(
|
|
209
202
|
socket: SocketIOSocket,
|
|
210
|
-
authorize: SocketIOAuthorizeFn
|
|
203
|
+
authorize: SocketIOAuthorizeFn
|
|
211
204
|
): void {
|
|
212
205
|
// Subscribe to a specific run
|
|
213
|
-
socket.on(
|
|
206
|
+
socket.on('workflow:subscribe', async (...args: unknown[]) => {
|
|
214
207
|
const runId = args[0];
|
|
215
|
-
if (typeof runId !==
|
|
208
|
+
if (typeof runId !== 'string') return;
|
|
216
209
|
|
|
217
210
|
try {
|
|
218
211
|
const allowed = await authorize(runId, socket);
|
|
@@ -221,7 +214,7 @@ export class SocketIOEventTransport implements EventTransport {
|
|
|
221
214
|
return;
|
|
222
215
|
}
|
|
223
216
|
} catch (error) {
|
|
224
|
-
this.logger.error(
|
|
217
|
+
this.logger.error('Authorization check failed:', error);
|
|
225
218
|
return;
|
|
226
219
|
}
|
|
227
220
|
|
|
@@ -229,23 +222,23 @@ export class SocketIOEventTransport implements EventTransport {
|
|
|
229
222
|
});
|
|
230
223
|
|
|
231
224
|
// Unsubscribe from a specific run
|
|
232
|
-
socket.on(
|
|
225
|
+
socket.on('workflow:unsubscribe', (...args: unknown[]) => {
|
|
233
226
|
const runId = args[0];
|
|
234
|
-
if (typeof runId ===
|
|
227
|
+
if (typeof runId === 'string') {
|
|
235
228
|
socket.leave(`${this.roomPrefix}${runId}`);
|
|
236
229
|
}
|
|
237
230
|
});
|
|
238
231
|
|
|
239
232
|
// Subscribe to all events (requires authorization to pass with '*' as runId)
|
|
240
|
-
socket.on(
|
|
233
|
+
socket.on('workflow:subscribe:all', async () => {
|
|
241
234
|
try {
|
|
242
|
-
const allowed = await authorize(
|
|
235
|
+
const allowed = await authorize('*', socket);
|
|
243
236
|
if (!allowed) {
|
|
244
|
-
this.logger.warn(
|
|
237
|
+
this.logger.warn('Global subscription denied');
|
|
245
238
|
return;
|
|
246
239
|
}
|
|
247
240
|
} catch (error) {
|
|
248
|
-
this.logger.error(
|
|
241
|
+
this.logger.error('Authorization check failed:', error);
|
|
249
242
|
return;
|
|
250
243
|
}
|
|
251
244
|
|
|
@@ -253,7 +246,7 @@ export class SocketIOEventTransport implements EventTransport {
|
|
|
253
246
|
});
|
|
254
247
|
|
|
255
248
|
// Unsubscribe from all events
|
|
256
|
-
socket.on(
|
|
249
|
+
socket.on('workflow:unsubscribe:all', () => {
|
|
257
250
|
socket.leave(this.globalRoom);
|
|
258
251
|
});
|
|
259
252
|
}
|