@reventlessdev/reventless-core 3.0.0-alpha.134 → 3.0.0-alpha.136
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/CHANGELOG.md +14 -0
- package/package.json +8 -8
- package/src/Projection.res +19 -10
- package/src/Projection.res.mjs +17 -9
- package/src/components/Aggregate/Aggregate_Callback.res +6 -3
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +5 -4
- package/src/components/EventLog/EventLog.res +13 -1
- package/src/components/EventLog/EventLog_Operations.res +37 -34
- package/src/components/EventLog/EventLog_Operations.res.mjs +30 -13
- package/src/components/ReadModel/ReadModel.res +5 -0
- package/src/components/ReadModel/ReadModel_Builder.res +12 -0
- package/src/components/ReadModel/ReadModel_Builder.res.mjs +3 -0
- package/src/plugin/component/Plugin_Structure.res +9 -2
- package/src/plugin/component/Plugin_Structure.res.mjs +7 -4
- package/src/plugin/component/SchemaWalker.res +13 -1
- package/src/plugin/component/SchemaWalker.res.mjs +5 -4
- package/src/util/CommandPublisher.res +20 -10
- package/src/util/CommandPublisher.res.mjs +6 -10
- package/src/util/Util_Array.res +1 -1
- package/src/util/Util_Array.res.mjs +1 -1
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +2 -1
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +4 -2
- package/tests/aggregate/AggregateConflictTest.res +3 -3
- package/tests/aggregate/AggregateConflictTest.res.mjs +1 -1
- package/tests/aggregate/AggregateFixtures.res +3 -3
- package/tests/aggregate/AggregateFixtures.res.mjs +4 -1
- package/tests/eventlog/EventLogFixtures.res +2 -2
- package/tests/eventlog/EventLogFixtures.res.mjs +8 -2
- package/tests/eventlog/EventLogRetryTest.res +12 -11
- package/tests/eventlog/EventLogRetryTest.res.mjs +31 -7
- package/tests/plugin/ManifestVisibilityTest.res +14 -0
- package/tests/plugin/ManifestVisibilityTest.res.mjs +10 -0
- package/tests/projection/ProjectionOptimizeTest.res +38 -0
- package/tests/projection/ProjectionOptimizeTest.res.mjs +75 -0
- package/tests/util/CommandPublisherTest.res +56 -0
- package/tests/util/CommandPublisherTest.res.mjs +78 -0
|
@@ -54,7 +54,8 @@ function Make(Spec) {
|
|
|
54
54
|
if (size <= 0) {
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
let commandsToSend = buffer.
|
|
57
|
+
let commandsToSend = buffer.slice(0, size);
|
|
58
|
+
buffer.splice(0, size);
|
|
58
59
|
let promise = Config.publishCommands(Spec.name, toJsons(commandsToSend));
|
|
59
60
|
running.contents = promise;
|
|
60
61
|
let exit = 0;
|
|
@@ -88,7 +89,8 @@ function Make(Spec) {
|
|
|
88
89
|
let bufferSizeStr = buffer.length.toString();
|
|
89
90
|
let chunkCountStr = chunkCount.contents.toString();
|
|
90
91
|
Effect.runSync(EffectLogger$ReventlessCore.logDebug("CommandPublisher", undefined, `send: bufferSize: ` + bufferSizeStr + `, chunk: ` + chunkCountStr + `, size: ` + sizeStr));
|
|
91
|
-
let commandsToSend$1 = buffer.
|
|
92
|
+
let commandsToSend$1 = buffer.slice(0, size$1);
|
|
93
|
+
buffer.splice(0, size$1);
|
|
92
94
|
let promise$1 = Config.publishCommands(Spec.name, toJsons(commandsToSend$1));
|
|
93
95
|
running.contents = promise$1;
|
|
94
96
|
let exit$1 = 0;
|
|
@@ -127,19 +129,13 @@ function Make(Spec) {
|
|
|
127
129
|
};
|
|
128
130
|
let clear = () => {
|
|
129
131
|
Effect.runSync(EffectLogger$ReventlessCore.logDebug("CommandPublisher", undefined, "clear"));
|
|
130
|
-
buffer.splice(0,
|
|
132
|
+
buffer.splice(0, buffer.length);
|
|
131
133
|
flush.contents = false;
|
|
132
134
|
};
|
|
133
135
|
let flush$1 = async () => {
|
|
134
136
|
Effect.runSync(EffectLogger$ReventlessCore.logDebug("CommandPublisher", undefined, "flush"));
|
|
135
137
|
flush.contents = true;
|
|
136
|
-
|
|
137
|
-
if (match === undefined) {
|
|
138
|
-
return await send();
|
|
139
|
-
}
|
|
140
|
-
while (Stdlib_Option.isNone(running.contents)) {
|
|
141
|
-
await finishRunning();
|
|
142
|
-
};
|
|
138
|
+
return await send();
|
|
143
139
|
};
|
|
144
140
|
return {
|
|
145
141
|
buffer: buffer,
|
package/src/util/Util_Array.res
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
let containsByPredicate: (array<'a>, 'a => bool) => bool = (arr, predicate) =>
|
|
2
|
-
arr->Array.find(predicate)->Option.
|
|
2
|
+
arr->Array.find(predicate)->Option.isSome
|
|
@@ -59,10 +59,11 @@ let structure: pluginStructure = {
|
|
|
59
59
|
consumedEventTypes: ["OrderPlaced"],
|
|
60
60
|
inboundCommandTypes: ["Ship"],
|
|
61
61
|
targetName: None,
|
|
62
|
+
externalSystem: None,
|
|
62
63
|
},
|
|
63
64
|
],
|
|
64
65
|
inboundTranslationSlices: [
|
|
65
|
-
{name: "FromBilling", commandTypes: ["RecordPayment"], targetName: "Order"},
|
|
66
|
+
{name: "FromBilling", commandTypes: ["RecordPayment"], targetName: "Order", externalSystem: None},
|
|
66
67
|
],
|
|
67
68
|
extensions: [
|
|
68
69
|
{
|
|
@@ -85,13 +85,15 @@ let structure_outboundTranslationSlices = [{
|
|
|
85
85
|
name: "ToShipper",
|
|
86
86
|
consumedEventTypes: ["OrderPlaced"],
|
|
87
87
|
inboundCommandTypes: ["Ship"],
|
|
88
|
-
targetName: undefined
|
|
88
|
+
targetName: undefined,
|
|
89
|
+
externalSystem: undefined
|
|
89
90
|
}];
|
|
90
91
|
|
|
91
92
|
let structure_inboundTranslationSlices = [{
|
|
92
93
|
name: "FromBilling",
|
|
93
94
|
commandTypes: ["RecordPayment"],
|
|
94
|
-
targetName: "Order"
|
|
95
|
+
targetName: "Order",
|
|
96
|
+
externalSystem: undefined
|
|
95
97
|
}];
|
|
96
98
|
|
|
97
99
|
let structure_extensions = [{
|
|
@@ -41,7 +41,7 @@ module TestBehavior = {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
type mockEL = {
|
|
44
|
-
appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit,
|
|
44
|
+
appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, EventLog.appendError>>,
|
|
45
45
|
replayFn: string => promise<array<AggSpec.event>>,
|
|
46
46
|
replayStreamFn: string => Stream.t<AggSpec.event, string, unit>,
|
|
47
47
|
getAll: unit => array<Message.event'<string, AggSpec.event>>,
|
|
@@ -61,7 +61,7 @@ let makeMockEL = (): mockEL => {
|
|
|
61
61
|
appendCount := appendCount.contents + 1
|
|
62
62
|
if failConflictRef.contents > 0 {
|
|
63
63
|
failConflictRef := failConflictRef.contents - 1
|
|
64
|
-
Error(
|
|
64
|
+
Error(EventLog.Conflict)
|
|
65
65
|
} else {
|
|
66
66
|
storedRef := storedRef.contents->Array.concat(newEvents)
|
|
67
67
|
Ok(())
|
|
@@ -114,7 +114,7 @@ module TestOps = {
|
|
|
114
114
|
type event = AggSpec.event
|
|
115
115
|
}
|
|
116
116
|
type operations = {
|
|
117
|
-
append: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit,
|
|
117
|
+
append: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, EventLog.appendError>>,
|
|
118
118
|
replay: string => promise<array<AggSpec.event>>,
|
|
119
119
|
replayStream: string => Stream.t<AggSpec.event, string, unit>,
|
|
120
120
|
appendStream: (int, string, Stream.t<AggSpec.event, string, unit>) => Effect.t<unit, string, unit>,
|
|
@@ -60,7 +60,7 @@ module TestBehavior = {
|
|
|
60
60
|
// ─────────────────────────────────────────────────────────────
|
|
61
61
|
|
|
62
62
|
type mockEL = {
|
|
63
|
-
appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit,
|
|
63
|
+
appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, EventLog.appendError>>,
|
|
64
64
|
replayFn: string => promise<array<AggSpec.event>>,
|
|
65
65
|
replayStreamFn: string => Stream.t<AggSpec.event, string, unit>,
|
|
66
66
|
getAll: unit => array<Message.event'<string, AggSpec.event>>,
|
|
@@ -75,7 +75,7 @@ let makeMockEL = (): mockEL => {
|
|
|
75
75
|
let appendFn = async (_seqNr, _id, newEvents) =>
|
|
76
76
|
if failRef.contents {
|
|
77
77
|
failRef := false
|
|
78
|
-
Error("append failed")
|
|
78
|
+
Error(EventLog.StorageFailure("append failed"))
|
|
79
79
|
} else {
|
|
80
80
|
storedRef := storedRef.contents->Array.concat(newEvents)
|
|
81
81
|
Ok(())
|
|
@@ -129,7 +129,7 @@ module TestOps = {
|
|
|
129
129
|
int,
|
|
130
130
|
string,
|
|
131
131
|
array<Message.event'<string, AggSpec.event>>,
|
|
132
|
-
) => promise<result<unit,
|
|
132
|
+
) => promise<result<unit, EventLog.appendError>>,
|
|
133
133
|
replay: string => promise<array<AggSpec.event>>,
|
|
134
134
|
replayStream: string => Stream.t<AggSpec.event, string, unit>,
|
|
135
135
|
appendStream: (int, string, Stream.t<AggSpec.event, string, unit>) => Effect.t<unit, string, unit>,
|
|
@@ -129,7 +129,10 @@ function makeMockEL() {
|
|
|
129
129
|
failRef.contents = false;
|
|
130
130
|
return {
|
|
131
131
|
TAG: "Error",
|
|
132
|
-
_0:
|
|
132
|
+
_0: {
|
|
133
|
+
TAG: "StorageFailure",
|
|
134
|
+
_0: "append failed"
|
|
135
|
+
}
|
|
133
136
|
};
|
|
134
137
|
} else {
|
|
135
138
|
storedRef.contents = storedRef.contents.concat(newEvents);
|
|
@@ -28,10 +28,10 @@ let mockStorage: EventLog_Adapter.operations = {
|
|
|
28
28
|
appendCallCount := appendCallCount.contents + 1
|
|
29
29
|
if failNextAppend.contents {
|
|
30
30
|
failNextAppend := false
|
|
31
|
-
Error("mock storage failure")
|
|
31
|
+
Error(EventLog.StorageFailure("mock storage failure"))
|
|
32
32
|
} else if failNextAppendsWithTransient.contents > 0 {
|
|
33
33
|
failNextAppendsWithTransient := failNextAppendsWithTransient.contents - 1
|
|
34
|
-
Error("ThrottlingException: Rate exceeded")
|
|
34
|
+
Error(EventLog.StorageFailure("ThrottlingException: Rate exceeded"))
|
|
35
35
|
} else {
|
|
36
36
|
let existing = storedEvents.contents->Dict.get(id)->Option.getOr([])
|
|
37
37
|
storedEvents.contents->Dict.set(id, existing->Array.concat(jsons))
|
|
@@ -47,14 +47,20 @@ async function mockStorage_append(_seqNr, id, jsons) {
|
|
|
47
47
|
failNextAppend.contents = false;
|
|
48
48
|
return {
|
|
49
49
|
TAG: "Error",
|
|
50
|
-
_0:
|
|
50
|
+
_0: {
|
|
51
|
+
TAG: "StorageFailure",
|
|
52
|
+
_0: "mock storage failure"
|
|
53
|
+
}
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
if (failNextAppendsWithTransient.contents > 0) {
|
|
54
57
|
failNextAppendsWithTransient.contents = failNextAppendsWithTransient.contents - 1 | 0;
|
|
55
58
|
return {
|
|
56
59
|
TAG: "Error",
|
|
57
|
-
_0:
|
|
60
|
+
_0: {
|
|
61
|
+
TAG: "StorageFailure",
|
|
62
|
+
_0: "ThrottlingException: Rate exceeded"
|
|
63
|
+
}
|
|
58
64
|
};
|
|
59
65
|
}
|
|
60
66
|
let existing = Stdlib_Option.getOr(storedEvents.contents[id], []);
|
|
@@ -16,36 +16,37 @@ let _ = beforeEach(() => reset())
|
|
|
16
16
|
|
|
17
17
|
describe("EventLog_Operations — retry:", () => {
|
|
18
18
|
describe("isTransient predicate:", () => {
|
|
19
|
+
let transient = msg => EventLog_Operations.isTransient(EventLog.StorageFailure(msg))
|
|
19
20
|
testPromise("ThrottlingException is transient", async () => {
|
|
20
|
-
expect(
|
|
21
|
+
expect(transient("ThrottlingException: Rate exceeded"))->toBe(true)
|
|
21
22
|
})
|
|
22
23
|
|
|
23
24
|
testPromise("ProvisionedThroughputExceededException is transient", async () => {
|
|
24
|
-
expect(
|
|
25
|
-
EventLog_Operations.isTransient("ProvisionedThroughputExceededException"),
|
|
26
|
-
)->toBe(true)
|
|
25
|
+
expect(transient("ProvisionedThroughputExceededException"))->toBe(true)
|
|
27
26
|
})
|
|
28
27
|
|
|
29
28
|
testPromise("ServiceUnavailable is transient", async () => {
|
|
30
|
-
expect(
|
|
29
|
+
expect(transient("ServiceUnavailable"))->toBe(true)
|
|
31
30
|
})
|
|
32
31
|
|
|
33
32
|
testPromise("RequestLimitExceeded is transient", async () => {
|
|
34
|
-
expect(
|
|
33
|
+
expect(transient("RequestLimitExceeded"))->toBe(true)
|
|
35
34
|
})
|
|
36
35
|
|
|
37
36
|
testPromise("InternalServerError is transient", async () => {
|
|
38
|
-
expect(
|
|
39
|
-
true,
|
|
40
|
-
)
|
|
37
|
+
expect(transient("InternalServerError: something went wrong"))->toBe(true)
|
|
41
38
|
})
|
|
42
39
|
|
|
43
40
|
testPromise("ValidationException is not transient", async () => {
|
|
44
|
-
expect(
|
|
41
|
+
expect(transient("ValidationException: invalid field"))->toBe(false)
|
|
45
42
|
})
|
|
46
43
|
|
|
47
44
|
testPromise("generic mock storage failure is not transient", async () => {
|
|
48
|
-
expect(
|
|
45
|
+
expect(transient("mock storage failure"))->toBe(false)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
testPromise("a Conflict is never transient (retried a layer up, not here)", async () => {
|
|
49
|
+
expect(EventLog_Operations.isTransient(EventLog.Conflict))->toBe(false)
|
|
49
50
|
})
|
|
50
51
|
})
|
|
51
52
|
|
|
@@ -74,25 +74,49 @@ globalThis.beforeEach(() => EventLogFixtures$ReventlessCore.reset());
|
|
|
74
74
|
globalThis.describe("EventLog_Operations — retry:", () => {
|
|
75
75
|
globalThis.describe("isTransient predicate:", () => {
|
|
76
76
|
globalThis.test("ThrottlingException is transient", async () => {
|
|
77
|
-
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient(
|
|
77
|
+
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
|
|
78
|
+
TAG: "StorageFailure",
|
|
79
|
+
_0: "ThrottlingException: Rate exceeded"
|
|
80
|
+
})).toBe(true);
|
|
78
81
|
});
|
|
79
82
|
globalThis.test("ProvisionedThroughputExceededException is transient", async () => {
|
|
80
|
-
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient(
|
|
83
|
+
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
|
|
84
|
+
TAG: "StorageFailure",
|
|
85
|
+
_0: "ProvisionedThroughputExceededException"
|
|
86
|
+
})).toBe(true);
|
|
81
87
|
});
|
|
82
88
|
globalThis.test("ServiceUnavailable is transient", async () => {
|
|
83
|
-
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient(
|
|
89
|
+
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
|
|
90
|
+
TAG: "StorageFailure",
|
|
91
|
+
_0: "ServiceUnavailable"
|
|
92
|
+
})).toBe(true);
|
|
84
93
|
});
|
|
85
94
|
globalThis.test("RequestLimitExceeded is transient", async () => {
|
|
86
|
-
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient(
|
|
95
|
+
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
|
|
96
|
+
TAG: "StorageFailure",
|
|
97
|
+
_0: "RequestLimitExceeded"
|
|
98
|
+
})).toBe(true);
|
|
87
99
|
});
|
|
88
100
|
globalThis.test("InternalServerError is transient", async () => {
|
|
89
|
-
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient(
|
|
101
|
+
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
|
|
102
|
+
TAG: "StorageFailure",
|
|
103
|
+
_0: "InternalServerError: something went wrong"
|
|
104
|
+
})).toBe(true);
|
|
90
105
|
});
|
|
91
106
|
globalThis.test("ValidationException is not transient", async () => {
|
|
92
|
-
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient(
|
|
107
|
+
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
|
|
108
|
+
TAG: "StorageFailure",
|
|
109
|
+
_0: "ValidationException: invalid field"
|
|
110
|
+
})).toBe(false);
|
|
93
111
|
});
|
|
94
112
|
globalThis.test("generic mock storage failure is not transient", async () => {
|
|
95
|
-
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient(
|
|
113
|
+
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
|
|
114
|
+
TAG: "StorageFailure",
|
|
115
|
+
_0: "mock storage failure"
|
|
116
|
+
})).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
globalThis.test("a Conflict is never transient (retried a layer up, not here)", async () => {
|
|
119
|
+
globalThis.expect(EventLog_Operations$ReventlessCore.isTransient("Conflict")).toBe(false);
|
|
96
120
|
});
|
|
97
121
|
});
|
|
98
122
|
globalThis.describe("permanent storage failure:", () => {
|
|
@@ -45,6 +45,9 @@ module PublicReadModel: ReventlessInfra.ReadModel.T
|
|
|
45
45
|
type role = unit
|
|
46
46
|
type component
|
|
47
47
|
let sourceNames: array<string> = []
|
|
48
|
+
// A read model that projects an event reaching it via a (DCB-log-sourced) mapping — the
|
|
49
|
+
// case Plugin_Structure must surface as a qualified `consumedEventTypes` entry.
|
|
50
|
+
let consumedEventNames: array<string> = ["OrderPlaced"]
|
|
48
51
|
let make = (~api as _, ~apiRole as _, ~allEventTopics as _, ~opts as _=?): component =>
|
|
49
52
|
Obj.magic(0)
|
|
50
53
|
let outputs = (_: component): ReventlessInfra.ReadModel.outputs => Obj.magic(0)
|
|
@@ -60,6 +63,7 @@ module InternalReadModel: ReventlessInfra.ReadModel.T
|
|
|
60
63
|
type role = unit
|
|
61
64
|
type component
|
|
62
65
|
let sourceNames: array<string> = []
|
|
66
|
+
let consumedEventNames: array<string> = []
|
|
63
67
|
let make = (~api as _, ~apiRole as _, ~allEventTopics as _, ~opts as _=?): component =>
|
|
64
68
|
Obj.magic(0)
|
|
65
69
|
let outputs = (_: component): ReventlessInfra.ReadModel.outputs => Obj.magic(0)
|
|
@@ -128,6 +132,16 @@ describe("Plugin_Structure.make — visibility tagging", () => {
|
|
|
128
132
|
expect(internal->Option.map(d => d.visibility))->toEqual(Some(Some("Internal")))
|
|
129
133
|
})
|
|
130
134
|
|
|
135
|
+
testSync("a read model's mapping-consumed events surface as qualified consumedEventTypes", () => {
|
|
136
|
+
// Q6 #2: a classic read model fed via a (DCB-log-sourced) mapping now reflects the
|
|
137
|
+
// events it projects from, qualified to the plugin — so DomainGraph draws the
|
|
138
|
+
// Event→ReadModel projection edge (e.g. OrderPlaced → Customers).
|
|
139
|
+
let public = structure.readModels->Array.find(d => d.name == "PublicView")
|
|
140
|
+
expect(public->Option.map(d => d.consumedEventTypes))->toEqual(
|
|
141
|
+
Some(["VisibilityPlugin.OrderPlaced"]),
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
|
|
131
145
|
testSync("aggregates / stateViewSlices stay empty when none are passed (sanity)", () => {
|
|
132
146
|
expect((
|
|
133
147
|
structure.aggregates->Array.length,
|
|
@@ -53,6 +53,8 @@ let InternalSpec = {
|
|
|
53
53
|
|
|
54
54
|
let sourceNames = [];
|
|
55
55
|
|
|
56
|
+
let consumedEventNames = ["OrderPlaced"];
|
|
57
|
+
|
|
56
58
|
function make(param, param$1, param$2, param$3) {
|
|
57
59
|
return 0;
|
|
58
60
|
}
|
|
@@ -89,6 +91,7 @@ let PublicReadModel_Spec = {
|
|
|
89
91
|
let PublicReadModel = {
|
|
90
92
|
Spec: PublicReadModel_Spec,
|
|
91
93
|
sourceNames: sourceNames,
|
|
94
|
+
consumedEventNames: consumedEventNames,
|
|
92
95
|
make: make,
|
|
93
96
|
outputs: outputs,
|
|
94
97
|
operations: operations,
|
|
@@ -97,6 +100,8 @@ let PublicReadModel = {
|
|
|
97
100
|
|
|
98
101
|
let sourceNames$1 = [];
|
|
99
102
|
|
|
103
|
+
let consumedEventNames$1 = [];
|
|
104
|
+
|
|
100
105
|
function make$1(param, param$1, param$2, param$3) {
|
|
101
106
|
return 0;
|
|
102
107
|
}
|
|
@@ -133,6 +138,7 @@ let InternalReadModel_Spec = {
|
|
|
133
138
|
let InternalReadModel = {
|
|
134
139
|
Spec: InternalReadModel_Spec,
|
|
135
140
|
sourceNames: sourceNames$1,
|
|
141
|
+
consumedEventNames: consumedEventNames$1,
|
|
136
142
|
make: make$1,
|
|
137
143
|
outputs: outputs$1,
|
|
138
144
|
operations: operations$1,
|
|
@@ -183,6 +189,10 @@ globalThis.describe("Plugin_Structure.make — visibility tagging", () => {
|
|
|
183
189
|
let internal = structure.readModels.find(d => d.name === "InternalView");
|
|
184
190
|
globalThis.expect(Stdlib_Option.map(internal, d => d.visibility)).toEqual("Internal");
|
|
185
191
|
});
|
|
192
|
+
globalThis.test("a read model's mapping-consumed events surface as qualified consumedEventTypes", () => {
|
|
193
|
+
let $$public = structure.readModels.find(d => d.name === "PublicView");
|
|
194
|
+
globalThis.expect(Stdlib_Option.map($$public, d => d.consumedEventTypes)).toEqual(["VisibilityPlugin.OrderPlaced"]);
|
|
195
|
+
});
|
|
186
196
|
globalThis.test("aggregates / stateViewSlices stay empty when none are passed (sanity)", () => {
|
|
187
197
|
globalThis.expect([
|
|
188
198
|
structure.aggregates.length,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Regression test for Projection.optimizeActions (plan A7 / D2).
|
|
2
|
+
//
|
|
3
|
+
// optimizeActions merges consecutive per-id actions. A non-merging pair (Delete
|
|
4
|
+
// then Update) grows the accumulator to length 2; the next merge (Update+Set)
|
|
5
|
+
// must fold only the last action and keep everything before it exactly once.
|
|
6
|
+
// Before the `~end=count-1` fix, `previousActions` still contained the
|
|
7
|
+
// just-merged action, so the earlier action was duplicated — applied twice.
|
|
8
|
+
|
|
9
|
+
open JestGlobals
|
|
10
|
+
open Reventless.Projection
|
|
11
|
+
|
|
12
|
+
describe("Projection.optimizeActions", () => {
|
|
13
|
+
testSync("does not duplicate the pre-merge action (off-by-one regression)", () => {
|
|
14
|
+
let optimized = Projection.optimizeActions([Delete("a"), Update("a", x => x + 1), Set("a", 99)])
|
|
15
|
+
// Was 3 (with a duplicated Update("a")) before the fix.
|
|
16
|
+
expect(optimized->Array.length)->toBe(2)
|
|
17
|
+
let sets =
|
|
18
|
+
optimized->Array.filterMap(a =>
|
|
19
|
+
switch a {
|
|
20
|
+
| Set(id, s) => Some((id, s))
|
|
21
|
+
| _ => None
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
expect(sets)->toEqual([("a", 99)])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
testSync("merges a same-id Create+Update chain into a single Create", () => {
|
|
28
|
+
let optimized = Projection.optimizeActions([Create("a", 0), Update("a", x => x + 1), Update("a", x => x + 10)])
|
|
29
|
+
let creates =
|
|
30
|
+
optimized->Array.filterMap(a =>
|
|
31
|
+
switch a {
|
|
32
|
+
| Create(id, s) => Some((id, s))
|
|
33
|
+
| _ => None
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
expect(creates)->toEqual([("a", 11)])
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Projection$ReventlessCore from "../../src/Projection.res.mjs";
|
|
5
|
+
|
|
6
|
+
globalThis.describe("Projection.optimizeActions", () => {
|
|
7
|
+
globalThis.test("does not duplicate the pre-merge action (off-by-one regression)", () => {
|
|
8
|
+
let optimized = Projection$ReventlessCore.optimizeActions([
|
|
9
|
+
{
|
|
10
|
+
TAG: "Delete",
|
|
11
|
+
_0: "a"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
TAG: "Update",
|
|
15
|
+
_0: "a",
|
|
16
|
+
_1: x => x + 1 | 0
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
TAG: "Set",
|
|
20
|
+
_0: "a",
|
|
21
|
+
_1: 99
|
|
22
|
+
}
|
|
23
|
+
]);
|
|
24
|
+
globalThis.expect(optimized.length).toBe(2);
|
|
25
|
+
let sets = Stdlib_Array.filterMap(optimized, a => {
|
|
26
|
+
if (typeof a !== "object" || a.TAG !== "Set") {
|
|
27
|
+
return;
|
|
28
|
+
} else {
|
|
29
|
+
return [
|
|
30
|
+
a._0,
|
|
31
|
+
a._1
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
globalThis.expect(sets).toEqual([[
|
|
36
|
+
"a",
|
|
37
|
+
99
|
|
38
|
+
]]);
|
|
39
|
+
});
|
|
40
|
+
globalThis.test("merges a same-id Create+Update chain into a single Create", () => {
|
|
41
|
+
let optimized = Projection$ReventlessCore.optimizeActions([
|
|
42
|
+
{
|
|
43
|
+
TAG: "Create",
|
|
44
|
+
_0: "a",
|
|
45
|
+
_1: 0
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
TAG: "Update",
|
|
49
|
+
_0: "a",
|
|
50
|
+
_1: x => x + 1 | 0
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
TAG: "Update",
|
|
54
|
+
_0: "a",
|
|
55
|
+
_1: x => x + 10 | 0
|
|
56
|
+
}
|
|
57
|
+
]);
|
|
58
|
+
let creates = Stdlib_Array.filterMap(optimized, a => {
|
|
59
|
+
if (typeof a !== "object" || a.TAG !== "Create") {
|
|
60
|
+
return;
|
|
61
|
+
} else {
|
|
62
|
+
return [
|
|
63
|
+
a._0,
|
|
64
|
+
a._1
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
globalThis.expect(creates).toEqual([[
|
|
69
|
+
"a",
|
|
70
|
+
11
|
|
71
|
+
]]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Regression tests for CommandPublisher (plan A7).
|
|
2
|
+
//
|
|
3
|
+
// Two bugs made the buffered publisher silently lose commands:
|
|
4
|
+
// - send() used the non-mutating `toSpliced`, so the buffer never drained and
|
|
5
|
+
// the wrong slice was published.
|
|
6
|
+
// - flush()'s in-flight arm looped on a dead condition and never sent.
|
|
7
|
+
// - clear() removed only the first buffered command instead of truncating.
|
|
8
|
+
//
|
|
9
|
+
// These tests drive the functor with a capturing publishCommands and assert the
|
|
10
|
+
// buffer flushes in order and clear() empties it.
|
|
11
|
+
|
|
12
|
+
open JestGlobals
|
|
13
|
+
|
|
14
|
+
S.enableJson()
|
|
15
|
+
|
|
16
|
+
let published: ref<array<Reventless.Message.commandJson>> = ref([])
|
|
17
|
+
|
|
18
|
+
module TestSpec = {
|
|
19
|
+
@schema let name = "TestCmd"
|
|
20
|
+
@schema type command = {value: int}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module TestConfig = {
|
|
24
|
+
let user = "tester"
|
|
25
|
+
let publishCommands: Task.publishCommands = (_name, cmds) => {
|
|
26
|
+
published := published.contents->Array.concat(cmds)
|
|
27
|
+
Promise.resolve()
|
|
28
|
+
}
|
|
29
|
+
let mode = CommandPublisher.SendAllInOneChunk
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module Pub = CommandPublisher.Make(TestSpec, TestConfig)
|
|
33
|
+
|
|
34
|
+
describe("CommandPublisher", () => {
|
|
35
|
+
testPromise("flush sends all buffered commands in order and drains the buffer", async () => {
|
|
36
|
+
published := []
|
|
37
|
+
Pub.publish("id1", {value: 1})
|
|
38
|
+
Pub.publish("id2", {value: 2})
|
|
39
|
+
await Pub.flush()
|
|
40
|
+
let ids = published.contents->Array.map((c: Reventless.Message.commandJson) => c.id)
|
|
41
|
+
expect(ids)->toEqual(["id1", "id2"])
|
|
42
|
+
// Buffer drained: a second flush sends nothing more.
|
|
43
|
+
published := []
|
|
44
|
+
await Pub.flush()
|
|
45
|
+
expect(published.contents->Array.length)->toBe(0)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
testPromise("clear truncates the whole buffer so nothing is later published", async () => {
|
|
49
|
+
published := []
|
|
50
|
+
Pub.publish("id3", {value: 3})
|
|
51
|
+
Pub.publish("id4", {value: 4})
|
|
52
|
+
Pub.clear()
|
|
53
|
+
await Pub.flush()
|
|
54
|
+
expect(published.contents->Array.length)->toBe(0)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as CommandPublisher$ReventlessCore from "../../src/util/CommandPublisher.res.mjs";
|
|
5
|
+
|
|
6
|
+
S.enableJson();
|
|
7
|
+
|
|
8
|
+
let published = {
|
|
9
|
+
contents: []
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
let commandSchema = S.schema(s => ({
|
|
13
|
+
value: s.m(S.int)
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
function commandAuthorization(param) {
|
|
17
|
+
return "AllowAuthenticated";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let TestSpec = {
|
|
21
|
+
name: "TestCmd",
|
|
22
|
+
commandSchema: commandSchema,
|
|
23
|
+
commandAuthorization: commandAuthorization
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function publishCommands(_name, cmds) {
|
|
27
|
+
published.contents = published.contents.concat(cmds);
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let TestConfig = {
|
|
32
|
+
user: "tester",
|
|
33
|
+
publishCommands: publishCommands,
|
|
34
|
+
mode: "SendAllInOneChunk"
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let Pub = CommandPublisher$ReventlessCore.Make(TestSpec)(TestConfig);
|
|
38
|
+
|
|
39
|
+
globalThis.describe("CommandPublisher", () => {
|
|
40
|
+
globalThis.test("flush sends all buffered commands in order and drains the buffer", async () => {
|
|
41
|
+
published.contents = [];
|
|
42
|
+
Pub.publish("id1", {
|
|
43
|
+
value: 1
|
|
44
|
+
});
|
|
45
|
+
Pub.publish("id2", {
|
|
46
|
+
value: 2
|
|
47
|
+
});
|
|
48
|
+
await Pub.flush();
|
|
49
|
+
let ids = published.contents.map(c => c.id);
|
|
50
|
+
globalThis.expect(ids).toEqual([
|
|
51
|
+
"id1",
|
|
52
|
+
"id2"
|
|
53
|
+
]);
|
|
54
|
+
published.contents = [];
|
|
55
|
+
await Pub.flush();
|
|
56
|
+
globalThis.expect(published.contents.length).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
globalThis.test("clear truncates the whole buffer so nothing is later published", async () => {
|
|
59
|
+
published.contents = [];
|
|
60
|
+
Pub.publish("id3", {
|
|
61
|
+
value: 3
|
|
62
|
+
});
|
|
63
|
+
Pub.publish("id4", {
|
|
64
|
+
value: 4
|
|
65
|
+
});
|
|
66
|
+
Pub.clear();
|
|
67
|
+
await Pub.flush();
|
|
68
|
+
globalThis.expect(published.contents.length).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
published,
|
|
74
|
+
TestSpec,
|
|
75
|
+
TestConfig,
|
|
76
|
+
Pub,
|
|
77
|
+
}
|
|
78
|
+
/* Not a pure module */
|