@auto-engineer/pipeline 1.68.0 → 1.70.0
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +63 -0
- package/dist/src/engine/workflow-processor.d.ts +3 -0
- package/dist/src/engine/workflow-processor.d.ts.map +1 -1
- package/dist/src/engine/workflow-processor.js +50 -7
- package/dist/src/engine/workflow-processor.js.map +1 -1
- package/dist/src/index.d.ts +0 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +0 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/server/phased-bridge.d.ts +13 -0
- package/dist/src/server/phased-bridge.d.ts.map +1 -0
- package/dist/src/server/phased-bridge.js +103 -0
- package/dist/src/server/phased-bridge.js.map +1 -0
- package/dist/src/server/pipeline-server.d.ts +2 -2
- package/dist/src/server/pipeline-server.d.ts.map +1 -1
- package/dist/src/server/pipeline-server.js +19 -39
- package/dist/src/server/pipeline-server.js.map +1 -1
- package/dist/src/server/v2-runtime-bridge.d.ts +21 -0
- package/dist/src/server/v2-runtime-bridge.d.ts.map +1 -0
- package/dist/src/server/v2-runtime-bridge.js +184 -0
- package/dist/src/server/v2-runtime-bridge.js.map +1 -0
- package/dist/src/store/pipeline-event-store.d.ts.map +1 -1
- package/dist/src/store/pipeline-event-store.js +0 -30
- package/dist/src/store/pipeline-event-store.js.map +1 -1
- package/dist/src/store/pipeline-read-model.d.ts +0 -15
- package/dist/src/store/pipeline-read-model.d.ts.map +1 -1
- package/dist/src/store/pipeline-read-model.js +0 -49
- package/dist/src/store/pipeline-read-model.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +10 -12
- package/package.json +3 -3
- package/src/engine/workflow-processor.specs.ts +101 -0
- package/src/engine/workflow-processor.ts +54 -8
- package/src/index.ts +0 -2
- package/src/server/phased-bridge.specs.ts +272 -0
- package/src/server/phased-bridge.ts +130 -0
- package/src/server/pipeline-server.specs.ts +94 -0
- package/src/server/pipeline-server.ts +21 -42
- package/src/server/v2-runtime-bridge.specs.ts +347 -0
- package/src/server/v2-runtime-bridge.ts +255 -0
- package/src/store/pipeline-event-store.specs.ts +0 -137
- package/src/store/pipeline-event-store.ts +0 -35
- package/src/store/pipeline-read-model.specs.ts +0 -567
- package/src/store/pipeline-read-model.ts +0 -71
- package/dist/src/projections/phased-execution-projection.d.ts +0 -77
- package/dist/src/projections/phased-execution-projection.d.ts.map +0 -1
- package/dist/src/projections/phased-execution-projection.js +0 -54
- package/dist/src/projections/phased-execution-projection.js.map +0 -1
- package/dist/src/projections/settled-instance-projection.d.ts +0 -67
- package/dist/src/projections/settled-instance-projection.d.ts.map +0 -1
- package/dist/src/projections/settled-instance-projection.js +0 -66
- package/dist/src/projections/settled-instance-projection.js.map +0 -1
- package/dist/src/runtime/phased-executor.d.ts +0 -34
- package/dist/src/runtime/phased-executor.d.ts.map +0 -1
- package/dist/src/runtime/phased-executor.js +0 -172
- package/dist/src/runtime/phased-executor.js.map +0 -1
- package/dist/src/runtime/settled-tracker.d.ts +0 -44
- package/dist/src/runtime/settled-tracker.d.ts.map +0 -1
- package/dist/src/runtime/settled-tracker.js +0 -170
- package/dist/src/runtime/settled-tracker.js.map +0 -1
- package/src/projections/phased-execution-projection.specs.ts +0 -202
- package/src/projections/phased-execution-projection.ts +0 -146
- package/src/projections/settled-instance-projection.specs.ts +0 -296
- package/src/projections/settled-instance-projection.ts +0 -160
- package/src/runtime/phased-executor.specs.ts +0 -680
- package/src/runtime/phased-executor.ts +0 -230
- package/src/runtime/settled-tracker.specs.ts +0 -1044
- package/src/runtime/settled-tracker.ts +0 -235
|
@@ -465,6 +465,44 @@ describe('PipelineServer', () => {
|
|
|
465
465
|
await server.stop();
|
|
466
466
|
});
|
|
467
467
|
|
|
468
|
+
it('should show settled node success after command dispatched via /command completes', async () => {
|
|
469
|
+
const handler = {
|
|
470
|
+
name: 'CheckTests',
|
|
471
|
+
events: ['TestsPassed'],
|
|
472
|
+
handle: async () => ({ type: 'TestsPassed', data: {} }),
|
|
473
|
+
};
|
|
474
|
+
const pipeline = define('test')
|
|
475
|
+
.on('Start')
|
|
476
|
+
.emit('CheckTests', {})
|
|
477
|
+
.settled(['CheckTests'])
|
|
478
|
+
.dispatch({ dispatches: [] }, () => {})
|
|
479
|
+
.build();
|
|
480
|
+
const server = new PipelineServer({ port: 0 });
|
|
481
|
+
server.registerCommandHandlers([handler]);
|
|
482
|
+
server.registerPipeline(pipeline);
|
|
483
|
+
await server.start();
|
|
484
|
+
|
|
485
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
486
|
+
method: 'POST',
|
|
487
|
+
headers: { 'Content-Type': 'application/json' },
|
|
488
|
+
body: JSON.stringify({ type: 'CheckTests', data: {} }),
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
492
|
+
|
|
493
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
494
|
+
const settledNode = data.nodes.find((n) => n.id === 'settled:CheckTests');
|
|
495
|
+
expect(settledNode).toEqual({
|
|
496
|
+
id: 'settled:CheckTests',
|
|
497
|
+
type: 'settled',
|
|
498
|
+
label: 'Settled',
|
|
499
|
+
status: 'success',
|
|
500
|
+
pendingCount: 0,
|
|
501
|
+
endedCount: 1,
|
|
502
|
+
});
|
|
503
|
+
await server.stop();
|
|
504
|
+
});
|
|
505
|
+
|
|
468
506
|
it('should show running status for command being executed', async () => {
|
|
469
507
|
let resolveHandler: () => void = () => {};
|
|
470
508
|
const handlerPromise = new Promise<void>((resolve) => {
|
|
@@ -2400,4 +2438,60 @@ describe('PipelineServer', () => {
|
|
|
2400
2438
|
await server.stop();
|
|
2401
2439
|
});
|
|
2402
2440
|
});
|
|
2441
|
+
|
|
2442
|
+
describe('emit-before-broadcast ordering', () => {
|
|
2443
|
+
it('should write to event store before broadcasting PipelineRunStarted via SSE', async () => {
|
|
2444
|
+
const server = new PipelineServer({ port: 0 });
|
|
2445
|
+
|
|
2446
|
+
const serverAny = server as unknown as {
|
|
2447
|
+
eventStoreContext: {
|
|
2448
|
+
eventStore: {
|
|
2449
|
+
appendToStream: (
|
|
2450
|
+
streamName: string,
|
|
2451
|
+
events: Array<{ type: string; data: unknown }>,
|
|
2452
|
+
options?: unknown,
|
|
2453
|
+
) => Promise<unknown>;
|
|
2454
|
+
};
|
|
2455
|
+
};
|
|
2456
|
+
sseManager: {
|
|
2457
|
+
broadcast: (event: unknown) => void;
|
|
2458
|
+
};
|
|
2459
|
+
};
|
|
2460
|
+
|
|
2461
|
+
await server.start();
|
|
2462
|
+
|
|
2463
|
+
const callOrder: string[] = [];
|
|
2464
|
+
|
|
2465
|
+
const originalAppend = serverAny.eventStoreContext.eventStore.appendToStream.bind(
|
|
2466
|
+
serverAny.eventStoreContext.eventStore,
|
|
2467
|
+
);
|
|
2468
|
+
serverAny.eventStoreContext.eventStore.appendToStream = async (streamName, events, options) => {
|
|
2469
|
+
const result = await originalAppend(streamName, events, options);
|
|
2470
|
+
if (events.some((e) => e.type === 'PipelineRunStarted')) {
|
|
2471
|
+
callOrder.push('emit');
|
|
2472
|
+
}
|
|
2473
|
+
return result;
|
|
2474
|
+
};
|
|
2475
|
+
|
|
2476
|
+
const originalBroadcast = serverAny.sseManager.broadcast.bind(serverAny.sseManager);
|
|
2477
|
+
serverAny.sseManager.broadcast = (event: { type?: string }) => {
|
|
2478
|
+
if (event.type === 'PipelineRunStarted') {
|
|
2479
|
+
callOrder.push('broadcast');
|
|
2480
|
+
}
|
|
2481
|
+
originalBroadcast(event);
|
|
2482
|
+
};
|
|
2483
|
+
|
|
2484
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
2485
|
+
method: 'POST',
|
|
2486
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2487
|
+
body: JSON.stringify({ type: 'RestartPipeline', data: {} }),
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2491
|
+
|
|
2492
|
+
expect(callOrder).toEqual(['emit', 'broadcast']);
|
|
2493
|
+
|
|
2494
|
+
await server.stop();
|
|
2495
|
+
});
|
|
2496
|
+
});
|
|
2403
2497
|
});
|
|
@@ -18,11 +18,11 @@ import type { FilterOptions, GraphIR, GraphNode, NodeStatus, NodeType } from '..
|
|
|
18
18
|
import type { PipelineContext } from '../runtime/context';
|
|
19
19
|
import type { EventDefinition } from '../runtime/event-command-map';
|
|
20
20
|
import { EventCommandMapper } from '../runtime/event-command-map';
|
|
21
|
-
import { PhasedExecutor } from '../runtime/phased-executor';
|
|
22
21
|
import { PipelineRuntime } from '../runtime/pipeline-runtime';
|
|
23
|
-
import { SettledTracker } from '../runtime/settled-tracker';
|
|
24
22
|
import { createPipelineEventStore, type PipelineEventStoreContext } from '../store/pipeline-event-store';
|
|
23
|
+
import { createPhasedBridge } from './phased-bridge';
|
|
25
24
|
import { SSEManager } from './sse-manager';
|
|
25
|
+
import { createV2RuntimeBridge } from './v2-runtime-bridge';
|
|
26
26
|
|
|
27
27
|
export type { EventDefinition };
|
|
28
28
|
|
|
@@ -54,9 +54,9 @@ export class PipelineServer {
|
|
|
54
54
|
private readonly runtimes: Map<string, PipelineRuntime> = new Map();
|
|
55
55
|
private actualPort: number;
|
|
56
56
|
private readonly requestedPort: number;
|
|
57
|
-
private readonly
|
|
57
|
+
private readonly settledBridge: ReturnType<typeof createV2RuntimeBridge>;
|
|
58
58
|
private readonly eventCommandMapper: EventCommandMapper;
|
|
59
|
-
private readonly
|
|
59
|
+
private readonly phasedBridge: ReturnType<typeof createPhasedBridge>;
|
|
60
60
|
private readonly sseManager: SSEManager;
|
|
61
61
|
private readonly eventStoreContext: PipelineEventStoreContext;
|
|
62
62
|
private readonly itemKeyExtractors = new Map<string, (data: unknown) => string | undefined>();
|
|
@@ -76,35 +76,18 @@ export class PipelineServer {
|
|
|
76
76
|
this.messageBus = createMessageBus();
|
|
77
77
|
this.eventStoreContext = createPipelineEventStore();
|
|
78
78
|
this.eventCommandMapper = new EventCommandMapper([]);
|
|
79
|
-
this.
|
|
80
|
-
readModel: this.eventStoreContext.readModel,
|
|
81
|
-
/* v8 ignore next 3 - integration callback tested via settled-tracker.specs.ts */
|
|
79
|
+
this.settledBridge = createV2RuntimeBridge({
|
|
82
80
|
onDispatch: (commandType, data, correlationId) => {
|
|
83
81
|
void this.dispatchFromSettled(commandType, data, correlationId);
|
|
84
82
|
},
|
|
85
|
-
/* v8 ignore next 4 - integration callback tested via settled-tracker.specs.ts */
|
|
86
|
-
onEventEmit: async (event) => {
|
|
87
|
-
const correlationId = event.data.correlationId;
|
|
88
|
-
await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [event]);
|
|
89
|
-
},
|
|
90
83
|
});
|
|
91
|
-
this.
|
|
92
|
-
readModel: this.eventStoreContext.readModel,
|
|
93
|
-
/* v8 ignore next 3 - integration callback tested via phased-executor.specs.ts */
|
|
84
|
+
this.phasedBridge = createPhasedBridge({
|
|
94
85
|
onDispatch: (commandType, data, correlationId) => {
|
|
95
86
|
void this.dispatchFromSettled(commandType, data, correlationId);
|
|
96
87
|
},
|
|
97
|
-
|
|
98
|
-
onComplete: (event, correlationId) => {
|
|
88
|
+
onPhasedComplete: (event, correlationId) => {
|
|
99
89
|
void this.handlePhasedComplete(event, correlationId);
|
|
100
90
|
},
|
|
101
|
-
/* v8 ignore next 5 - integration callback tested via phased-executor.specs.ts */
|
|
102
|
-
onEventEmit: async (event) => {
|
|
103
|
-
const data = event.data as Record<string, unknown>;
|
|
104
|
-
const correlationId =
|
|
105
|
-
(data.correlationId as string) ?? (data.executionId as string)?.split('-')[1] ?? 'default';
|
|
106
|
-
await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [event]);
|
|
107
|
-
},
|
|
108
91
|
});
|
|
109
92
|
this.sseManager = new SSEManager();
|
|
110
93
|
}
|
|
@@ -143,12 +126,9 @@ export class PipelineServer {
|
|
|
143
126
|
|
|
144
127
|
for (const handler of pipeline.descriptor.handlers) {
|
|
145
128
|
if (handler.type === 'settled') {
|
|
146
|
-
this.
|
|
147
|
-
commandTypes: handler.commandTypes,
|
|
148
|
-
handler: handler.handler,
|
|
149
|
-
});
|
|
129
|
+
this.settledBridge.registerSettled(handler);
|
|
150
130
|
} else if (handler.type === 'foreach-phased') {
|
|
151
|
-
this.
|
|
131
|
+
this.phasedBridge.registerPhased(handler);
|
|
152
132
|
}
|
|
153
133
|
}
|
|
154
134
|
}
|
|
@@ -521,13 +501,13 @@ export class PipelineServer {
|
|
|
521
501
|
};
|
|
522
502
|
}
|
|
523
503
|
|
|
524
|
-
private
|
|
504
|
+
private addStatusToSettledNode(node: GraphNode, correlationId?: string): GraphNode {
|
|
525
505
|
if (correlationId === undefined) {
|
|
526
506
|
return { ...node, status: 'idle' as NodeStatus, pendingCount: 0, endedCount: 0 };
|
|
527
507
|
}
|
|
528
508
|
const commandTypes = node.id.replace(/^settled:/, '');
|
|
529
509
|
const templateId = `template-${commandTypes}`;
|
|
530
|
-
const stats =
|
|
510
|
+
const stats = this.settledBridge.getSettledStats(correlationId, templateId);
|
|
531
511
|
return {
|
|
532
512
|
...node,
|
|
533
513
|
status: stats.status,
|
|
@@ -663,13 +643,13 @@ export class PipelineServer {
|
|
|
663
643
|
}
|
|
664
644
|
|
|
665
645
|
private async broadcastPipelineRunStarted(correlationId: string, triggerCommand: string): Promise<void> {
|
|
646
|
+
await this.emitPipelineRunStarted(correlationId, triggerCommand);
|
|
666
647
|
const event: Event & { correlationId: string } = {
|
|
667
648
|
type: 'PipelineRunStarted',
|
|
668
649
|
data: { correlationId, triggerCommand },
|
|
669
650
|
correlationId,
|
|
670
651
|
};
|
|
671
652
|
this.sseManager.broadcast(event);
|
|
672
|
-
await this.emitPipelineRunStarted(correlationId, triggerCommand);
|
|
673
653
|
}
|
|
674
654
|
|
|
675
655
|
private extractItemKey(commandType: string, data: unknown, requestId: string): string {
|
|
@@ -997,7 +977,7 @@ export class PipelineServer {
|
|
|
997
977
|
await this.getOrCreateItemStatus(this.currentSessionId, command.type, itemKey, command.requestId);
|
|
998
978
|
|
|
999
979
|
await this.updateNodeStatus(this.currentSessionId, command.type, 'running');
|
|
1000
|
-
|
|
980
|
+
this.settledBridge.onCommandStarted(command, this.currentSessionId);
|
|
1001
981
|
|
|
1002
982
|
const ctx = this.createContext(command.correlationId);
|
|
1003
983
|
let events: Event[];
|
|
@@ -1043,10 +1023,11 @@ export class PipelineServer {
|
|
|
1043
1023
|
|
|
1044
1024
|
const sourceCommand = this.eventCommandMapper.getSourceCommand(eventWithIds.type);
|
|
1045
1025
|
if (sourceCommand !== undefined) {
|
|
1046
|
-
|
|
1026
|
+
const result = eventWithIds.type.includes('Failed') ? 'failure' : 'success';
|
|
1027
|
+
this.settledBridge.onEventReceived(eventWithIds, sourceCommand, result, this.currentSessionId);
|
|
1047
1028
|
}
|
|
1048
1029
|
|
|
1049
|
-
|
|
1030
|
+
this.routeEventToPhasedExecutor(eventWithIds);
|
|
1050
1031
|
}
|
|
1051
1032
|
|
|
1052
1033
|
await Promise.all(eventsWithIds.map((e) => this.routeEventToPipelines(e)));
|
|
@@ -1061,7 +1042,7 @@ export class PipelineServer {
|
|
|
1061
1042
|
return 'success';
|
|
1062
1043
|
}
|
|
1063
1044
|
|
|
1064
|
-
/* v8 ignore next 11 - integration
|
|
1045
|
+
/* v8 ignore next 11 - integration callback tested via v2-runtime-bridge.specs.ts */
|
|
1065
1046
|
private async dispatchFromSettled(commandType: string, data: unknown, correlationId: string): Promise<void> {
|
|
1066
1047
|
const requestId = `req-${nanoid()}`;
|
|
1067
1048
|
const command: Command & { correlationId: string; requestId: string } = {
|
|
@@ -1074,7 +1055,7 @@ export class PipelineServer {
|
|
|
1074
1055
|
await this.processCommand(command);
|
|
1075
1056
|
}
|
|
1076
1057
|
|
|
1077
|
-
/* v8 ignore next 10 - integration
|
|
1058
|
+
/* v8 ignore next 10 - integration callback tested via phased-bridge.specs.ts */
|
|
1078
1059
|
private async handlePhasedComplete(event: Event, correlationId: string): Promise<void> {
|
|
1079
1060
|
const requestId = `req-${nanoid()}`;
|
|
1080
1061
|
const eventWithIds: EventWithCorrelation = {
|
|
@@ -1118,22 +1099,20 @@ export class PipelineServer {
|
|
|
1118
1099
|
await this.emitCommandDispatched(effectiveCorrelationId, requestId, type, data as Record<string, unknown>);
|
|
1119
1100
|
void this.processCommand(command);
|
|
1120
1101
|
},
|
|
1121
|
-
/* v8 ignore next 3 - integration path tested via pipeline-runtime.specs.ts */
|
|
1122
1102
|
startPhased: async (handler, event) => {
|
|
1123
|
-
|
|
1103
|
+
this.phasedBridge.startPhased(handler, event, correlationId);
|
|
1124
1104
|
},
|
|
1125
1105
|
eventStore: this.eventStoreContext.eventStore,
|
|
1126
1106
|
messageBus: this.messageBus,
|
|
1127
1107
|
};
|
|
1128
1108
|
}
|
|
1129
1109
|
|
|
1130
|
-
|
|
1131
|
-
private async routeEventToPhasedExecutor(event: EventWithCorrelation): Promise<void> {
|
|
1110
|
+
private routeEventToPhasedExecutor(event: EventWithCorrelation): void {
|
|
1132
1111
|
for (const pipeline of this.pipelines.values()) {
|
|
1133
1112
|
for (const handler of pipeline.descriptor.handlers) {
|
|
1134
1113
|
if (handler.type === 'foreach-phased') {
|
|
1135
1114
|
const itemKey = handler.completion.itemKey(event);
|
|
1136
|
-
|
|
1115
|
+
this.phasedBridge.onPhasedItemEvent(event, itemKey);
|
|
1137
1116
|
}
|
|
1138
1117
|
}
|
|
1139
1118
|
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type { Command, Event } from '@auto-engineer/message-bus';
|
|
2
|
+
import type { SettledHandlerDescriptor } from '../core/descriptors.js';
|
|
3
|
+
import { createV2RuntimeBridge } from './v2-runtime-bridge.js';
|
|
4
|
+
|
|
5
|
+
function makeCommand(type: string, correlationId: string, requestId = 'req-1'): Command {
|
|
6
|
+
return { type, data: {}, correlationId, requestId };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function makeEvent(type: string, correlationId: string, data: Record<string, unknown> = {}): Event {
|
|
10
|
+
return { type, data, correlationId };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('V2RuntimeBridge', () => {
|
|
14
|
+
describe('registerSettled', () => {
|
|
15
|
+
it('creates workflow in processor that accepts keyed events', () => {
|
|
16
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
17
|
+
const descriptor: SettledHandlerDescriptor = {
|
|
18
|
+
type: 'settled',
|
|
19
|
+
commandTypes: ['CheckTests', 'CheckTypes'],
|
|
20
|
+
handler: () => undefined,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
bridge.registerSettled(descriptor);
|
|
24
|
+
|
|
25
|
+
const stats = bridge.getSettledStats('corr-1', 'template-CheckTests,CheckTypes');
|
|
26
|
+
expect(stats).toEqual({ status: 'idle', pendingCount: 0, endedCount: 0 });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('onCommandStarted', () => {
|
|
31
|
+
it('emits StartSettled for relevant groups only', () => {
|
|
32
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
33
|
+
bridge.registerSettled({
|
|
34
|
+
type: 'settled',
|
|
35
|
+
commandTypes: ['A', 'B'],
|
|
36
|
+
handler: () => undefined,
|
|
37
|
+
});
|
|
38
|
+
bridge.registerSettled({
|
|
39
|
+
type: 'settled',
|
|
40
|
+
commandTypes: ['C', 'D'],
|
|
41
|
+
handler: () => undefined,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
45
|
+
|
|
46
|
+
const statsAB = bridge.getSettledStats('corr-1', 'template-A,B');
|
|
47
|
+
expect(statsAB).toEqual({ status: 'running', pendingCount: 1, endedCount: 0 });
|
|
48
|
+
|
|
49
|
+
const statsCD = bridge.getSettledStats('corr-1', 'template-C,D');
|
|
50
|
+
expect(statsCD).toEqual({ status: 'idle', pendingCount: 0, endedCount: 0 });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('ignores commands with invalid correlationId or requestId', () => {
|
|
54
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
55
|
+
bridge.registerSettled({
|
|
56
|
+
type: 'settled',
|
|
57
|
+
commandTypes: ['A'],
|
|
58
|
+
handler: () => undefined,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
bridge.onCommandStarted(makeCommand('A', ''));
|
|
62
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1', ''));
|
|
63
|
+
|
|
64
|
+
const stats1 = bridge.getSettledStats('', 'template-A');
|
|
65
|
+
expect(stats1).toEqual({ status: 'idle', pendingCount: 0, endedCount: 0 });
|
|
66
|
+
|
|
67
|
+
const stats2 = bridge.getSettledStats('corr-1', 'template-A');
|
|
68
|
+
expect(stats2).toEqual({ status: 'idle', pendingCount: 0, endedCount: 0 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('ignores commands for unregistered command types', () => {
|
|
72
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
73
|
+
bridge.registerSettled({
|
|
74
|
+
type: 'settled',
|
|
75
|
+
commandTypes: ['A'],
|
|
76
|
+
handler: () => undefined,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
bridge.onCommandStarted(makeCommand('Z', 'corr-1'));
|
|
80
|
+
|
|
81
|
+
const stats = bridge.getSettledStats('corr-1', 'template-A');
|
|
82
|
+
expect(stats).toEqual({ status: 'idle', pendingCount: 0, endedCount: 0 });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('onEventReceived', () => {
|
|
87
|
+
it('translates to CommandCompleted and buffers event', () => {
|
|
88
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
89
|
+
const handlerCalls: Record<string, Event[]>[] = [];
|
|
90
|
+
bridge.registerSettled({
|
|
91
|
+
type: 'settled',
|
|
92
|
+
commandTypes: ['A'],
|
|
93
|
+
handler: (events) => {
|
|
94
|
+
handlerCalls.push(events);
|
|
95
|
+
return undefined;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
100
|
+
bridge.onEventReceived(makeEvent('ACompleted', 'corr-1', { value: 42 }), 'A');
|
|
101
|
+
|
|
102
|
+
expect(handlerCalls).toHaveLength(1);
|
|
103
|
+
expect(handlerCalls[0]).toEqual({
|
|
104
|
+
A: [{ type: 'ACompleted', data: { value: 42 }, correlationId: 'corr-1' }],
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('ignores events for unregistered command types', () => {
|
|
109
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
110
|
+
const handlerCalls: unknown[] = [];
|
|
111
|
+
bridge.registerSettled({
|
|
112
|
+
type: 'settled',
|
|
113
|
+
commandTypes: ['A'],
|
|
114
|
+
handler: (events) => {
|
|
115
|
+
handlerCalls.push(events);
|
|
116
|
+
return undefined;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
121
|
+
bridge.onEventReceived(makeEvent('XCompleted', 'corr-1'), 'X');
|
|
122
|
+
|
|
123
|
+
expect(handlerCalls).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('ignores events with empty correlationId', () => {
|
|
127
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
128
|
+
const handlerCalls: unknown[] = [];
|
|
129
|
+
bridge.registerSettled({
|
|
130
|
+
type: 'settled',
|
|
131
|
+
commandTypes: ['A'],
|
|
132
|
+
handler: (events) => {
|
|
133
|
+
handlerCalls.push(events);
|
|
134
|
+
return undefined;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
139
|
+
bridge.onEventReceived(makeEvent('ACompleted', ''), 'A');
|
|
140
|
+
|
|
141
|
+
expect(handlerCalls).toHaveLength(0);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('AllSettled fires v1 callback', () => {
|
|
146
|
+
it('invokes handler with correct buffered events grouped by commandType', () => {
|
|
147
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
148
|
+
const handlerCalls: Record<string, Event[]>[] = [];
|
|
149
|
+
bridge.registerSettled({
|
|
150
|
+
type: 'settled',
|
|
151
|
+
commandTypes: ['A', 'B'],
|
|
152
|
+
handler: (events) => {
|
|
153
|
+
handlerCalls.push(events);
|
|
154
|
+
return undefined;
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
159
|
+
bridge.onCommandStarted(makeCommand('B', 'corr-1'));
|
|
160
|
+
bridge.onEventReceived(makeEvent('ADone', 'corr-1', { a: 1 }), 'A');
|
|
161
|
+
bridge.onEventReceived(makeEvent('BDone', 'corr-1', { b: 2 }), 'B');
|
|
162
|
+
|
|
163
|
+
expect(handlerCalls).toHaveLength(1);
|
|
164
|
+
expect(handlerCalls[0]).toEqual({
|
|
165
|
+
A: [{ type: 'ADone', data: { a: 1 }, correlationId: 'corr-1' }],
|
|
166
|
+
B: [{ type: 'BDone', data: { b: 2 }, correlationId: 'corr-1' }],
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('passes send function that dispatches via onDispatch', () => {
|
|
171
|
+
const dispatched: Array<{ commandType: string; data: unknown; correlationId: string }> = [];
|
|
172
|
+
const bridge = createV2RuntimeBridge({
|
|
173
|
+
onDispatch: (commandType, data, correlationId) => {
|
|
174
|
+
dispatched.push({ commandType, data, correlationId });
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
bridge.registerSettled({
|
|
179
|
+
type: 'settled',
|
|
180
|
+
commandTypes: ['A'],
|
|
181
|
+
handler: (_events, send) => {
|
|
182
|
+
send('NextCommand', { payload: true });
|
|
183
|
+
return undefined;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
188
|
+
bridge.onEventReceived(makeEvent('ADone', 'corr-1'), 'A');
|
|
189
|
+
|
|
190
|
+
expect(dispatched).toEqual([{ commandType: 'NextCommand', data: { payload: true }, correlationId: 'corr-1' }]);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('RetryCommands', () => {
|
|
195
|
+
it('triggers onDispatch for each failed commandType', () => {
|
|
196
|
+
const dispatched: Array<{ commandType: string; data: unknown; correlationId: string }> = [];
|
|
197
|
+
const bridge = createV2RuntimeBridge({
|
|
198
|
+
onDispatch: (commandType, data, correlationId) => {
|
|
199
|
+
dispatched.push({ commandType, data, correlationId });
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
bridge.registerSettled({
|
|
204
|
+
type: 'settled',
|
|
205
|
+
commandTypes: ['A', 'B'],
|
|
206
|
+
handler: () => undefined,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
210
|
+
bridge.onCommandStarted(makeCommand('B', 'corr-1'));
|
|
211
|
+
bridge.onEventReceived(makeEvent('ADone', 'corr-1'), 'A');
|
|
212
|
+
bridge.onEventReceived(makeEvent('BFailed', 'corr-1'), 'B', 'failure');
|
|
213
|
+
|
|
214
|
+
expect(dispatched).toEqual([{ commandType: 'B', data: {}, correlationId: 'corr-1' }]);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('persist: true resets instance', () => {
|
|
219
|
+
it('allows receiving new commands after reset', () => {
|
|
220
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
221
|
+
const handlerCalls: Record<string, Event[]>[] = [];
|
|
222
|
+
bridge.registerSettled({
|
|
223
|
+
type: 'settled',
|
|
224
|
+
commandTypes: ['A'],
|
|
225
|
+
handler: (events) => {
|
|
226
|
+
handlerCalls.push(events);
|
|
227
|
+
return { persist: true };
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
232
|
+
bridge.onEventReceived(makeEvent('ADone', 'corr-1', { round: 1 }), 'A');
|
|
233
|
+
|
|
234
|
+
expect(handlerCalls).toHaveLength(1);
|
|
235
|
+
|
|
236
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
237
|
+
bridge.onEventReceived(makeEvent('ADone', 'corr-1', { round: 2 }), 'A');
|
|
238
|
+
|
|
239
|
+
expect(handlerCalls).toHaveLength(2);
|
|
240
|
+
expect(handlerCalls[1]).toEqual({
|
|
241
|
+
A: [{ type: 'ADone', data: { round: 2 }, correlationId: 'corr-1' }],
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('independent correlationIds', () => {
|
|
247
|
+
it('tracks two correlationIds independently', () => {
|
|
248
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
249
|
+
const handlerCalls: Array<{ correlationId: string; events: Record<string, Event[]> }> = [];
|
|
250
|
+
|
|
251
|
+
bridge.registerSettled({
|
|
252
|
+
type: 'settled',
|
|
253
|
+
commandTypes: ['A', 'B'],
|
|
254
|
+
handler: (events) => {
|
|
255
|
+
const corrId = Object.values(events).flat()[0]?.correlationId ?? '';
|
|
256
|
+
handlerCalls.push({ correlationId: corrId, events });
|
|
257
|
+
return undefined;
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
262
|
+
bridge.onCommandStarted(makeCommand('B', 'corr-1'));
|
|
263
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-2'));
|
|
264
|
+
bridge.onCommandStarted(makeCommand('B', 'corr-2'));
|
|
265
|
+
|
|
266
|
+
bridge.onEventReceived(makeEvent('ADone', 'corr-1', { v: 1 }), 'A');
|
|
267
|
+
bridge.onEventReceived(makeEvent('ADone', 'corr-2', { v: 2 }), 'A');
|
|
268
|
+
bridge.onEventReceived(makeEvent('BDone', 'corr-1', { v: 3 }), 'B');
|
|
269
|
+
|
|
270
|
+
expect(handlerCalls).toHaveLength(1);
|
|
271
|
+
expect(handlerCalls[0].correlationId).toBe('corr-1');
|
|
272
|
+
|
|
273
|
+
bridge.onEventReceived(makeEvent('BDone', 'corr-2', { v: 4 }), 'B');
|
|
274
|
+
|
|
275
|
+
expect(handlerCalls).toHaveLength(2);
|
|
276
|
+
expect(handlerCalls[1].correlationId).toBe('corr-2');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('getSettledStats', () => {
|
|
281
|
+
it('returns idle for unknown correlationId', () => {
|
|
282
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
283
|
+
bridge.registerSettled({
|
|
284
|
+
type: 'settled',
|
|
285
|
+
commandTypes: ['A'],
|
|
286
|
+
handler: () => undefined,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const stats = bridge.getSettledStats('unknown', 'template-A');
|
|
290
|
+
expect(stats).toEqual({ status: 'idle', pendingCount: 0, endedCount: 0 });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('returns running while waiting for completions', () => {
|
|
294
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
295
|
+
bridge.registerSettled({
|
|
296
|
+
type: 'settled',
|
|
297
|
+
commandTypes: ['A', 'B'],
|
|
298
|
+
handler: () => undefined,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
302
|
+
|
|
303
|
+
const stats = bridge.getSettledStats('corr-1', 'template-A,B');
|
|
304
|
+
expect(stats).toEqual({ status: 'running', pendingCount: 1, endedCount: 0 });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('returns success after AllSettled', () => {
|
|
308
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
309
|
+
bridge.registerSettled({
|
|
310
|
+
type: 'settled',
|
|
311
|
+
commandTypes: ['A'],
|
|
312
|
+
handler: () => undefined,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
316
|
+
bridge.onEventReceived(makeEvent('ADone', 'corr-1'), 'A');
|
|
317
|
+
|
|
318
|
+
const stats = bridge.getSettledStats('corr-1', 'template-A');
|
|
319
|
+
expect(stats).toEqual({ status: 'success', pendingCount: 0, endedCount: 1 });
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('returns error after SettledFailed', () => {
|
|
323
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
324
|
+
bridge.registerSettled(
|
|
325
|
+
{
|
|
326
|
+
type: 'settled',
|
|
327
|
+
commandTypes: ['A'],
|
|
328
|
+
handler: () => undefined,
|
|
329
|
+
},
|
|
330
|
+
{ maxRetries: 0 },
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
334
|
+
bridge.onEventReceived(makeEvent('AFailed', 'corr-1'), 'A', 'failure');
|
|
335
|
+
|
|
336
|
+
const stats = bridge.getSettledStats('corr-1', 'template-A');
|
|
337
|
+
expect(stats).toEqual({ status: 'error', pendingCount: 0, endedCount: 0 });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('returns idle for unknown templateId', () => {
|
|
341
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
342
|
+
|
|
343
|
+
const stats = bridge.getSettledStats('corr-1', 'template-Unknown');
|
|
344
|
+
expect(stats).toEqual({ status: 'idle', pendingCount: 0, endedCount: 0 });
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|