@cratis/chronicle 6.25.0 → 6.26.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/dist/testing/InProcessConstraints.d.ts +1 -0
- package/dist/testing/InProcessConstraints.d.ts.map +1 -1
- package/dist/testing/InProcessConstraints.js +57 -12
- package/dist/testing/InProcessConstraints.js.map +1 -1
- package/package.json +1 -1
- package/testing/EventScenario.constraint-cycles.spec.ts +236 -0
- package/testing/EventScenario.constraints.spec.ts +4 -4
- package/testing/EventScenario.fixtures.spec.ts +2 -1
- package/testing/InProcessConstraints.ts +54 -11
|
@@ -8,6 +8,7 @@ export declare class InProcessConstraints {
|
|
|
8
8
|
private readonly _definitions;
|
|
9
9
|
private readonly _constrainedProperties;
|
|
10
10
|
private readonly _removalTypes;
|
|
11
|
+
private readonly _propertyRemovalTypes;
|
|
11
12
|
constructor(_definitions: ReadonlyMap<string, ConstraintCapture>);
|
|
12
13
|
isRemovalOnlyType(eventTypeId: string): boolean;
|
|
13
14
|
hasRemovalType(eventTypeId: string): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InProcessConstraints.d.ts","sourceRoot":"","sources":["../../testing/InProcessConstraints.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,IAAI,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AACvG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAEpF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAGpF,MAAM,MAAM,uBAAuB,GAAG,4BAA4B,CAAC;AAiBnE,6FAA6F;AAC7F,qBAAa,oBAAoB;
|
|
1
|
+
{"version":3,"file":"InProcessConstraints.d.ts","sourceRoot":"","sources":["../../testing/InProcessConstraints.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,IAAI,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AACvG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAEpF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAGpF,MAAM,MAAM,uBAAuB,GAAG,4BAA4B,CAAC;AAiBnE,6FAA6F;AAC7F,qBAAa,oBAAoB;IAKjB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAJzC,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAkC;IACzE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAqB;IAE3D,YAA6B,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAsEhF;IAED,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE9C;IAED,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAgD;IAE5F,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEpE;IAED,yGAAyG;IACzG,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,EAAE,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,uBAAuB,EAAE,CA4FzG;IAED,cAAc,CAAC,SAAS,EAAE,mBAAmB,GAAG,mBAAmB,CAElE;IAED,OAAO,CAAC,WAAW;CAGtB"}
|
|
@@ -21,6 +21,7 @@ export class InProcessConstraints {
|
|
|
21
21
|
_definitions;
|
|
22
22
|
_constrainedProperties = new Map();
|
|
23
23
|
_removalTypes = new Set();
|
|
24
|
+
_propertyRemovalTypes = new Set();
|
|
24
25
|
constructor(_definitions) {
|
|
25
26
|
this._definitions = _definitions;
|
|
26
27
|
const coveredTypes = new Map();
|
|
@@ -48,6 +49,7 @@ export class InProcessConstraints {
|
|
|
48
49
|
}
|
|
49
50
|
removalOwners.set(id, name);
|
|
50
51
|
this._removalTypes.add(id);
|
|
52
|
+
this._propertyRemovalTypes.add(id);
|
|
51
53
|
}
|
|
52
54
|
for (const entry of unique.eventDefinitions) {
|
|
53
55
|
const properties = this._constrainedProperties.get(entry.eventTypeId) ?? new Set();
|
|
@@ -57,9 +59,23 @@ export class InProcessConstraints {
|
|
|
57
59
|
}
|
|
58
60
|
else if (capture.uniqueEventType) {
|
|
59
61
|
const unique = capture.uniqueEventType;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
const ids = unique.eventTypeIds ?? [unique.eventTypeId];
|
|
63
|
+
const removals = unique.removedWithEventTypeIds ?? [];
|
|
64
|
+
const shared = ids.filter(id => removals.includes(id)).length;
|
|
65
|
+
// Exactly the shapes installed by the pinned fixtures: one covered type without removal
|
|
66
|
+
// (constraints.json), two covered types with two separate removers
|
|
67
|
+
// (constraints-event-type-siblings.json), and three covered types with three removers of
|
|
68
|
+
// which one is also covered (constraints-event-type-cycles.json).
|
|
69
|
+
if (!(ids.length === 1 && removals.length === 0) &&
|
|
70
|
+
!(ids.length === 2 && removals.length === 2 && shared === 0) &&
|
|
71
|
+
!(ids.length === 3 && removals.length === 3 && shared === 1)) {
|
|
72
|
+
throw this.unsupported(name, 'This unique event type set and removal combination is not fixture-backed.');
|
|
62
73
|
}
|
|
74
|
+
// Each cycle fixture installs its definition alone; interaction with other definitions is unproven.
|
|
75
|
+
if (removals.length && _definitions.size !== 1) {
|
|
76
|
+
throw this.unsupported(name, 'Unique event cycles alongside other definitions are not fixture-backed.');
|
|
77
|
+
}
|
|
78
|
+
removals.forEach(id => this._removalTypes.add(id));
|
|
63
79
|
}
|
|
64
80
|
else {
|
|
65
81
|
throw this.unsupported(name, 'Unknown constraint definition.');
|
|
@@ -84,7 +100,7 @@ export class InProcessConstraints {
|
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
102
|
isRemovalOnlyType(eventTypeId) {
|
|
87
|
-
return this.
|
|
103
|
+
return this._propertyRemovalTypes.has(eventTypeId) && !this._constrainedProperties.has(eventTypeId);
|
|
88
104
|
}
|
|
89
105
|
hasRemovalType(eventTypeId) { return this._removalTypes.has(eventTypeId); }
|
|
90
106
|
isConstrainedProperty(eventTypeId, property) {
|
|
@@ -121,10 +137,11 @@ export class InProcessConstraints {
|
|
|
121
137
|
owners.set(name, claims);
|
|
122
138
|
}
|
|
123
139
|
const stagedKeys = new Map();
|
|
124
|
-
const
|
|
140
|
+
const stagedCycles = new Map();
|
|
125
141
|
for (const event of incoming) {
|
|
126
142
|
const type = event.eventType.id.value;
|
|
127
143
|
const source = event.context.eventSourceId;
|
|
144
|
+
const violationCount = violations.length;
|
|
128
145
|
for (const [name, definition] of this._definitions) {
|
|
129
146
|
const property = definition.uniqueConstraint?.eventDefinitions.find(entry => entry.eventTypeId === type)?.properties[0];
|
|
130
147
|
if (property !== undefined) {
|
|
@@ -147,17 +164,45 @@ export class InProcessConstraints {
|
|
|
147
164
|
}
|
|
148
165
|
const uniqueType = definition.uniqueEventType;
|
|
149
166
|
if (uniqueType && (uniqueType.eventTypeIds ?? [uniqueType.eventTypeId]).includes(type)) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
const covered = uniqueType.eventTypeIds ?? [uniqueType.eventTypeId];
|
|
168
|
+
const removals = uniqueType.removedWithEventTypeIds ?? [];
|
|
169
|
+
// Durable answer: the earliest covered event after the latest removal for this source.
|
|
170
|
+
let first;
|
|
171
|
+
for (const prior of history) {
|
|
172
|
+
if (prior.context.eventSourceId !== source)
|
|
173
|
+
continue;
|
|
174
|
+
const priorType = prior.eventType.id.value;
|
|
175
|
+
if (removals.includes(priorType))
|
|
176
|
+
first = undefined;
|
|
177
|
+
else if (!first && covered.includes(priorType))
|
|
178
|
+
first = prior;
|
|
179
|
+
}
|
|
180
|
+
const cycles = stagedCycles.get(name) ?? new Map();
|
|
181
|
+
const state = cycles.get(source);
|
|
182
|
+
// A cycle state recorded earlier in this append overrides durable history.
|
|
183
|
+
if (state === 'open' || state === undefined && first) {
|
|
184
|
+
// Durable storage supplies the sequence even when a staged claim causes rejection.
|
|
185
|
+
const sequence = first?.context.sequenceNumber.toString() ?? unavailable;
|
|
186
|
+
violations.push({ EventTypeId: type, SequenceNumber: BigInt(sequence), ConstraintType: 2,
|
|
155
187
|
ConstraintName: name,
|
|
156
|
-
Message: `Event '${type}' violated a unique event type constraint on sequence number ${
|
|
188
|
+
Message: `Event '${type}' violated a unique event type constraint on sequence number ${sequence}`,
|
|
157
189
|
Details: {} });
|
|
158
190
|
}
|
|
159
|
-
else
|
|
160
|
-
|
|
191
|
+
else {
|
|
192
|
+
cycles.set(source, 'open');
|
|
193
|
+
stagedCycles.set(name, cycles);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Kernel batch observers release a cycle only after the entire event validates.
|
|
198
|
+
// A covered-and-removal event first attempts its claim, then releases it.
|
|
199
|
+
if (violations.length === violationCount) {
|
|
200
|
+
for (const [name, definition] of this._definitions) {
|
|
201
|
+
if (definition.uniqueEventType?.removedWithEventTypeIds?.includes(type)) {
|
|
202
|
+
const cycles = stagedCycles.get(name) ?? new Map();
|
|
203
|
+
cycles.set(source, 'released');
|
|
204
|
+
stagedCycles.set(name, cycles);
|
|
205
|
+
}
|
|
161
206
|
}
|
|
162
207
|
}
|
|
163
208
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InProcessConstraints.js","sourceRoot":"","sources":["../../testing/InProcessConstraints.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,qGAAqG;AAErG,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC,OAAO,EAAE,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAEhF,OAAO,EAAE,iCAAiC,EAAE,MAAM,wCAAwC,CAAC;AAI3F,MAAM,WAAW,GAAG,sBAAsB,CAAC;AAC3C,oGAAoG;AACpG,MAAM,YAAY,GAAG,iCAAiC,CAAC;AAEvD,SAAS,eAAe,CAAC,KAAc,EAAE,IAAY;IACjD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACxE,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IAChE,MAAM,IAAI,iCAAiC,CAAC,uBAAuB,EAAE,IAAI,EACrE,mEAAmE,CAAC,CAAC;AAC7E,CAAC;AAED,SAAS,OAAO,CAAC,KAAa;IAC1B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5D,CAAC;AAED,6FAA6F;AAC7F,MAAM,OAAO,oBAAoB;
|
|
1
|
+
{"version":3,"file":"InProcessConstraints.js","sourceRoot":"","sources":["../../testing/InProcessConstraints.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,236 @@
|
|
|
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 'reflect-metadata';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { chai, describe, it, vi } from 'vitest';
|
|
7
|
+
import { field } from '@cratis/fundamentals';
|
|
8
|
+
import { eventType, getEventTypeMetadata } from '../events/eventTypeDecorator.js';
|
|
9
|
+
import { constraint } from '../events/constraints/constraint.js';
|
|
10
|
+
import { compileConstraints } from '../events/constraints/Constraints.js';
|
|
11
|
+
import type { IConstraint } from '../events/constraints/IConstraint.js';
|
|
12
|
+
import type { IConstraintBuilder } from '../events/constraints/IConstraintBuilder.js';
|
|
13
|
+
import { removeConstraint } from '../events/constraints/removeConstraint.js';
|
|
14
|
+
import { EventScenario, UnsupportedEventSequenceOperation } from './index.js';
|
|
15
|
+
import { InProcessConstraints } from './InProcessConstraints.js';
|
|
16
|
+
import { ReactorScenario } from './ReactorScenario.js';
|
|
17
|
+
import { reactor } from '../reactors/reactor.js';
|
|
18
|
+
|
|
19
|
+
chai.should();
|
|
20
|
+
class OracleCycleFirst { @field(String) label: string; constructor(label: string) { this.label = label; } }
|
|
21
|
+
eventType('OracleCycleFirst')(OracleCycleFirst);
|
|
22
|
+
class OracleCycleSibling { @field(String) label: string; constructor(label: string) { this.label = label; } }
|
|
23
|
+
eventType('OracleCycleSibling')(OracleCycleSibling);
|
|
24
|
+
class OracleCycleRemoved { @field(String) label: string; constructor(label: string) { this.label = label; } }
|
|
25
|
+
eventType('OracleCycleRemoved')(OracleCycleRemoved);
|
|
26
|
+
removeConstraint('OracleCycle')(OracleCycleRemoved);
|
|
27
|
+
class OracleCycleExpired { @field(String) label: string; constructor(label: string) { this.label = label; } }
|
|
28
|
+
eventType('OracleCycleExpired')(OracleCycleExpired);
|
|
29
|
+
removeConstraint('OracleCycle')(OracleCycleExpired);
|
|
30
|
+
class OracleCycleRenewed { @field(String) label: string; constructor(label: string) { this.label = label; } }
|
|
31
|
+
eventType('OracleCycleRenewed')(OracleCycleRenewed);
|
|
32
|
+
removeConstraint('OracleCycle')(OracleCycleRenewed);
|
|
33
|
+
// Same-named uniqueFor declarations merge into one definition covering the whole set.
|
|
34
|
+
class CycleFirst implements IConstraint {
|
|
35
|
+
define(builder: IConstraintBuilder) { builder.uniqueFor(OracleCycleFirst, 'Cycle occupied', 'OracleCycle'); }
|
|
36
|
+
}
|
|
37
|
+
constraint('CycleFirst')(CycleFirst);
|
|
38
|
+
class CycleSibling implements IConstraint {
|
|
39
|
+
define(builder: IConstraintBuilder) { builder.uniqueFor(OracleCycleSibling, undefined, 'OracleCycle'); }
|
|
40
|
+
}
|
|
41
|
+
constraint('CycleSibling')(CycleSibling);
|
|
42
|
+
class CycleRenewed implements IConstraint {
|
|
43
|
+
define(builder: IConstraintBuilder) { builder.uniqueFor(OracleCycleRenewed, undefined, 'OracleCycle'); }
|
|
44
|
+
}
|
|
45
|
+
constraint('CycleRenewed')(CycleRenewed);
|
|
46
|
+
|
|
47
|
+
const aliases = { first: OracleCycleFirst, sibling: OracleCycleSibling, cycleRemoved: OracleCycleRemoved,
|
|
48
|
+
cycleExpired: OracleCycleExpired, renewed: OracleCycleRenewed };
|
|
49
|
+
type Alias = keyof typeof aliases;
|
|
50
|
+
type Input = { source: string; type: Alias; value: string };
|
|
51
|
+
type History = { sequence: string; source: string; sourceType: string; streamType: string; streamId: string;
|
|
52
|
+
type: string; content: Record<string, string>; hash: string };
|
|
53
|
+
type Violation = { EventTypeId: string; SequenceNumber: string; ConstraintType: number; ConstraintName: string;
|
|
54
|
+
Message: string; Details: Record<string, string> };
|
|
55
|
+
type Outcome = { success: boolean; sequences: string[]; violations: Array<{ id: string; message: string; details: Record<string, string> }>;
|
|
56
|
+
wireViolations: Violation[]; errors: string[]; history: History[]; next: string };
|
|
57
|
+
type Fixture = { eventSchemas: Record<string, { eventTypeId: string; properties: Record<string, string> }>;
|
|
58
|
+
constraintDefinitions: Array<{ kind: string; name: string; eventTypes: string[]; removedWithEventTypeIds: string[]; message: string }>;
|
|
59
|
+
isolatedConstraintOperations: Array<{ mode: 'single' | 'batch'; events: Input[] }>;
|
|
60
|
+
expected: { outcomes: Outcome[] } };
|
|
61
|
+
const aliasById = Object.fromEntries(Object.entries(aliases).map(([alias, type]) => [getEventTypeMetadata(type)!.eventType.id.value, alias]));
|
|
62
|
+
const makeEvent = ({ type, value }: Input): object => new aliases[type](value);
|
|
63
|
+
const historyOf = (scenario: EventScenario): History[] => scenario.appendedEvents.map(entry => ({
|
|
64
|
+
sequence: entry.context.sequenceNumber.toString(), source: entry.context.eventSourceId,
|
|
65
|
+
sourceType: entry.context.eventSourceType, streamType: entry.context.eventStreamType, streamId: entry.context.eventStreamId,
|
|
66
|
+
type: entry.eventType.id.value, content: entry.content as Record<string, string>, hash: entry.context.hash
|
|
67
|
+
}));
|
|
68
|
+
const unsupported = async (action: () => unknown, operation: string, reason: string) => {
|
|
69
|
+
await Promise.resolve().then(action).then(() => { throw new Error('Expected rejection'); }, error => {
|
|
70
|
+
(error instanceof UnsupportedEventSequenceOperation).should.be.true;
|
|
71
|
+
(error as Error).message.should.include(`UnsupportedEventSequenceOperation: ${operation}:`);
|
|
72
|
+
(error as Error).message.should.include(reason);
|
|
73
|
+
(error as Error).message.should.include('Use a kernel-backed test.');
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const siblingTypes = [OracleCycleFirst, OracleCycleSibling, OracleCycleRemoved, OracleCycleExpired];
|
|
77
|
+
const cycleTypes = [...siblingTypes, OracleCycleRenewed];
|
|
78
|
+
const shapeReason = 'This unique event type set and removal combination is not fixture-backed.';
|
|
79
|
+
|
|
80
|
+
describe('fixture-backed unique event type cycles', () => {
|
|
81
|
+
for (const [name, selected, constraints] of [
|
|
82
|
+
['constraints-event-type-siblings', siblingTypes, [CycleFirst, CycleSibling]],
|
|
83
|
+
['constraints-event-type-cycles', cycleTypes, [CycleFirst, CycleSibling, CycleRenewed]]
|
|
84
|
+
] as const) {
|
|
85
|
+
it(`matches the pinned kernel ${name}: raw and mapped violations, cycles, hashes and rollback`, async () => {
|
|
86
|
+
const fixture = JSON.parse(readFileSync(new URL(`./fixtures/${name}.json`, import.meta.url), 'utf8')) as Fixture;
|
|
87
|
+
Object.fromEntries(selected.map(type => {
|
|
88
|
+
const metadata = getEventTypeMetadata(type)!;
|
|
89
|
+
return [aliasById[metadata.eventType.id.value], { eventTypeId: metadata.eventType.id.value,
|
|
90
|
+
properties: Object.fromEntries(Object.entries(metadata.schema.properties ?? {}).map(([key, property]) => [key, property.type])) }];
|
|
91
|
+
})).should.deep.equal(fixture.eventSchemas);
|
|
92
|
+
const compiled = compileConstraints({ eventTypes: [...selected], constraints: [...constraints] });
|
|
93
|
+
[...compiled].map(([key, capture]) => ({ kind: 'uniqueEventType', name: key,
|
|
94
|
+
eventTypes: capture.uniqueEventType!.eventTypeIds!.map(id => aliasById[id]),
|
|
95
|
+
removedWithEventTypeIds: capture.uniqueEventType!.removedWithEventTypeIds!.map(id => aliasById[id]),
|
|
96
|
+
message: capture.uniqueEventType!.message ?? ''
|
|
97
|
+
})).should.deep.equal(fixture.constraintDefinitions);
|
|
98
|
+
fixture.isolatedConstraintOperations.length.should.be.greaterThan(0);
|
|
99
|
+
fixture.expected.outcomes.length.should.equal(fixture.isolatedConstraintOperations.length);
|
|
100
|
+
fixture.expected.outcomes.some(outcome => !outcome.success).should.be.true;
|
|
101
|
+
const scenario = new EventScenario({ artifacts: { eventTypes: [...selected], constraints: [...constraints] } });
|
|
102
|
+
const notifications = scenario.eventSequence.appendOperations[Symbol.asyncIterator]();
|
|
103
|
+
const validate = vi.spyOn(InProcessConstraints.prototype, 'validate');
|
|
104
|
+
let count = 0;
|
|
105
|
+
try {
|
|
106
|
+
for (const [index, operation] of fixture.isolatedConstraintOperations.entries()) {
|
|
107
|
+
const expected = fixture.expected.outcomes[index];
|
|
108
|
+
const before = historyOf(scenario);
|
|
109
|
+
const calls = validate.mock.calls.length;
|
|
110
|
+
const nextNotification = notifications.next();
|
|
111
|
+
const results = operation.mode === 'single'
|
|
112
|
+
? [await scenario.append(operation.events[0].source, makeEvent(operation.events[0]))]
|
|
113
|
+
: await scenario.appendMany(operation.events.map(entry => ({ eventSourceId: entry.source, event: makeEvent(entry) })));
|
|
114
|
+
const notification = (await nextNotification).value!;
|
|
115
|
+
validate.mock.calls.length.should.equal(calls + 1);
|
|
116
|
+
const raw = validate.mock.results.at(-1)?.value as ReturnType<InProcessConstraints['validate']>;
|
|
117
|
+
raw.map(({ SequenceNumber, ...violation }) => ({ ...violation, SequenceNumber: SequenceNumber.toString() }))
|
|
118
|
+
.should.deep.equal(expected.wireViolations);
|
|
119
|
+
notification.length.should.equal(operation.events.length);
|
|
120
|
+
notification.every(item => item.result.isSuccess === expected.success).should.be.true;
|
|
121
|
+
count += operation.events.length;
|
|
122
|
+
scenario.results.length.should.equal(count);
|
|
123
|
+
results.length.should.equal(operation.events.length);
|
|
124
|
+
for (const [position, result] of results.entries()) {
|
|
125
|
+
result.isSuccess.should.equal(expected.success);
|
|
126
|
+
result.sequenceNumber.value.toString().should.equal(expected.success ? expected.sequences[position] : '0');
|
|
127
|
+
result.constraintViolations.map(item => ({ id: item.constraintId, message: item.message, details: item.details }))
|
|
128
|
+
.should.deep.equal(expected.violations);
|
|
129
|
+
result.errors.map(item => item.message).should.deep.equal(expected.errors);
|
|
130
|
+
}
|
|
131
|
+
if (!expected.success) historyOf(scenario).should.deep.equal(before);
|
|
132
|
+
historyOf(scenario).should.deep.equal(expected.history);
|
|
133
|
+
(await scenario.eventSequence.getNextSequenceNumber()).value.toString().should.equal(expected.next);
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
validate.mockRestore();
|
|
137
|
+
await notifications.return?.();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
it('rolls back a release and reclaim in a failing setup call without results or notifications', async () => {
|
|
143
|
+
const scenario = new EventScenario({ artifacts: { eventTypes: siblingTypes, constraints: [CycleFirst, CycleSibling] } });
|
|
144
|
+
const notifications = scenario.eventSequence.appendOperations[Symbol.asyncIterator]();
|
|
145
|
+
try {
|
|
146
|
+
const first = notifications.next();
|
|
147
|
+
await scenario.given.forEventSource('A').events(new OracleCycleFirst('claim'));
|
|
148
|
+
(await first).value!.length.should.equal(1);
|
|
149
|
+
const before = historyOf(scenario);
|
|
150
|
+
await scenario.given.forEventSource('A').events(new OracleCycleRemoved('release'), new OracleCycleFirst('reopen'),
|
|
151
|
+
new OracleCycleSibling('blocked'))
|
|
152
|
+
.then(() => { throw new Error('Invalid setup succeeded'); }, error => {
|
|
153
|
+
(error as Error).message.should.include('EventScenario given setup failed');
|
|
154
|
+
});
|
|
155
|
+
historyOf(scenario).should.deep.equal(before);
|
|
156
|
+
scenario.results.length.should.equal(0);
|
|
157
|
+
(await scenario.eventSequence.getNextSequenceNumber()).value.should.equal(1n);
|
|
158
|
+
const next = notifications.next();
|
|
159
|
+
const blocked = await scenario.append('A', new OracleCycleSibling('still held'));
|
|
160
|
+
blocked.constraintViolations.map(item => item.constraintId).should.deep.equal(['OracleCycle']);
|
|
161
|
+
(await next).value!.map(item => item.event.eventType.id.value).should.deep.equal(['OracleCycleSibling']);
|
|
162
|
+
} finally { await notifications.return?.(); }
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('delivers only committed claim and release events to a reactor', async () => {
|
|
166
|
+
const calls: string[] = [];
|
|
167
|
+
@reactor('cycle-reactor')
|
|
168
|
+
class CycleReactor {
|
|
169
|
+
oracleCycleFirst(event: OracleCycleFirst) { calls.push(event.label); }
|
|
170
|
+
oracleCycleSibling(event: OracleCycleSibling) { calls.push(event.label); }
|
|
171
|
+
oracleCycleExpired(event: OracleCycleExpired) { calls.push(event.label); }
|
|
172
|
+
}
|
|
173
|
+
const subject = new ReactorScenario(CycleReactor,
|
|
174
|
+
{ artifacts: { eventTypes: siblingTypes, constraints: [CycleFirst, CycleSibling] } });
|
|
175
|
+
await subject.when.forEventSource('A').events(new OracleCycleFirst('claim'));
|
|
176
|
+
await subject.when.forEventSource('A').events(new OracleCycleSibling('blocked'))
|
|
177
|
+
.then(() => { throw new Error('Occupied cycle delivered'); }, error => {
|
|
178
|
+
(error as Error).message.should.include('ReactorScenario action append failed');
|
|
179
|
+
});
|
|
180
|
+
calls.should.deep.equal(['claim']);
|
|
181
|
+
subject.results.length.should.equal(1);
|
|
182
|
+
await subject.when.forEventSource('A').events(new OracleCycleExpired('release'), new OracleCycleSibling('reclaim'));
|
|
183
|
+
calls.should.deep.equal(['claim', 'release', 'reclaim']);
|
|
184
|
+
subject.results.length.should.equal(2);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('rejects unsupported input in a batch and leaves history and results unchanged', async () => {
|
|
188
|
+
const scenario = new EventScenario({ artifacts: { eventTypes: siblingTypes, constraints: [CycleFirst, CycleSibling] } });
|
|
189
|
+
(await scenario.append('A', new OracleCycleFirst('claim'))).isSuccess.should.be.true;
|
|
190
|
+
const before = historyOf(scenario);
|
|
191
|
+
const results = scenario.results;
|
|
192
|
+
await unsupported(() => scenario.appendMany([{ eventSourceId: 'A', event: new OracleCycleExpired('release') },
|
|
193
|
+
{ eventSourceId: 'B!', event: new OracleCycleFirst('claim') }]), 'appendMany.source (B!)',
|
|
194
|
+
'Only simple source identifiers are fixture-backed.');
|
|
195
|
+
historyOf(scenario).should.deep.equal(before);
|
|
196
|
+
scenario.results.should.deep.equal(results);
|
|
197
|
+
const blocked = await scenario.append('A', new OracleCycleSibling('still held'));
|
|
198
|
+
blocked.isSuccess.should.be.false;
|
|
199
|
+
blocked.constraintViolations.map(item => item.message).should.deep.equal(['Cycle occupied']);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('rejects covered and removal type shapes that no fixture installs, at the shape check', async () => {
|
|
203
|
+
await unsupported(() => new EventScenario({ artifacts: {
|
|
204
|
+
eventTypes: [OracleCycleFirst, OracleCycleSibling, OracleCycleRemoved], constraints: [CycleFirst, CycleSibling]
|
|
205
|
+
} }), 'artifacts.constraints (OracleCycle)', shapeReason);
|
|
206
|
+
await unsupported(() => new EventScenario({ artifacts: {
|
|
207
|
+
eventTypes: [OracleCycleFirst, OracleCycleRemoved, OracleCycleRenewed], constraints: [CycleFirst, CycleRenewed]
|
|
208
|
+
} }), 'artifacts.constraints (OracleCycle)', shapeReason);
|
|
209
|
+
await unsupported(() => new EventScenario({ artifacts: {
|
|
210
|
+
eventTypes: [OracleCycleFirst, OracleCycleSibling, OracleCycleRemoved, OracleCycleRenewed],
|
|
211
|
+
constraints: [CycleFirst, CycleSibling, CycleRenewed]
|
|
212
|
+
} }), 'artifacts.constraints (OracleCycle)', shapeReason);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('rejects a fixture-shaped cycle installed alongside another definition', async () => {
|
|
216
|
+
class OtherOnce { @field(String) label = 'other'; }
|
|
217
|
+
eventType('OracleCycleOtherOnce')(OtherOnce);
|
|
218
|
+
class OtherOnceConstraint implements IConstraint {
|
|
219
|
+
define(builder: IConstraintBuilder) { builder.uniqueFor(OtherOnce, undefined, 'OracleCycleOther'); }
|
|
220
|
+
}
|
|
221
|
+
constraint('OracleCycleOther')(OtherOnceConstraint);
|
|
222
|
+
await unsupported(() => new EventScenario({ artifacts: {
|
|
223
|
+
eventTypes: [...siblingTypes, OtherOnce], constraints: [CycleFirst, CycleSibling, OtherOnceConstraint]
|
|
224
|
+
} }), 'artifacts.constraints (OracleCycle)', 'Unique event cycles alongside other definitions are not fixture-backed.');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('rejects a fieldless event-type remover at the schema check', async () => {
|
|
228
|
+
class FieldlessRemoval {}
|
|
229
|
+
eventType('OracleCycleFieldless')(FieldlessRemoval);
|
|
230
|
+
removeConstraint('OracleCycle')(FieldlessRemoval);
|
|
231
|
+
await unsupported(() => new EventScenario({ artifacts: {
|
|
232
|
+
eventTypes: [OracleCycleFirst, OracleCycleSibling, OracleCycleRemoved, FieldlessRemoval],
|
|
233
|
+
constraints: [CycleFirst, CycleSibling]
|
|
234
|
+
} }), 'artifacts.eventTypes.schema (FieldlessRemoval)', 'Only flat, unclassified string/boolean fields are fixture-backed.');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -172,7 +172,7 @@ describe('fixture-backed unscoped constraints', () => {
|
|
|
172
172
|
const production = compileConstraints({ eventTypes: [GlobalOnce, SelectedOnce], constraints: [] });
|
|
173
173
|
production.get('ScenarioSharedOnce')!.uniqueEventType!.eventTypeIds.should.deep.equal(['GlobalOnce', 'SelectedOnce']);
|
|
174
174
|
unsupported(() => new EventScenario({ artifacts: { eventTypes: [SelectedOnce] } }), 'artifacts.constraints')
|
|
175
|
-
.message.should.include('
|
|
175
|
+
.message.should.include('This unique event type set and removal combination is not fixture-backed.');
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
it('rejects removal of a selected unique-event-type constraint at construction', () => {
|
|
@@ -183,8 +183,8 @@ describe('fixture-backed unscoped constraints', () => {
|
|
|
183
183
|
eventType('OnceNameRemoval')(Removal);
|
|
184
184
|
removeConstraint('OnceName')(Removal);
|
|
185
185
|
const message = unsupported(() => new EventScenario({ artifacts: { eventTypes: [Once, Removal] } }), 'artifacts.constraints').message;
|
|
186
|
-
message.should.include('
|
|
187
|
-
message.should.include('
|
|
186
|
+
message.should.include('OnceName');
|
|
187
|
+
message.should.include('This unique event type set and removal combination is not fixture-backed.');
|
|
188
188
|
});
|
|
189
189
|
|
|
190
190
|
it('rejects a global removal decorator targeting a selected constraint', () => {
|
|
@@ -374,7 +374,7 @@ describe('fixture-backed unscoped constraints', () => {
|
|
|
374
374
|
eventType('AnotherOnce')(AnotherOnce);
|
|
375
375
|
unique('OracleOnce')(AnotherOnce);
|
|
376
376
|
unsupported(() => new EventScenario({ artifacts: { eventTypes: [...types, AnotherOnce], constraints: [OracleFluentOnceConstraint] } }), 'artifacts.constraints')
|
|
377
|
-
.message.should.include('
|
|
377
|
+
.message.should.include('This unique event type set and removal combination is not fixture-backed.');
|
|
378
378
|
const subject = scenario();
|
|
379
379
|
await subject.append('A', new OracleKeyClaimed('Alpha'));
|
|
380
380
|
await subject.append('B', new OracleKeyClaimed('beta'));
|
|
@@ -57,7 +57,8 @@ interface Fixture {
|
|
|
57
57
|
const directory = new URL('./fixtures/', import.meta.url);
|
|
58
58
|
const fixtures = readdirSync(directory).filter(name => name.endsWith('.json') &&
|
|
59
59
|
!['batches.json', 'batch-omitted-routes.json', 'batch-rollback.json', 'builders.json', 'constraints.json', 'constraints-isolation.json', 'constraints-key-domain.json',
|
|
60
|
-
'constraints-property-lifecycle.json', 'constraints-property-covered-removal.json'
|
|
60
|
+
'constraints-property-lifecycle.json', 'constraints-property-covered-removal.json', 'constraints-event-type-cycles.json',
|
|
61
|
+
'constraints-event-type-siblings.json'].includes(name)).map(name => ({
|
|
61
62
|
name, fixture: JSON.parse(readFileSync(new URL(name, directory), 'utf8')) as Fixture
|
|
62
63
|
}));
|
|
63
64
|
const artifacts = { eventTypes: [OracleEventRecorded, AlternateRecorded, BoundaryRecorded], constraints: [] };
|
|
@@ -30,6 +30,7 @@ function keyHash(value: string): string {
|
|
|
30
30
|
export class InProcessConstraints {
|
|
31
31
|
private readonly _constrainedProperties = new Map<string, Set<string>>();
|
|
32
32
|
private readonly _removalTypes = new Set<string>();
|
|
33
|
+
private readonly _propertyRemovalTypes = new Set<string>();
|
|
33
34
|
|
|
34
35
|
constructor(private readonly _definitions: ReadonlyMap<string, ConstraintCapture>) {
|
|
35
36
|
const coveredTypes = new Map<string, string>();
|
|
@@ -56,6 +57,7 @@ export class InProcessConstraints {
|
|
|
56
57
|
}
|
|
57
58
|
removalOwners.set(id, name);
|
|
58
59
|
this._removalTypes.add(id);
|
|
60
|
+
this._propertyRemovalTypes.add(id);
|
|
59
61
|
}
|
|
60
62
|
for (const entry of unique.eventDefinitions) {
|
|
61
63
|
const properties = this._constrainedProperties.get(entry.eventTypeId) ?? new Set<string>();
|
|
@@ -64,9 +66,23 @@ export class InProcessConstraints {
|
|
|
64
66
|
}
|
|
65
67
|
} else if (capture.uniqueEventType) {
|
|
66
68
|
const unique = capture.uniqueEventType;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
const ids = unique.eventTypeIds ?? [unique.eventTypeId];
|
|
70
|
+
const removals = unique.removedWithEventTypeIds ?? [];
|
|
71
|
+
const shared = ids.filter(id => removals.includes(id)).length;
|
|
72
|
+
// Exactly the shapes installed by the pinned fixtures: one covered type without removal
|
|
73
|
+
// (constraints.json), two covered types with two separate removers
|
|
74
|
+
// (constraints-event-type-siblings.json), and three covered types with three removers of
|
|
75
|
+
// which one is also covered (constraints-event-type-cycles.json).
|
|
76
|
+
if (!(ids.length === 1 && removals.length === 0) &&
|
|
77
|
+
!(ids.length === 2 && removals.length === 2 && shared === 0) &&
|
|
78
|
+
!(ids.length === 3 && removals.length === 3 && shared === 1)) {
|
|
79
|
+
throw this.unsupported(name, 'This unique event type set and removal combination is not fixture-backed.');
|
|
69
80
|
}
|
|
81
|
+
// Each cycle fixture installs its definition alone; interaction with other definitions is unproven.
|
|
82
|
+
if (removals.length && _definitions.size !== 1) {
|
|
83
|
+
throw this.unsupported(name, 'Unique event cycles alongside other definitions are not fixture-backed.');
|
|
84
|
+
}
|
|
85
|
+
removals.forEach(id => this._removalTypes.add(id));
|
|
70
86
|
} else {
|
|
71
87
|
throw this.unsupported(name, 'Unknown constraint definition.');
|
|
72
88
|
}
|
|
@@ -89,7 +105,7 @@ export class InProcessConstraints {
|
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
isRemovalOnlyType(eventTypeId: string): boolean {
|
|
92
|
-
return this.
|
|
108
|
+
return this._propertyRemovalTypes.has(eventTypeId) && !this._constrainedProperties.has(eventTypeId);
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
hasRemovalType(eventTypeId: string): boolean { return this._removalTypes.has(eventTypeId); }
|
|
@@ -126,10 +142,11 @@ export class InProcessConstraints {
|
|
|
126
142
|
owners.set(name, claims);
|
|
127
143
|
}
|
|
128
144
|
const stagedKeys = new Map<string, Map<string, string>>();
|
|
129
|
-
const
|
|
145
|
+
const stagedCycles = new Map<string, Map<string, 'open' | 'released'>>();
|
|
130
146
|
for (const event of incoming) {
|
|
131
147
|
const type = event.eventType.id.value;
|
|
132
148
|
const source = event.context.eventSourceId;
|
|
149
|
+
const violationCount = violations.length;
|
|
133
150
|
for (const [name, definition] of this._definitions) {
|
|
134
151
|
const property = definition.uniqueConstraint?.eventDefinitions.find(entry => entry.eventTypeId === type)?.properties[0];
|
|
135
152
|
if (property !== undefined) {
|
|
@@ -151,15 +168,41 @@ export class InProcessConstraints {
|
|
|
151
168
|
}
|
|
152
169
|
const uniqueType = definition.uniqueEventType;
|
|
153
170
|
if (uniqueType && (uniqueType.eventTypeIds ?? [uniqueType.eventTypeId]).includes(type)) {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
171
|
+
const covered = uniqueType.eventTypeIds ?? [uniqueType.eventTypeId];
|
|
172
|
+
const removals = uniqueType.removedWithEventTypeIds ?? [];
|
|
173
|
+
// Durable answer: the earliest covered event after the latest removal for this source.
|
|
174
|
+
let first: AppendedEvent | undefined;
|
|
175
|
+
for (const prior of history) {
|
|
176
|
+
if (prior.context.eventSourceId !== source) continue;
|
|
177
|
+
const priorType = prior.eventType.id.value;
|
|
178
|
+
if (removals.includes(priorType)) first = undefined;
|
|
179
|
+
else if (!first && covered.includes(priorType)) first = prior;
|
|
180
|
+
}
|
|
181
|
+
const cycles = stagedCycles.get(name) ?? new Map<string, 'open' | 'released'>();
|
|
182
|
+
const state = cycles.get(source);
|
|
183
|
+
// A cycle state recorded earlier in this append overrides durable history.
|
|
184
|
+
if (state === 'open' || state === undefined && first) {
|
|
185
|
+
// Durable storage supplies the sequence even when a staged claim causes rejection.
|
|
186
|
+
const sequence = first?.context.sequenceNumber.toString() ?? unavailable;
|
|
187
|
+
violations.push({ EventTypeId: type, SequenceNumber: BigInt(sequence), ConstraintType: 2,
|
|
159
188
|
ConstraintName: name,
|
|
160
|
-
Message: `Event '${type}' violated a unique event type constraint on sequence number ${
|
|
189
|
+
Message: `Event '${type}' violated a unique event type constraint on sequence number ${sequence}`,
|
|
161
190
|
Details: {} });
|
|
162
|
-
} else
|
|
191
|
+
} else {
|
|
192
|
+
cycles.set(source, 'open');
|
|
193
|
+
stagedCycles.set(name, cycles);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Kernel batch observers release a cycle only after the entire event validates.
|
|
198
|
+
// A covered-and-removal event first attempts its claim, then releases it.
|
|
199
|
+
if (violations.length === violationCount) {
|
|
200
|
+
for (const [name, definition] of this._definitions) {
|
|
201
|
+
if (definition.uniqueEventType?.removedWithEventTypeIds?.includes(type)) {
|
|
202
|
+
const cycles = stagedCycles.get(name) ?? new Map<string, 'open' | 'released'>();
|
|
203
|
+
cycles.set(source, 'released');
|
|
204
|
+
stagedCycles.set(name, cycles);
|
|
205
|
+
}
|
|
163
206
|
}
|
|
164
207
|
}
|
|
165
208
|
}
|