@auto-engineer/pipeline 1.105.0 → 1.107.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/ketchup-plan.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## TODO
4
4
 
5
+ ### Fix Settled Node Labels and Event Routing (Bursts 107-112)
6
+
7
+ - [x] Burst 107: processSettledHandler uses descriptor.label for graph node [depends: none]
8
+ - [x] Burst 108: bridge filters commands by sourceEventTypes [depends: none]
9
+ - [x] Burst 109: builder sets sourceEventTypes from emit chain event type [depends: none]
10
+ - [x] Burst 110: Add label and sourceEventTypes to descriptor and builder (merged into 107+109)
11
+ - [x] Burst 111: Add source event filtering to bridge (merged into 108)
12
+ - [x] Burst 112: Thread source event type through pipeline server [depends: 110, 111]
13
+
5
14
  ### Graph Rendering Fix (Burst 106)
6
15
 
7
16
  - [ ] Burst 106: Show source commands whose events are listened to by the pipeline [depends: none]
package/package.json CHANGED
@@ -14,8 +14,8 @@
14
14
  "get-port": "^7.1.0",
15
15
  "jose": "^5.9.6",
16
16
  "nanoid": "^5.0.0",
17
- "@auto-engineer/file-store": "1.105.0",
18
- "@auto-engineer/message-bus": "1.105.0"
17
+ "@auto-engineer/file-store": "1.107.0",
18
+ "@auto-engineer/message-bus": "1.107.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/cors": "^2.8.17",
@@ -24,7 +24,7 @@
24
24
  "publishConfig": {
25
25
  "access": "public"
26
26
  },
27
- "version": "1.105.0",
27
+ "version": "1.107.0",
28
28
  "scripts": {
29
29
  "build": "tsc && tsx ../../scripts/fix-esm-imports.ts",
30
30
  "test": "vitest run --reporter=dot",
@@ -153,6 +153,64 @@ describe('settled()', () => {
153
153
  expect(settledNode?.label).toBe('Settled');
154
154
  });
155
155
 
156
+ it('sets sourceEventTypes from preceding emit chain event type', () => {
157
+ const pipeline = define('test')
158
+ .on('SliceImplemented')
159
+ .emit('CheckTests', {})
160
+ .settled(['CheckTests', 'CheckTypes'])
161
+ .dispatch({ dispatches: ['ImplementSlice'] }, () => {})
162
+ .build();
163
+
164
+ const descriptor = pipeline.descriptor.handlers[1] as SettledHandlerDescriptor;
165
+ expect(descriptor.sourceEventTypes).toEqual(['SliceImplemented']);
166
+ });
167
+
168
+ it('has no sourceEventTypes for top-level settled', () => {
169
+ const pipeline = define('test')
170
+ .settled(['CheckTests'])
171
+ .dispatch({ dispatches: [] }, () => {})
172
+ .build();
173
+
174
+ const descriptor = pipeline.descriptor.handlers[0] as SettledHandlerDescriptor;
175
+ expect(descriptor.sourceEventTypes).toBeUndefined();
176
+ });
177
+
178
+ it('uses custom label passed to settled()', () => {
179
+ const pipeline = define('test')
180
+ .settled(['CheckTests', 'CheckTypes'], 'Slice Checks')
181
+ .dispatch({ dispatches: ['ImplementSlice'] }, () => {})
182
+ .build();
183
+
184
+ const descriptor = pipeline.descriptor.handlers[0] as SettledHandlerDescriptor;
185
+ expect(descriptor.label).toBe('Slice Checks');
186
+
187
+ const graph = pipeline.toGraph();
188
+ const settledNode = graph.nodes.find((n) => n.id.startsWith('settled:'));
189
+ expect(settledNode).toEqual({
190
+ id: 'settled:settled-0',
191
+ type: 'settled',
192
+ label: 'Slice Checks',
193
+ });
194
+ });
195
+
196
+ it('uses label from descriptor for settled graph nodes', () => {
197
+ const pipeline = define('test')
198
+ .settled(['CheckTests', 'CheckTypes'])
199
+ .dispatch({ dispatches: ['ImplementSlice'] }, () => {})
200
+ .build();
201
+
202
+ const handlers = pipeline.descriptor.handlers as SettledHandlerDescriptor[];
203
+ expect(handlers[0].label).toBe('ImplementSlice Settled');
204
+
205
+ const graph = pipeline.toGraph();
206
+ const settledNode = graph.nodes.find((n) => n.id.startsWith('settled:'));
207
+ expect(settledNode).toEqual({
208
+ id: 'settled:settled-0',
209
+ type: 'settled',
210
+ label: 'ImplementSlice Settled',
211
+ });
212
+ });
213
+
156
214
  it('should accept options-first dispatch with dispatches array', () => {
157
215
  const pipeline = define('test')
158
216
  .settled(['CheckA'])
@@ -34,7 +34,7 @@ export interface PipelineBuilder {
34
34
  key<E>(name: string, extractor: (event: E) => string): PipelineBuilder;
35
35
  declare(commandType: string): DeclareBuilder;
36
36
  on(eventType: string): TriggerBuilder;
37
- settled(commandTypes: readonly string[]): SettledBuilder;
37
+ settled(commandTypes: readonly string[], label?: string): SettledBuilder;
38
38
  build(): Pipeline;
39
39
  }
40
40
 
@@ -55,7 +55,7 @@ export interface SettledBuilder {
55
55
  export interface SettledChain {
56
56
  declare(commandType: string): DeclareBuilder;
57
57
  on(eventType: string): TriggerBuilder;
58
- settled(commandTypes: readonly string[]): SettledBuilder;
58
+ settled(commandTypes: readonly string[], label?: string): SettledBuilder;
59
59
  build(): Pipeline;
60
60
  }
61
61
 
@@ -133,7 +133,7 @@ export interface EmitChain {
133
133
  emit(commandType: string, data: unknown): EmitChain;
134
134
  declare(commandType: string): DeclareBuilder;
135
135
  on(eventType: string): TriggerBuilder;
136
- settled(commandTypes: readonly string[]): SettledBuilder;
136
+ settled(commandTypes: readonly string[], label?: string): SettledBuilder;
137
137
  build(): Pipeline;
138
138
  }
139
139
 
@@ -156,11 +156,16 @@ class PipelineBuilderImpl implements PipelineBuilder {
156
156
  private descriptionValue?: string;
157
157
  private readonly keys: Map<string, KeyExtractor> = new Map();
158
158
  private readonly handlers: HandlerDescriptor[] = [];
159
+ private settledCounter = 0;
159
160
 
160
161
  constructor(name: string) {
161
162
  this.name = name;
162
163
  }
163
164
 
165
+ nextSettledId(): string {
166
+ return `settled-${this.settledCounter++}`;
167
+ }
168
+
164
169
  version(v: string): PipelineBuilder {
165
170
  this.versionValue = v;
166
171
  return this;
@@ -184,8 +189,8 @@ class PipelineBuilderImpl implements PipelineBuilder {
184
189
  return new TriggerBuilderImpl(this, eventType);
185
190
  }
186
191
 
187
- settled(commandTypes: readonly string[]): SettledBuilder {
188
- return new SettledBuilderImpl(this, commandTypes);
192
+ settled(commandTypes: readonly string[], label?: string): SettledBuilder {
193
+ return new SettledBuilderImpl(this, commandTypes, undefined, label);
189
194
  }
190
195
 
191
196
  addHandler(handler: HandlerDescriptor): void {
@@ -288,8 +293,9 @@ function processAcceptsHandler(ctx: GraphBuilderContext, handler: AcceptsDescrip
288
293
  }
289
294
 
290
295
  function processSettledHandler(ctx: GraphBuilderContext, handler: SettledHandlerDescriptor): void {
291
- const settledNodeId = `settled:${handler.commandTypes.join(',')}`;
292
- addNode(ctx, settledNodeId, 'settled', 'Settled');
296
+ const settledKey = handler.settledId ?? handler.commandTypes.join(',');
297
+ const settledNodeId = `settled:${settledKey}`;
298
+ addNode(ctx, settledNodeId, 'settled', handler.label ?? 'Settled');
293
299
 
294
300
  for (const commandType of handler.commandTypes) {
295
301
  addNode(ctx, `cmd:${commandType}`, 'command', commandType);
@@ -402,9 +408,9 @@ class EmitChainImpl implements EmitChain {
402
408
  return new TriggerBuilderImpl(this.parent, eventType);
403
409
  }
404
410
 
405
- settled(commandTypes: readonly string[]): SettledBuilder {
411
+ settled(commandTypes: readonly string[], label?: string): SettledBuilder {
406
412
  this.finalizeHandler();
407
- return new SettledBuilderImpl(this.parent, commandTypes);
413
+ return new SettledBuilderImpl(this.parent, commandTypes, this.eventType, label);
408
414
  }
409
415
 
410
416
  build(): Pipeline {
@@ -707,6 +713,8 @@ class SettledBuilderImpl implements SettledBuilder {
707
713
  constructor(
708
714
  private readonly parent: PipelineBuilderImpl,
709
715
  private readonly commandTypes: readonly string[],
716
+ private readonly sourceEventType?: string,
717
+ private readonly customLabel?: string,
710
718
  ) {}
711
719
 
712
720
  dispatch<const D extends readonly string[]>(
@@ -716,7 +724,14 @@ class SettledBuilderImpl implements SettledBuilder {
716
724
  send: (commandType: D[number], data: unknown) => void,
717
725
  ) => undefined | { persist: boolean },
718
726
  ): SettledChain {
719
- return new SettledChainImpl(this.parent, this.commandTypes, handler as SettledHandler, options.dispatches);
727
+ return new SettledChainImpl(
728
+ this.parent,
729
+ this.commandTypes,
730
+ handler as SettledHandler,
731
+ options.dispatches,
732
+ this.sourceEventType,
733
+ this.customLabel,
734
+ );
720
735
  }
721
736
  }
722
737
 
@@ -726,6 +741,8 @@ class SettledChainImpl implements SettledChain {
726
741
  private readonly commandTypes: readonly string[],
727
742
  private readonly handler: SettledHandler,
728
743
  private readonly dispatches?: readonly string[],
744
+ private readonly sourceEventType?: string,
745
+ private readonly customLabel?: string,
729
746
  ) {}
730
747
 
731
748
  declare(commandType: string): DeclareBuilder {
@@ -738,9 +755,9 @@ class SettledChainImpl implements SettledChain {
738
755
  return new TriggerBuilderImpl(this.parent, eventType);
739
756
  }
740
757
 
741
- settled(commandTypes: readonly string[]): SettledBuilder {
758
+ settled(commandTypes: readonly string[], label?: string): SettledBuilder {
742
759
  this.finalizeHandler();
743
- return new SettledBuilderImpl(this.parent, commandTypes);
760
+ return new SettledBuilderImpl(this.parent, commandTypes, undefined, label);
744
761
  }
745
762
 
746
763
  build(): Pipeline {
@@ -749,11 +766,16 @@ class SettledChainImpl implements SettledChain {
749
766
  }
750
767
 
751
768
  private finalizeHandler(): void {
769
+ const autoLabel = this.dispatches && this.dispatches.length > 0 ? `${this.dispatches[0]} Settled` : undefined;
770
+ const settledLabel = this.customLabel ?? autoLabel;
752
771
  const descriptor: SettledHandlerDescriptor = {
753
772
  type: 'settled',
754
773
  commandTypes: this.commandTypes,
755
774
  handler: this.handler,
756
775
  dispatches: this.dispatches,
776
+ settledId: this.parent.nextSettledId(),
777
+ label: settledLabel,
778
+ sourceEventTypes: this.sourceEventType ? [this.sourceEventType] : undefined,
757
779
  };
758
780
  this.parent.addHandler(descriptor);
759
781
  }
@@ -85,6 +85,9 @@ export interface SettledHandlerDescriptor {
85
85
  commandTypes: readonly string[];
86
86
  handler: SettledHandler;
87
87
  dispatches?: readonly string[];
88
+ settledId?: string;
89
+ label?: string;
90
+ sourceEventTypes?: readonly string[];
88
91
  }
89
92
 
90
93
  export interface AcceptsDescriptor {
@@ -460,7 +460,7 @@ describe('PipelineServer', () => {
460
460
  server.registerPipeline(pipeline);
461
461
  await server.start();
462
462
  const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
463
- const settledNode = data.nodes.find((n) => n.id === 'settled:CheckTests');
463
+ const settledNode = data.nodes.find((n) => n.id === 'settled:settled-0');
464
464
  expect(settledNode?.status).toBe('idle');
465
465
  expect(settledNode?.pendingCount).toBe(0);
466
466
  expect(settledNode?.endedCount).toBe(0);
@@ -493,7 +493,7 @@ describe('PipelineServer', () => {
493
493
  await new Promise((r) => setTimeout(r, 100));
494
494
 
495
495
  const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
496
- const settledNode = data.nodes.find((n) => n.id === 'settled:CheckTests');
496
+ const settledNode = data.nodes.find((n) => n.id === 'settled:settled-0');
497
497
  expect(settledNode?.status).toBeDefined();
498
498
  expect(settledNode?.pendingCount).toBeDefined();
499
499
  expect(settledNode?.endedCount).toBeDefined();
@@ -526,9 +526,9 @@ describe('PipelineServer', () => {
526
526
  await new Promise((r) => setTimeout(r, 100));
527
527
 
528
528
  const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
529
- const settledNode = data.nodes.find((n) => n.id === 'settled:CheckTests');
529
+ const settledNode = data.nodes.find((n) => n.id === 'settled:settled-0');
530
530
  expect(settledNode).toEqual({
531
- id: 'settled:CheckTests',
531
+ id: 'settled:settled-0',
532
532
  type: 'settled',
533
533
  label: 'Settled',
534
534
  status: 'success',
@@ -1407,8 +1407,8 @@ describe('PipelineServer', () => {
1407
1407
  await server.start();
1408
1408
  const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
1409
1409
  const mermaid = await res.text();
1410
- expect(mermaid).toContain('CheckA --> settled_CheckA_CheckB');
1411
- expect(mermaid).toContain('CheckB --> settled_CheckA_CheckB');
1410
+ expect(mermaid).toContain('CheckA --> settled_settled-0');
1411
+ expect(mermaid).toContain('CheckB --> settled_settled-0');
1412
1412
  await server.stop();
1413
1413
  });
1414
1414
 
@@ -1435,7 +1435,7 @@ describe('PipelineServer', () => {
1435
1435
  await server.start();
1436
1436
  const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
1437
1437
  const mermaid = await res.text();
1438
- expect(mermaid).toContain('settled_CheckA -.->|retry| RetryCommand');
1438
+ expect(mermaid).toContain('settled_settled-0 -.->|retry| RetryCommand');
1439
1439
  await server.stop();
1440
1440
  });
1441
1441
 
@@ -75,6 +75,7 @@ export class PipelineServer {
75
75
  private sqliteEventStore?: EventStore;
76
76
  private currentSessionId = '';
77
77
  private readonly quiescenceTracker: QuiescenceTracker;
78
+ private readonly requestIdToSourceEvent = new Map<string, string>();
78
79
 
79
80
  constructor(config: PipelineServerConfig) {
80
81
  this.storeFileName = config.storeFileName;
@@ -993,6 +994,12 @@ export class PipelineServer {
993
994
  return false;
994
995
  }
995
996
 
997
+ private settledCommandTypes(graph: GraphIR, settledNodeId: string): string[] {
998
+ return graph.edges
999
+ .filter((e) => e.to === settledNodeId && e.from.startsWith('cmd:'))
1000
+ .map((e) => e.from.replace('cmd:', ''));
1001
+ }
1002
+
996
1003
  private extractPipelineEvents(graph: GraphIR, commandToEvents: Record<string, string[]>): Set<string> {
997
1004
  const pipelineEvents = new Set<string>();
998
1005
 
@@ -1001,8 +1008,7 @@ export class PipelineServer {
1001
1008
  pipelineEvents.add(node.id.replace('evt:', ''));
1002
1009
  }
1003
1010
  if (node.id.startsWith('settled:')) {
1004
- const commandTypes = node.id.replace('settled:', '').split(',');
1005
- for (const commandType of commandTypes) {
1011
+ for (const commandType of this.settledCommandTypes(graph, node.id)) {
1006
1012
  const events = commandToEvents[commandType];
1007
1013
  if (events !== undefined) {
1008
1014
  for (const eventName of events) {
@@ -1034,8 +1040,8 @@ export class PipelineServer {
1034
1040
  commandNodes.add(commandName);
1035
1041
  lines.push(` ${commandName}[${node.label}]`);
1036
1042
  } else if (node.id.startsWith('settled:')) {
1037
- const commandTypes = node.id.replace('settled:', '').split(',');
1038
- const safeId = `settled_${commandTypes.join('_')}`;
1043
+ const commandTypes = this.settledCommandTypes(graph, node.id);
1044
+ const safeId = `settled_${node.id.replace('settled:', '')}`;
1039
1045
  settledNodes.add(safeId);
1040
1046
  lines.push(` ${safeId}{{${commandTypes.join(', ')}}}`);
1041
1047
  }
@@ -1067,8 +1073,7 @@ export class PipelineServer {
1067
1073
  if (nodeId.startsWith('cmd:')) {
1068
1074
  return nodeId.replace('cmd:', '');
1069
1075
  }
1070
- const commandTypes = nodeId.replace('settled:', '').split(',');
1071
- return `settled_${commandTypes.join('_')}`;
1076
+ return `settled_${nodeId.replace('settled:', '')}`;
1072
1077
  }
1073
1078
 
1074
1079
  private addMermaidStyles(
@@ -1135,7 +1140,9 @@ export class PipelineServer {
1135
1140
  await this.getOrCreateItemStatus(this.currentSessionId, command.type, itemKey, command.requestId);
1136
1141
 
1137
1142
  await this.updateNodeStatus(this.currentSessionId, command.type, 'running');
1138
- this.settledBridge.onCommandStarted(command, this.currentSessionId);
1143
+ const sourceEventType = this.requestIdToSourceEvent.get(command.requestId);
1144
+ this.requestIdToSourceEvent.delete(command.requestId);
1145
+ this.settledBridge.onCommandStarted(command, this.currentSessionId, sourceEventType);
1139
1146
 
1140
1147
  const ctx = this.createContext(command.correlationId, signal);
1141
1148
  let events: Event[];
@@ -1193,7 +1200,7 @@ export class PipelineServer {
1193
1200
  const sourceCommand = this.eventCommandMapper.getSourceCommand(eventWithIds.type);
1194
1201
  if (sourceCommand !== undefined) {
1195
1202
  const result = eventWithIds.type.includes('Failed') ? 'failure' : 'success';
1196
- this.settledBridge.onEventReceived(eventWithIds, sourceCommand, result, this.currentSessionId);
1203
+ this.settledBridge.onEventReceived(eventWithIds, sourceCommand, result, this.currentSessionId, sourceEventType);
1197
1204
  }
1198
1205
 
1199
1206
  this.routeEventToPhasedExecutor(eventWithIds);
@@ -1237,12 +1244,12 @@ export class PipelineServer {
1237
1244
  }
1238
1245
 
1239
1246
  private async routeEventToPipelines(event: EventWithCorrelation): Promise<void> {
1240
- const ctx = this.createContext(event.correlationId);
1247
+ const ctx = this.createContext(event.correlationId, undefined, event.type);
1241
1248
  const runtimes = Array.from(this.runtimes.values());
1242
1249
  await Promise.all(runtimes.map((runtime) => runtime.handleEvent(event, ctx)));
1243
1250
  }
1244
1251
 
1245
- private createContext(correlationId: string, signal?: AbortSignal): PipelineContext {
1252
+ private createContext(correlationId: string, signal?: AbortSignal, sourceEventType?: string): PipelineContext {
1246
1253
  return {
1247
1254
  correlationId,
1248
1255
  signal,
@@ -1266,6 +1273,9 @@ export class PipelineServer {
1266
1273
  correlationId: effectiveCorrelationId,
1267
1274
  requestId,
1268
1275
  };
1276
+ if (sourceEventType) {
1277
+ this.requestIdToSourceEvent.set(requestId, sourceEventType);
1278
+ }
1269
1279
  await this.emitCommandDispatched(effectiveCorrelationId, requestId, type, data as Record<string, unknown>);
1270
1280
  void this.processCommand(command);
1271
1281
  },
@@ -344,4 +344,120 @@ describe('V2RuntimeBridge', () => {
344
344
  expect(stats).toEqual({ status: 'idle', pendingCount: 0, endedCount: 0 });
345
345
  });
346
346
  });
347
+
348
+ describe('sourceEventTypes filtering', () => {
349
+ it('only routes commands to settled blocks matching source event type', () => {
350
+ const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
351
+ const handler1Calls: Record<string, Event[]>[] = [];
352
+ const handler2Calls: Record<string, Event[]>[] = [];
353
+
354
+ bridge.registerSettled({
355
+ type: 'settled',
356
+ commandTypes: ['A', 'B'],
357
+ settledId: 'settled-0',
358
+ sourceEventTypes: ['Foo'],
359
+ handler: (events) => {
360
+ handler1Calls.push(events);
361
+ return undefined;
362
+ },
363
+ });
364
+
365
+ bridge.registerSettled({
366
+ type: 'settled',
367
+ commandTypes: ['A', 'B'],
368
+ settledId: 'settled-1',
369
+ sourceEventTypes: ['Bar'],
370
+ handler: (events) => {
371
+ handler2Calls.push(events);
372
+ return undefined;
373
+ },
374
+ });
375
+
376
+ bridge.onCommandStarted(makeCommand('A', 'corr-1'), undefined, 'Foo');
377
+ bridge.onCommandStarted(makeCommand('B', 'corr-1'), undefined, 'Foo');
378
+ bridge.onEventReceived(makeEvent('ADone', 'corr-1', { a: 1 }), 'A', 'success', undefined, 'Foo');
379
+ bridge.onEventReceived(makeEvent('BDone', 'corr-1', { b: 2 }), 'B', 'success', undefined, 'Foo');
380
+
381
+ expect(handler1Calls).toHaveLength(1);
382
+ expect(handler2Calls).toHaveLength(0);
383
+ });
384
+
385
+ it('routes to all blocks when sourceEventType is not provided', () => {
386
+ const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
387
+ const handler1Calls: Record<string, Event[]>[] = [];
388
+ const handler2Calls: Record<string, Event[]>[] = [];
389
+
390
+ bridge.registerSettled({
391
+ type: 'settled',
392
+ commandTypes: ['A'],
393
+ settledId: 'settled-0',
394
+ sourceEventTypes: ['Foo'],
395
+ handler: (events) => {
396
+ handler1Calls.push(events);
397
+ return undefined;
398
+ },
399
+ });
400
+
401
+ bridge.registerSettled({
402
+ type: 'settled',
403
+ commandTypes: ['A'],
404
+ settledId: 'settled-1',
405
+ sourceEventTypes: ['Bar'],
406
+ handler: (events) => {
407
+ handler2Calls.push(events);
408
+ return undefined;
409
+ },
410
+ });
411
+
412
+ bridge.onCommandStarted(makeCommand('A', 'corr-1'));
413
+ bridge.onEventReceived(makeEvent('ADone', 'corr-1'), 'A');
414
+
415
+ expect(handler1Calls).toHaveLength(1);
416
+ expect(handler2Calls).toHaveLength(1);
417
+ });
418
+ });
419
+
420
+ describe('multiple settled blocks with same command types', () => {
421
+ it('fires both handlers independently when two blocks watch the same commands', () => {
422
+ const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
423
+ const handler1Calls: Record<string, Event[]>[] = [];
424
+ const handler2Calls: Record<string, Event[]>[] = [];
425
+
426
+ bridge.registerSettled({
427
+ type: 'settled',
428
+ commandTypes: ['A', 'B'],
429
+ settledId: 'settled-0',
430
+ handler: (events) => {
431
+ handler1Calls.push(events);
432
+ return undefined;
433
+ },
434
+ });
435
+
436
+ bridge.registerSettled({
437
+ type: 'settled',
438
+ commandTypes: ['A', 'B'],
439
+ settledId: 'settled-1',
440
+ handler: (events) => {
441
+ handler2Calls.push(events);
442
+ return undefined;
443
+ },
444
+ });
445
+
446
+ bridge.onCommandStarted(makeCommand('A', 'corr-1'));
447
+ bridge.onCommandStarted(makeCommand('B', 'corr-1'));
448
+ bridge.onEventReceived(makeEvent('ADone', 'corr-1', { a: 1 }), 'A');
449
+ bridge.onEventReceived(makeEvent('BDone', 'corr-1', { b: 2 }), 'B');
450
+
451
+ expect(handler1Calls).toHaveLength(1);
452
+ expect(handler2Calls).toHaveLength(1);
453
+ expect(handler1Calls[0]).toEqual({
454
+ A: [{ type: 'ADone', data: { a: 1 }, correlationId: 'corr-1' }],
455
+ B: [{ type: 'BDone', data: { b: 2 }, correlationId: 'corr-1' }],
456
+ });
457
+ expect(handler2Calls[0]).toEqual({
458
+ A: [{ type: 'ADone', data: { a: 1 }, correlationId: 'corr-1' }],
459
+ B: [{ type: 'BDone', data: { b: 2 }, correlationId: 'corr-1' }],
460
+ });
461
+ });
462
+ });
347
463
  });
@@ -20,6 +20,7 @@ interface RegisteredSettled {
20
20
  descriptor: SettledHandlerDescriptor;
21
21
  commandTypes: readonly string[];
22
22
  maxRetries: number;
23
+ sourceEventTypes?: readonly string[];
23
24
  }
24
25
 
25
26
  type SettledEvent = SettledInput | SettledOutput;
@@ -142,13 +143,16 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
142
143
  return {
143
144
  registerSettled(descriptor: SettledHandlerDescriptor, config?: { maxRetries?: number }): void {
144
145
  const commandTypes = descriptor.commandTypes;
145
- const templateId = `template-${commandTypes.join(',')}`;
146
+ const templateId = descriptor.settledId
147
+ ? `template-${descriptor.settledId}`
148
+ : `template-${commandTypes.join(',')}`;
146
149
 
147
150
  const registration: RegisteredSettled = {
148
151
  templateId,
149
152
  descriptor,
150
153
  commandTypes,
151
154
  maxRetries: config?.maxRetries ?? 3,
155
+ sourceEventTypes: descriptor.sourceEventTypes,
152
156
  };
153
157
 
154
158
  registrations.set(templateId, registration);
@@ -160,7 +164,7 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
160
164
  }
161
165
  },
162
166
 
163
- onCommandStarted(command: Command, sessionCorrelationId?: string): void {
167
+ onCommandStarted(command: Command, sessionCorrelationId?: string, sourceEventType?: string): void {
164
168
  const { type: commandType, correlationId, requestId } = command;
165
169
 
166
170
  if (!isValidId(correlationId) || !isValidId(requestId)) {
@@ -177,6 +181,14 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
177
181
  for (const templateId of templateIds) {
178
182
  const registration = registrations.get(templateId);
179
183
  if (registration) {
184
+ if (
185
+ sourceEventType &&
186
+ registration.sourceEventTypes &&
187
+ registration.sourceEventTypes.length > 0 &&
188
+ !registration.sourceEventTypes.includes(sourceEventType)
189
+ ) {
190
+ continue;
191
+ }
180
192
  const history = getHistory(templateId, keyCorrelationId);
181
193
  const input: SettledInput = {
182
194
  type: 'StartSettled',
@@ -192,6 +204,7 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
192
204
  sourceCommandType: string,
193
205
  result: 'success' | 'failure' = 'success',
194
206
  sessionCorrelationId?: string,
207
+ sourceEventType?: string,
195
208
  ): void {
196
209
  const correlationId = event.correlationId;
197
210
 
@@ -208,6 +221,14 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
208
221
 
209
222
  for (const templateId of templateIds) {
210
223
  const registration = registrations.get(templateId)!;
224
+ if (
225
+ sourceEventType &&
226
+ registration.sourceEventTypes &&
227
+ registration.sourceEventTypes.length > 0 &&
228
+ !registration.sourceEventTypes.includes(sourceEventType)
229
+ ) {
230
+ continue;
231
+ }
211
232
  const existing = ensureBuffer(templateId, keyCorrelationId, sourceCommandType);
212
233
  existing.push(event);
213
234