@cratis/chronicle 6.39.0 → 6.41.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.
Files changed (99) hide show
  1. package/ChronicleClient.ts +2 -3
  2. package/ChronicleOptions.ts +5 -1
  3. package/EventStore.ts +1 -2
  4. package/WellKnownTelemetryNames.ts +21 -0
  5. package/dist/ChronicleClient.d.ts.map +1 -1
  6. package/dist/ChronicleClient.js +2 -3
  7. package/dist/ChronicleClient.js.map +1 -1
  8. package/dist/ChronicleOptions.d.ts +1 -1
  9. package/dist/ChronicleOptions.d.ts.map +1 -1
  10. package/dist/ChronicleOptions.js +5 -1
  11. package/dist/ChronicleOptions.js.map +1 -1
  12. package/dist/EventStore.d.ts.map +1 -1
  13. package/dist/EventStore.js +1 -2
  14. package/dist/EventStore.js.map +1 -1
  15. package/dist/WellKnownTelemetryNames.d.ts +21 -0
  16. package/dist/WellKnownTelemetryNames.d.ts.map +1 -1
  17. package/dist/WellKnownTelemetryNames.js +21 -0
  18. package/dist/WellKnownTelemetryNames.js.map +1 -1
  19. package/dist/eventSequences/EventSequence.js +9 -9
  20. package/dist/eventSequences/EventSequence.js.map +1 -1
  21. package/dist/telemetry/ChronicleTelemetryOptions.d.ts +3 -1
  22. package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -1
  23. package/dist/telemetry/observeOperation.d.ts +3 -1
  24. package/dist/telemetry/observeOperation.d.ts.map +1 -1
  25. package/dist/telemetry/observeOperation.js +3 -1
  26. package/dist/telemetry/observeOperation.js.map +1 -1
  27. package/dist/testing/EventScenario.js +1 -1
  28. package/dist/testing/EventScenario.js.map +1 -1
  29. package/dist/testing/EventScenarioGiven.d.ts +3 -1
  30. package/dist/testing/EventScenarioGiven.d.ts.map +1 -1
  31. package/dist/testing/EventScenarioGiven.js +4 -2
  32. package/dist/testing/EventScenarioGiven.js.map +1 -1
  33. package/dist/testing/EventScenarioGivenBuilder.d.ts +2 -1
  34. package/dist/testing/EventScenarioGivenBuilder.d.ts.map +1 -1
  35. package/dist/testing/EventScenarioGivenBuilder.js +1 -1
  36. package/dist/testing/EventScenarioGivenBuilder.js.map +1 -1
  37. package/dist/testing/EventScenarioSeed.d.ts +3 -2
  38. package/dist/testing/EventScenarioSeed.d.ts.map +1 -1
  39. package/dist/testing/EventScenarioSeed.js +2 -2
  40. package/dist/testing/EventScenarioSeed.js.map +1 -1
  41. package/dist/testing/EventScenarioWhen.d.ts +3 -1
  42. package/dist/testing/EventScenarioWhen.d.ts.map +1 -1
  43. package/dist/testing/EventScenarioWhen.js +5 -3
  44. package/dist/testing/EventScenarioWhen.js.map +1 -1
  45. package/dist/testing/EventScenarioWhenBuilder.d.ts +2 -1
  46. package/dist/testing/EventScenarioWhenBuilder.d.ts.map +1 -1
  47. package/dist/testing/EventScenarioWhenBuilder.js +1 -1
  48. package/dist/testing/EventScenarioWhenBuilder.js.map +1 -1
  49. package/dist/testing/InProcessEventSequence.d.ts +1 -2
  50. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  51. package/dist/testing/InProcessEventSequence.js +4 -22
  52. package/dist/testing/InProcessEventSequence.js.map +1 -1
  53. package/dist/testing/ReactorScenario.d.ts +3 -2
  54. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  55. package/dist/testing/ReactorScenario.js +5 -5
  56. package/dist/testing/ReactorScenario.js.map +1 -1
  57. package/dist/testing/ReadModelScenario.d.ts +2 -1
  58. package/dist/testing/ReadModelScenario.d.ts.map +1 -1
  59. package/dist/testing/ReadModelScenario.js +6 -1
  60. package/dist/testing/ReadModelScenario.js.map +1 -1
  61. package/dist/testing/ReadModelScenarioGiven.d.ts +3 -1
  62. package/dist/testing/ReadModelScenarioGiven.d.ts.map +1 -1
  63. package/dist/testing/ReadModelScenarioGiven.js +4 -2
  64. package/dist/testing/ReadModelScenarioGiven.js.map +1 -1
  65. package/dist/testing/ReadModelScenarioGivenBuilder.d.ts +2 -1
  66. package/dist/testing/ReadModelScenarioGivenBuilder.d.ts.map +1 -1
  67. package/dist/testing/ReadModelScenarioGivenBuilder.js +2 -2
  68. package/dist/testing/ReadModelScenarioGivenBuilder.js.map +1 -1
  69. package/dist/testing/singleAppendRoute.d.ts +9 -0
  70. package/dist/testing/singleAppendRoute.d.ts.map +1 -0
  71. package/dist/testing/singleAppendRoute.js +24 -0
  72. package/dist/testing/singleAppendRoute.js.map +1 -0
  73. package/dist/tsconfig.tsbuildinfo +1 -1
  74. package/eventSequences/EventSequence.ts +9 -9
  75. package/for_ChronicleOptions/when_configuring_telemetry/with_span_names.spec.ts +32 -0
  76. package/package.json +1 -1
  77. package/telemetry/ChronicleTelemetryOptions.ts +4 -1
  78. package/telemetry/for_ChronicleTelemetry/given/a_client.fixture.ts +47 -0
  79. package/telemetry/for_ChronicleTelemetry/given/a_span_name_registry.fixture.ts +33 -0
  80. package/telemetry/for_ChronicleTelemetry/when_exposing_names/with_legacy_compatibility.spec.ts +44 -0
  81. package/telemetry/for_ChronicleTelemetry/when_naming_spans/with_per_client_modes.spec.ts +138 -0
  82. package/telemetry/for_ChronicleTelemetry/when_recording_product_attributes/with_both_naming_modes.spec.ts +52 -0
  83. package/telemetry/observeOperation.ts +9 -1
  84. package/testing/EventScenario.ts +1 -1
  85. package/testing/EventScenarioGiven.ts +3 -2
  86. package/testing/EventScenarioGivenBuilder.ts +2 -1
  87. package/testing/EventScenarioSeed.ts +5 -4
  88. package/testing/EventScenarioWhen.ts +4 -3
  89. package/testing/EventScenarioWhenBuilder.ts +2 -1
  90. package/testing/InProcessEventSequence.ts +4 -23
  91. package/testing/ReactorScenario.ts +7 -7
  92. package/testing/ReadModelScenario.ts +8 -1
  93. package/testing/ReadModelScenarioGiven.ts +4 -2
  94. package/testing/ReadModelScenarioGivenBuilder.ts +3 -2
  95. package/testing/for_EventScenario/given/routed_scenario.fixture.ts +39 -0
  96. package/testing/for_EventScenario/when_using_routed_builders/with_routing_options.spec.ts +116 -0
  97. package/testing/for_ReactorScenario/when_using_routed_builders/with_routing_options.spec.ts +88 -0
  98. package/testing/for_ReadModelScenario/when_seeding_with_options/with_route_validation.spec.ts +42 -0
  99. package/testing/singleAppendRoute.ts +28 -0
@@ -0,0 +1,116 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { chai, describe, it, vi } from 'vitest';
5
+ import type { AppendOptions } from '../../../eventSequences/AppendOptions.js';
6
+ import { Claim, createEvents, rejection, route, routing } from '../given/routed_scenario.fixture.js';
7
+
8
+ chai.should();
9
+ describe('when seeding with routed builders', () => {
10
+ it('should record each setup route through single append without action results', async () => {
11
+ const scenario = createEvents();
12
+ await scenario.given.forEventSource('A', route).events(new Claim('Alpha'), new Claim('Beta'));
13
+ scenario.appendedEvents.map(event => routing(event.context)).should.deep.equal([route, route]);
14
+ scenario.results.should.have.lengthOf(0);
15
+ (await scenario.append('B', new Claim('Alpha'), route)).isSuccess.should.be.true;
16
+ (await scenario.append('B', new Claim('Beta'), route)).isSuccess.should.be.false;
17
+ });
18
+
19
+ it('should roll back routed setup and preserve claims in other scopes', async () => {
20
+ const scenario = createEvents();
21
+ await scenario.given.forEventSource('A', route).events(new Claim('Taken'));
22
+ await scenario.given.forEventSource('A').events(new Claim('DefaultKey'));
23
+ const before = scenario.appendedEvents;
24
+ await scenario.given.forEventSource('B', route).events(new Claim('Temporary'), new Claim('Taken')).then(
25
+ () => { throw new Error('Expected failed setup'); }, error => (error as Error).message.should.include('EventScenario given setup failed'));
26
+ scenario.appendedEvents.should.deep.equal(before);
27
+ scenario.results.should.have.lengthOf(0);
28
+ (await scenario.when.forEventSource('C', route).event(new Claim('Temporary'))).sequenceNumber.value.should.equal(2n);
29
+ (await scenario.when.forEventSource('D').event(new Claim('DefaultKey'))).isSuccess.should.be.false;
30
+ });
31
+ });
32
+
33
+ describe('when acting with routed builders', () => {
34
+ it('should preserve append metadata identically to direct atomic batches', async () => {
35
+ vi.useFakeTimers({ toFake: ['Date'] });
36
+ vi.setSystemTime(new Date('2026-01-02T03:04:05.000Z'));
37
+ try {
38
+ const scenario = createEvents();
39
+ const direct = createEvents();
40
+ const subject = 'CustomerSubject';
41
+ const correlationId = '11111111-2222-3333-4444-555555555555';
42
+ const occurred = new Date('2025-12-01T12:34:56.000Z');
43
+ const tags = ['Priority', 'Routed'];
44
+ await scenario.when.forEventSource('A', { ...route, subject, correlationId, occurred, tags }).events(new Claim('Alpha'), new Claim('Beta'));
45
+ await direct.appendMany('A', [new Claim('Alpha'), new Claim('Beta')], { ...route, subject, correlationId, occurred, tags });
46
+ scenario.appendedEvents.map(event => event.context).should.deep.equal(direct.appendedEvents.map(event => event.context));
47
+ } finally { vi.useRealTimers(); }
48
+ });
49
+
50
+ it('should use the route for single actions and atomic batches', async () => {
51
+ const scenario = createEvents();
52
+ (await scenario.when.forEventSource('A', route).event(new Claim('Alpha'))).isSuccess.should.be.true;
53
+ const results = await scenario.when.forEventSource('B', { ...route, streamId: 'East' }).events(new Claim('Alpha'), new Claim('Beta'));
54
+ results.every(result => result.isSuccess).should.be.true;
55
+ scenario.appendedEvents.map(event => routing(event.context)).should.deep.equal([route,
56
+ { ...route, streamId: 'East' }, { ...route, streamId: 'East' }]);
57
+ scenario.results.should.have.lengthOf(3);
58
+ });
59
+
60
+ it('should honor every scoped dimension and reject only the matching route', async () => {
61
+ const scenario = createEvents();
62
+ await scenario.given.forEventSource('A', route).events(new Claim('Taken'));
63
+ for (const dimension of ['sourceType', 'streamType', 'streamId'] as const) {
64
+ (await scenario.when.forEventSource(`B-${dimension}`, { ...route, [dimension]: 'Other' }).event(new Claim('Taken'))).isSuccess.should.be.true;
65
+ }
66
+ const before = scenario.appendedEvents;
67
+ const results = await scenario.when.forEventSource('C', route).events(new Claim('Temporary'), new Claim('Taken'));
68
+ results.every(result => !result.isSuccess).should.be.true;
69
+ scenario.appendedEvents.should.deep.equal(before);
70
+ (await scenario.when.forEventSource('D', route).event(new Claim('Temporary'))).sequenceNumber.value.should.equal(4n);
71
+ });
72
+
73
+ it('should retain omitted routing and resolve empty strings to exact defaults', async () => {
74
+ const scenario = createEvents();
75
+ await scenario.given.forEventSource('A').events(new Claim('Given'));
76
+ await scenario.given.forEventSource('B', { sourceType: '', streamType: '', streamId: '' }).events(new Claim('Empty'));
77
+ await scenario.when.forEventSource('C').event(new Claim('Single'));
78
+ await scenario.when.forEventSource('D', {}).events(new Claim('Batch'));
79
+ scenario.appendedEvents.map(event => routing(event.context)).should.deep.equal(Array(4).fill({ sourceType: 'Default', streamType: 'All', streamId: 'Default' }));
80
+ (await scenario.when.forEventSource('E', { streamId: '' }).event(new Claim('Given'))).isSuccess.should.be.false;
81
+ });
82
+ });
83
+
84
+ describe('when routed builders receive unsupported options', () => {
85
+ it('should reject identically to their append path without changing history, results or notifications', async () => {
86
+ const scenario = createEvents();
87
+ await scenario.given.forEventSource('A', route).events(new Claim('Taken'));
88
+ const before = scenario.appendedEvents;
89
+ const notifications = scenario.eventLog.appendOperations[Symbol.asyncIterator]();
90
+ const pending = notifications.next();
91
+ try {
92
+ const invalid: unknown[] = [null, [], { concurrencyScopes: {} }, { arbitrary: true }];
93
+ for (const dimension of ['sourceType', 'streamType', 'streamId'] as const) {
94
+ for (const value of [' ', 'a|b', 'é', '*', 'a:b', 'a\u0000b', 1, null]) invalid.push({ [dimension]: value });
95
+ }
96
+ for (const value of invalid) {
97
+ const options = value as AppendOptions;
98
+ const single = await rejection(() => scenario.append('B', new Claim('Other'), options));
99
+ (await rejection(() => scenario.given.forEventSource('B', options).events(new Claim('Other')))).message.should.equal(single.message);
100
+ (await rejection(() => scenario.when.forEventSource('B', options).event(new Claim('Other')))).message.should.equal(single.message);
101
+ const batch = await rejection(() => scenario.appendMany('B', [new Claim('Other')], options));
102
+ (await rejection(() => scenario.when.forEventSource('B', options).events(new Claim('Other')))).message.should.equal(batch.message);
103
+ }
104
+ const metadata = { tags: [] };
105
+ const single = await rejection(() => scenario.append('B', new Claim('Other'), metadata));
106
+ (await rejection(() => scenario.given.forEventSource('B', metadata).events(new Claim('Other')))).message.should.equal(single.message);
107
+ (await rejection(() => scenario.when.forEventSource('B', metadata).event(new Claim('Other')))).message.should.equal(single.message);
108
+ scenario.appendedEvents.should.deep.equal(before);
109
+ scenario.results.should.have.lengthOf(0);
110
+ (await scenario.eventLog.getNextSequenceNumber()).value.should.equal(1n);
111
+ const rejected = await scenario.when.forEventSource('B', route).event(new Claim('Taken'));
112
+ rejected.isSuccess.should.be.false;
113
+ (await pending).value![0].result.should.equal(rejected);
114
+ } finally { await notifications.return?.(); }
115
+ });
116
+ });
@@ -0,0 +1,88 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { chai, describe, it } from 'vitest';
5
+ import type { AppendOptions } from '../../../eventSequences/AppendOptions.js';
6
+ import { Claim, Effect, createEvents, createReactor, rejection, route, routing } from '../../for_EventScenario/given/routed_scenario.fixture.js';
7
+
8
+ chai.should();
9
+ describe('when delivering routed setup and action events', () => {
10
+ it('should preserve append metadata in handled and triggering contexts', async () => {
11
+ const scenario = createReactor();
12
+ const subject = 'CustomerSubject';
13
+ const correlationId = '11111111-2222-3333-4444-555555555555';
14
+ const occurred = new Date('2025-12-01T12:34:56.000Z');
15
+ const tags = ['Priority', 'Routed'];
16
+ await scenario.when.forEventSource('A', { ...route, subject, correlationId, occurred, tags }).events(new Claim('Alpha'), new Claim('Beta'));
17
+ const contexts = [...scenario.results.flatMap(result => result.handled), ...scenario.sideEffects.map(effect => effect.triggeringContext)];
18
+ contexts.map(context => ({ subject: context.subject, correlationId: context.correlationId, occurred: context.occurred,
19
+ tags: context.tags.map(tag => tag.value) })).should.deep.equal(Array(4).fill({ subject, correlationId, occurred, tags }));
20
+ });
21
+
22
+ it('should expose the recorded route in handled and triggering contexts', async () => {
23
+ const scenario = createReactor();
24
+ await scenario.given.forEventSource('A', route).events(new Claim('Given'));
25
+ await scenario.when.forEventSource('B', { ...route, streamId: 'East' }).events(new Claim('First'), new Claim('Second'));
26
+ const expected = [route, { ...route, streamId: 'East' }, { ...route, streamId: 'East' }];
27
+ scenario.appendedEvents.map(event => routing(event.context)).should.deep.equal(expected);
28
+ scenario.results.flatMap(result => result.handled.map(routing)).should.deep.equal(expected);
29
+ scenario.sideEffects.map(effect => routing(effect.triggeringContext)).should.deep.equal(expected);
30
+ scenario.sideEffects.map(effect => effect.triggeringContext).should.deep.equal(scenario.appendedEvents.map(event => event.context));
31
+ scenario.sideEffects.map(effect => effect.target.eventStreamId).should.deep.equal(['West', 'East', 'East']);
32
+ scenario.produced.map(effect => (effect as Effect).key).should.deep.equal([
33
+ 'Given:Customer/Orders/West', 'First:Customer/Orders/East', 'Second:Customer/Orders/East']);
34
+ });
35
+
36
+ it('should enforce scoped constraints before delivering any rejected action batch', async () => {
37
+ const scenario = createReactor();
38
+ await scenario.given.forEventSource('A', route).events(new Claim('Taken'));
39
+ await scenario.when.forEventSource('B', { ...route, streamId: 'East' }).events(new Claim('Taken'));
40
+ const before = scenario.appendedEvents;
41
+ await scenario.when.forEventSource('C', route).events(new Claim('Temporary'), new Claim('Taken')).then(
42
+ () => { throw new Error('Expected constraint rejection'); }, error => (error as Error).message.should.include('ReactorScenario action append failed'));
43
+ scenario.appendedEvents.should.deep.equal(before);
44
+ scenario.produced.should.have.lengthOf(2);
45
+ scenario.results.should.have.lengthOf(2);
46
+ await scenario.when.forEventSource('D', route).events(new Claim('Temporary'));
47
+ scenario.appendedEvents[2].context.sequenceNumber.should.equal(2n);
48
+ });
49
+
50
+ it('should roll back rejected routed setup before recording effects or delivery outcomes', async () => {
51
+ const scenario = createReactor();
52
+ await scenario.given.forEventSource('A', route).events(new Claim('Taken'));
53
+ await scenario.given.forEventSource('B', route).events(new Claim('Temporary'), new Claim('Taken')).then(
54
+ () => { throw new Error('Expected failed setup'); }, error => (error as Error).message.should.include('EventScenario given setup failed'));
55
+ scenario.appendedEvents.should.have.lengthOf(1);
56
+ scenario.produced.should.have.lengthOf(1);
57
+ scenario.results.should.have.lengthOf(1);
58
+ await scenario.when.forEventSource('C', route).events(new Claim('Temporary'));
59
+ scenario.appendedEvents[1].context.sequenceNumber.should.equal(1n);
60
+ });
61
+
62
+ it('should keep default routing for omitted and empty route options', async () => {
63
+ const scenario = createReactor();
64
+ await scenario.given.forEventSource('A').events(new Claim('Given'));
65
+ await scenario.when.forEventSource('B', { sourceType: '', streamType: '', streamId: '' }).events(new Claim('Action'));
66
+ scenario.results.flatMap(result => result.handled.map(routing)).should.deep.equal(Array(2).fill({ sourceType: 'Default', streamType: 'All', streamId: 'Default' }));
67
+ });
68
+ });
69
+
70
+ describe('when reactor builders receive unsupported routing', () => {
71
+ it('should reject through append validation without delivering or poisoning the next action', async () => {
72
+ const scenario = createReactor();
73
+ const direct = createEvents();
74
+ for (const value of [null, [], { sourceType: 1 }, { streamType: 'a|b' }, { streamId: ' ' }, { arbitrary: true }]) {
75
+ const options = value as AppendOptions;
76
+ const single = await rejection(() => direct.append('A', new Claim('Other'), options));
77
+ (await rejection(() => scenario.given.forEventSource('A', options).events(new Claim('Other')))).message.should.equal(single.message);
78
+ const batch = await rejection(() => direct.appendMany('A', [new Claim('Other')], options));
79
+ (await rejection(() => scenario.when.forEventSource('A', options).events(new Claim('Other')))).message.should.equal(batch.message);
80
+ }
81
+ scenario.appendedEvents.should.have.lengthOf(0);
82
+ scenario.results.should.have.lengthOf(0);
83
+ scenario.sideEffects.should.have.lengthOf(0);
84
+ await scenario.when.forEventSource('A', route).events(new Claim('Accepted'));
85
+ scenario.appendedEvents[0].context.sequenceNumber.should.equal(0n);
86
+ scenario.results[0].completed.should.be.true;
87
+ });
88
+ });
@@ -0,0 +1,42 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { chai, describe, it } from 'vitest';
5
+ import type { AppendOptions } from '../../../eventSequences/AppendOptions.js';
6
+ import { reducer } from '../../../reducers/reducer.js';
7
+ import { ReadModelScenario, UnsupportedProjectionOperation } from '../../index.js';
8
+ import { Claim, createEvents, rejection, route } from '../../for_EventScenario/given/routed_scenario.fixture.js';
9
+
10
+ chai.should();
11
+ class Count { claimed = 0; }
12
+ class Counter { claim(_event: Claim, current: Count | undefined): Count { return { claimed: (current?.claimed ?? 0) + 1 }; } }
13
+ reducer('BuilderRouteCounter', undefined, Count)(Counter);
14
+ const create = () => new ReadModelScenario(Count, { eventTypes: [Claim], reducers: [Counter], projections: [] });
15
+
16
+ describe('when read-model setup receives route options', () => {
17
+ it('should preserve default setup with omitted, explicit or empty defaults', async () => {
18
+ const scenario = create();
19
+ scenario.given.forEventSource('A').events(new Claim('First'));
20
+ scenario.given.forEventSource('A', { sourceType: '', streamType: '', streamId: '' }).events(new Claim('Second'));
21
+ scenario.given.forEventSource('A', { sourceType: 'Default', streamType: 'All', streamId: 'Default' }).events(new Claim('Third'));
22
+ (await scenario.instance)!.claimed.should.equal(3);
23
+ });
24
+
25
+ it('should reuse single-append rejection messages without collecting unsupported input', async () => {
26
+ const scenario = create();
27
+ const events = createEvents();
28
+ for (const value of [{ sourceType: ' ' }, { streamType: null }, { streamId: 1 }, { tags: [] }, { arbitrary: true }, null, []]) {
29
+ const options = value as AppendOptions;
30
+ const error = await rejection(() => events.append('A', new Claim('Other'), options));
31
+ (await rejection(() => scenario.given.forEventSource('A', options).events(new Claim('Other')))).message.should.equal(error.message);
32
+ }
33
+ ((await scenario.instance) === null).should.be.true;
34
+ });
35
+
36
+ it('should retain the unproven routed read-model boundary instead of simulating projection routing', async () => {
37
+ const scenario = create();
38
+ (() => scenario.given.forEventSource('A', route).events(new Claim('Other'))).should.throw(UnsupportedProjectionOperation, 'routed events');
39
+ scenario.given.forEventSource('A').events(new Claim('Accepted'));
40
+ (await scenario.instance)!.claimed.should.equal(1);
41
+ });
42
+ });
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { AppendOptions } from '../eventSequences/AppendOptions.js';
5
+ import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
6
+
7
+ /** The fixture-proven single-append route boundary, shared with read-model setup. */
8
+ export function singleAppendRoute(options: AppendOptions | undefined, artifact: string,
9
+ unsupported = (operation: string, artifact: string, reason: string) => new UnsupportedEventSequenceOperation(operation, artifact, reason)
10
+ ): { sourceType: string; streamType: string; streamId: string } {
11
+ if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) {
12
+ throw unsupported('append.options', artifact, 'Append options must be an object.');
13
+ }
14
+ if (options && (Reflect.ownKeys(options).some(key => !['sourceType', 'streamType', 'streamId'].includes(String(key))) ||
15
+ options.correlationId !== undefined || options.subject !== undefined || options.occurred !== undefined ||
16
+ options.eventSourceId !== undefined || options.concurrencyScope !== undefined || options.tags !== undefined ||
17
+ options.concurrencyScopes !== undefined)) {
18
+ throw unsupported('append.options', artifact, 'Only sourceType, streamType and streamId routing options are fixture-backed for single append.');
19
+ }
20
+ const route = { sourceType: options?.sourceType, streamType: options?.streamType, streamId: options?.streamId };
21
+ for (const [name, value] of Object.entries(route)) {
22
+ if (value !== undefined && (typeof value !== 'string' || !/^[A-Za-z0-9_-]*$/.test(value))) {
23
+ throw unsupported(`append.${name}`, String(value), 'Only simple routing identifiers or empty defaults are fixture-backed.');
24
+ }
25
+ }
26
+ // ts-proto omits empty strings; the append pipeline resolves omissions to these exact values.
27
+ return { sourceType: route.sourceType || 'Default', streamType: route.streamType || 'All', streamId: route.streamId || 'Default' };
28
+ }