@reventlessdev/reventless-core 3.0.0-alpha.135 → 3.0.0-alpha.137
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 +19 -0
- package/package.json +8 -8
- package/src/Message.res +43 -10
- package/src/Message.res.mjs +21 -4
- package/src/Projection.res +83 -55
- package/src/Projection.res.mjs +69 -57
- package/src/components/Aggregate/Aggregate_Callback.res +67 -9
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +42 -13
- package/src/components/DcbEventLog/DcbEventLog_Operations.res +3 -0
- package/src/components/DcbEventLog/DcbEventLog_Operations.res.mjs +3 -1
- package/src/components/EventLog/EventLog.res +33 -2
- package/src/components/EventLog/EventLog_Adapter.res +3 -1
- package/src/components/EventLog/EventLog_Builder.res +4 -0
- package/src/components/EventLog/EventLog_Builder.res.mjs +3 -1
- package/src/components/EventLog/EventLog_Operations.res +48 -36
- package/src/components/EventLog/EventLog_Operations.res.mjs +36 -15
- package/src/components/EventLog/EventPublish_Callback.res +7 -0
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +26 -6
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +21 -6
- package/src/plugin/component/SchemaWalker.res +13 -1
- package/src/plugin/component/SchemaWalker.res.mjs +5 -4
- package/src/plugin/lifecycle/PluginBehavior.res +6 -0
- package/src/plugin/lifecycle/PluginBehavior.res.mjs +3 -0
- package/src/util/CommandPublisher.res +20 -10
- package/src/util/CommandPublisher.res.mjs +6 -10
- package/src/util/Logger.res +10 -0
- package/src/util/Logger.res.mjs +12 -2
- package/src/util/Util_Array.res +1 -1
- package/src/util/Util_Array.res.mjs +1 -1
- package/tests/aggregate/AggregateCacheTest.res +271 -0
- package/tests/aggregate/AggregateCacheTest.res.mjs +433 -0
- package/tests/aggregate/AggregateCallbackTest.res +4 -1
- package/tests/aggregate/AggregateCallbackTest.res.mjs +4 -1
- package/tests/aggregate/AggregateCausationTest.res +4 -1
- package/tests/aggregate/AggregateCausationTest.res.mjs +4 -1
- package/tests/aggregate/AggregateConflictTest.res +17 -6
- package/tests/aggregate/AggregateConflictTest.res.mjs +27 -4
- package/tests/aggregate/AggregateFixtures.res +13 -5
- package/tests/aggregate/AggregateFixtures.res.mjs +26 -3
- package/tests/aggregate/AggregateRejectionTest.res +4 -1
- package/tests/aggregate/AggregateRejectionTest.res.mjs +4 -1
- package/tests/eventlog/EventLogFixtures.res +15 -4
- package/tests/eventlog/EventLogFixtures.res.mjs +35 -5
- package/tests/eventlog/EventLogRetryTest.res +12 -11
- package/tests/eventlog/EventLogRetryTest.res.mjs +31 -7
- package/tests/logger/LoggerLazyTest.res +36 -0
- package/tests/logger/LoggerLazyTest.res.mjs +40 -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
|
@@ -4,7 +4,10 @@ import * as Effect from "effect/Effect";
|
|
|
4
4
|
import * as Stream from "effect/Stream";
|
|
5
5
|
import * as AggregateFixtures$ReventlessCore from "./AggregateFixtures.res.mjs";
|
|
6
6
|
|
|
7
|
-
globalThis.beforeEach(() =>
|
|
7
|
+
globalThis.beforeEach(() => {
|
|
8
|
+
AggregateFixtures$ReventlessCore.mock.reset();
|
|
9
|
+
AggregateFixtures$ReventlessCore.TestHandler.resetCache();
|
|
10
|
+
});
|
|
8
11
|
|
|
9
12
|
globalThis.describe("Aggregate_Callback.handleCommands:", () => {
|
|
10
13
|
globalThis.describe("single command — new aggregate", () => {
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
open JestGlobals
|
|
9
9
|
open AggregateFixtures
|
|
10
10
|
|
|
11
|
-
let _ = beforeEach(() =>
|
|
11
|
+
let _ = beforeEach(() => {
|
|
12
|
+
mock.reset()
|
|
13
|
+
TestHandler.resetCache()
|
|
14
|
+
})
|
|
12
15
|
|
|
13
16
|
describe("Aggregate_Callback causation propagation:", () => {
|
|
14
17
|
testPromise("event.meta.causationId = command.meta.msgId", async () => {
|
|
@@ -4,7 +4,10 @@ import * as Effect from "effect/Effect";
|
|
|
4
4
|
import * as Stream from "effect/Stream";
|
|
5
5
|
import * as AggregateFixtures$ReventlessCore from "./AggregateFixtures.res.mjs";
|
|
6
6
|
|
|
7
|
-
globalThis.beforeEach(() =>
|
|
7
|
+
globalThis.beforeEach(() => {
|
|
8
|
+
AggregateFixtures$ReventlessCore.mock.reset();
|
|
9
|
+
AggregateFixtures$ReventlessCore.TestHandler.resetCache();
|
|
10
|
+
});
|
|
8
11
|
|
|
9
12
|
globalThis.describe("Aggregate_Callback causation propagation:", () => {
|
|
10
13
|
globalThis.test("event.meta.causationId = command.meta.msgId", async () => {
|
|
@@ -27,6 +27,10 @@ module TestBehavior = {
|
|
|
27
27
|
|
|
28
28
|
let initialState = {name: ""}
|
|
29
29
|
|
|
30
|
+
// Hand-written behavior (no @@reventless.behavior PPX) — satisfy the
|
|
31
|
+
// Behavior.T snapshot field manually.
|
|
32
|
+
let snapshot = None
|
|
33
|
+
|
|
30
34
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
31
35
|
|
|
32
36
|
let evolve = (_state: state, event: AggSpec.event): state =>
|
|
@@ -41,7 +45,7 @@ module TestBehavior = {
|
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
type mockEL = {
|
|
44
|
-
appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit,
|
|
48
|
+
appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, EventLog.appendError>>,
|
|
45
49
|
replayFn: string => promise<array<AggSpec.event>>,
|
|
46
50
|
replayStreamFn: string => Stream.t<AggSpec.event, string, unit>,
|
|
47
51
|
getAll: unit => array<Message.event'<string, AggSpec.event>>,
|
|
@@ -61,7 +65,7 @@ let makeMockEL = (): mockEL => {
|
|
|
61
65
|
appendCount := appendCount.contents + 1
|
|
62
66
|
if failConflictRef.contents > 0 {
|
|
63
67
|
failConflictRef := failConflictRef.contents - 1
|
|
64
|
-
Error(
|
|
68
|
+
Error(EventLog.Conflict)
|
|
65
69
|
} else {
|
|
66
70
|
storedRef := storedRef.contents->Array.concat(newEvents)
|
|
67
71
|
Ok(())
|
|
@@ -114,10 +118,12 @@ module TestOps = {
|
|
|
114
118
|
type event = AggSpec.event
|
|
115
119
|
}
|
|
116
120
|
type operations = {
|
|
117
|
-
append: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit,
|
|
121
|
+
append: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, EventLog.appendError>>,
|
|
118
122
|
replay: string => promise<array<AggSpec.event>>,
|
|
119
|
-
replayStream: string => Stream.t<AggSpec.event, string, unit>,
|
|
123
|
+
replayStream: (string, ~fromSeq: int=?) => Stream.t<AggSpec.event, string, unit>,
|
|
120
124
|
appendStream: (int, string, Stream.t<AggSpec.event, string, unit>) => Effect.t<unit, string, unit>,
|
|
125
|
+
latestSnapshot: string => promise<result<option<EventLog.snapshot>, string>>,
|
|
126
|
+
writeSnapshot: (string, EventLog.snapshot) => promise<result<unit, string>>,
|
|
121
127
|
}
|
|
122
128
|
type component = Component.t<OuterEventLog.t, OuterEventLog.outputs, operations>
|
|
123
129
|
let make = (~name as _: string, ~opts as _=?): component => Obj.magic(0)
|
|
@@ -125,8 +131,10 @@ module TestOps = {
|
|
|
125
131
|
let eventLog: EventLog.operations = {
|
|
126
132
|
append: mock.appendFn,
|
|
127
133
|
replay: mock.replayFn,
|
|
128
|
-
replayStream: mock.replayStreamFn,
|
|
134
|
+
replayStream: (id, ~fromSeq as _=?) => mock.replayStreamFn(id),
|
|
129
135
|
appendStream: (_startingSeqNr, _id, stream) => stream->Stream.runDrain,
|
|
136
|
+
latestSnapshot: async _ => Ok(None),
|
|
137
|
+
writeSnapshot: async (_, _) => Ok(),
|
|
130
138
|
}
|
|
131
139
|
}
|
|
132
140
|
|
|
@@ -152,7 +160,10 @@ let makeTopicItem = (reference, command): CommandTopic.topicItem<
|
|
|
152
160
|
reference,
|
|
153
161
|
}
|
|
154
162
|
|
|
155
|
-
let _ = beforeEach(() =>
|
|
163
|
+
let _ = beforeEach(() => {
|
|
164
|
+
mock.reset()
|
|
165
|
+
TestHandler.resetCache()
|
|
166
|
+
})
|
|
156
167
|
|
|
157
168
|
describe("Aggregate_Callback — conflict retry:", () => {
|
|
158
169
|
testPromise("1 conflict retries and succeeds", async () => {
|
|
@@ -63,6 +63,7 @@ function decide(_state, command) {
|
|
|
63
63
|
let TestBehavior = {
|
|
64
64
|
Spec: undefined,
|
|
65
65
|
initialState: initialState,
|
|
66
|
+
snapshot: undefined,
|
|
66
67
|
moduleUrl: moduleUrl$1,
|
|
67
68
|
evolve: evolve,
|
|
68
69
|
decide: decide
|
|
@@ -87,7 +88,7 @@ function makeMockEL() {
|
|
|
87
88
|
failConflictRef.contents = failConflictRef.contents - 1 | 0;
|
|
88
89
|
return {
|
|
89
90
|
TAG: "Error",
|
|
90
|
-
_0: "
|
|
91
|
+
_0: "Conflict"
|
|
91
92
|
};
|
|
92
93
|
} else {
|
|
93
94
|
storedRef.contents = storedRef.contents.concat(newEvents);
|
|
@@ -145,17 +146,35 @@ let eventLog_append = mock.appendFn;
|
|
|
145
146
|
|
|
146
147
|
let eventLog_replay = mock.replayFn;
|
|
147
148
|
|
|
148
|
-
|
|
149
|
+
function eventLog_replayStream(id, param) {
|
|
150
|
+
return mock.replayStreamFn(id);
|
|
151
|
+
}
|
|
149
152
|
|
|
150
153
|
function eventLog_appendStream(_startingSeqNr, _id, stream) {
|
|
151
154
|
return Stream.runDrain(stream);
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
async function eventLog_latestSnapshot(param) {
|
|
158
|
+
return {
|
|
159
|
+
TAG: "Ok",
|
|
160
|
+
_0: undefined
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function eventLog_writeSnapshot(param, param$1) {
|
|
165
|
+
return {
|
|
166
|
+
TAG: "Ok",
|
|
167
|
+
_0: undefined
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
154
171
|
let eventLog = {
|
|
155
172
|
append: eventLog_append,
|
|
156
173
|
replay: eventLog_replay,
|
|
157
174
|
replayStream: eventLog_replayStream,
|
|
158
|
-
appendStream: eventLog_appendStream
|
|
175
|
+
appendStream: eventLog_appendStream,
|
|
176
|
+
latestSnapshot: eventLog_latestSnapshot,
|
|
177
|
+
writeSnapshot: eventLog_writeSnapshot
|
|
159
178
|
};
|
|
160
179
|
|
|
161
180
|
let TestOps = {
|
|
@@ -182,6 +201,7 @@ let TestHandler = Aggregate_Callback$ReventlessCore.Make({
|
|
|
182
201
|
initialState: initialState,
|
|
183
202
|
evolve: evolve,
|
|
184
203
|
decide: decide,
|
|
204
|
+
snapshot: undefined,
|
|
185
205
|
moduleUrl: moduleUrl$1
|
|
186
206
|
})({
|
|
187
207
|
Spec: {
|
|
@@ -236,7 +256,10 @@ function makeTopicItem(reference, command) {
|
|
|
236
256
|
};
|
|
237
257
|
}
|
|
238
258
|
|
|
239
|
-
globalThis.beforeEach(() =>
|
|
259
|
+
globalThis.beforeEach(() => {
|
|
260
|
+
mock.reset();
|
|
261
|
+
TestHandler.resetCache();
|
|
262
|
+
});
|
|
240
263
|
|
|
241
264
|
globalThis.describe("Aggregate_Callback — conflict retry:", () => {
|
|
242
265
|
globalThis.test("1 conflict retries and succeeds", async () => {
|
|
@@ -38,6 +38,10 @@ module TestBehavior = {
|
|
|
38
38
|
|
|
39
39
|
let initialState = NotCreated
|
|
40
40
|
|
|
41
|
+
// Hand-written behavior (no @@reventless.behavior PPX) — satisfy the
|
|
42
|
+
// Behavior.T snapshot field manually.
|
|
43
|
+
let snapshot = None
|
|
44
|
+
|
|
41
45
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
42
46
|
|
|
43
47
|
let evolve = (_state: state, event: AggSpec.event): state =>
|
|
@@ -60,7 +64,7 @@ module TestBehavior = {
|
|
|
60
64
|
// ─────────────────────────────────────────────────────────────
|
|
61
65
|
|
|
62
66
|
type mockEL = {
|
|
63
|
-
appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit,
|
|
67
|
+
appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, EventLog.appendError>>,
|
|
64
68
|
replayFn: string => promise<array<AggSpec.event>>,
|
|
65
69
|
replayStreamFn: string => Stream.t<AggSpec.event, string, unit>,
|
|
66
70
|
getAll: unit => array<Message.event'<string, AggSpec.event>>,
|
|
@@ -75,7 +79,7 @@ let makeMockEL = (): mockEL => {
|
|
|
75
79
|
let appendFn = async (_seqNr, _id, newEvents) =>
|
|
76
80
|
if failRef.contents {
|
|
77
81
|
failRef := false
|
|
78
|
-
Error("append failed")
|
|
82
|
+
Error(EventLog.StorageFailure("append failed"))
|
|
79
83
|
} else {
|
|
80
84
|
storedRef := storedRef.contents->Array.concat(newEvents)
|
|
81
85
|
Ok(())
|
|
@@ -129,10 +133,12 @@ module TestOps = {
|
|
|
129
133
|
int,
|
|
130
134
|
string,
|
|
131
135
|
array<Message.event'<string, AggSpec.event>>,
|
|
132
|
-
) => promise<result<unit,
|
|
136
|
+
) => promise<result<unit, EventLog.appendError>>,
|
|
133
137
|
replay: string => promise<array<AggSpec.event>>,
|
|
134
|
-
replayStream: string => Stream.t<AggSpec.event, string, unit>,
|
|
138
|
+
replayStream: (string, ~fromSeq: int=?) => Stream.t<AggSpec.event, string, unit>,
|
|
135
139
|
appendStream: (int, string, Stream.t<AggSpec.event, string, unit>) => Effect.t<unit, string, unit>,
|
|
140
|
+
latestSnapshot: string => promise<result<option<EventLog.snapshot>, string>>,
|
|
141
|
+
writeSnapshot: (string, EventLog.snapshot) => promise<result<unit, string>>,
|
|
136
142
|
}
|
|
137
143
|
type component = Component.t<OuterEventLog.t, OuterEventLog.outputs, operations>
|
|
138
144
|
// Never called — satisfies module type only
|
|
@@ -141,8 +147,10 @@ module TestOps = {
|
|
|
141
147
|
let eventLog: EventLog.operations = {
|
|
142
148
|
append: mock.appendFn,
|
|
143
149
|
replay: mock.replayFn,
|
|
144
|
-
replayStream: mock.replayStreamFn,
|
|
150
|
+
replayStream: (id, ~fromSeq as _=?) => mock.replayStreamFn(id),
|
|
145
151
|
appendStream: (_startingSeqNr, _id, stream) => stream->Stream.runDrain,
|
|
152
|
+
latestSnapshot: async _ => Ok(None),
|
|
153
|
+
writeSnapshot: async (_, _) => Ok(),
|
|
146
154
|
}
|
|
147
155
|
}
|
|
148
156
|
|
|
@@ -112,6 +112,7 @@ let TestBehavior = {
|
|
|
112
112
|
Spec: undefined,
|
|
113
113
|
stateSchema: stateSchema,
|
|
114
114
|
initialState: "NotCreated",
|
|
115
|
+
snapshot: undefined,
|
|
115
116
|
moduleUrl: moduleUrl$1,
|
|
116
117
|
evolve: evolve,
|
|
117
118
|
decide: decide
|
|
@@ -129,7 +130,10 @@ function makeMockEL() {
|
|
|
129
130
|
failRef.contents = false;
|
|
130
131
|
return {
|
|
131
132
|
TAG: "Error",
|
|
132
|
-
_0:
|
|
133
|
+
_0: {
|
|
134
|
+
TAG: "StorageFailure",
|
|
135
|
+
_0: "append failed"
|
|
136
|
+
}
|
|
133
137
|
};
|
|
134
138
|
} else {
|
|
135
139
|
storedRef.contents = storedRef.contents.concat(newEvents);
|
|
@@ -177,17 +181,35 @@ let eventLog_append = mock.appendFn;
|
|
|
177
181
|
|
|
178
182
|
let eventLog_replay = mock.replayFn;
|
|
179
183
|
|
|
180
|
-
|
|
184
|
+
function eventLog_replayStream(id, param) {
|
|
185
|
+
return mock.replayStreamFn(id);
|
|
186
|
+
}
|
|
181
187
|
|
|
182
188
|
function eventLog_appendStream(_startingSeqNr, _id, stream) {
|
|
183
189
|
return Stream.runDrain(stream);
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
async function eventLog_latestSnapshot(param) {
|
|
193
|
+
return {
|
|
194
|
+
TAG: "Ok",
|
|
195
|
+
_0: undefined
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function eventLog_writeSnapshot(param, param$1) {
|
|
200
|
+
return {
|
|
201
|
+
TAG: "Ok",
|
|
202
|
+
_0: undefined
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
186
206
|
let eventLog = {
|
|
187
207
|
append: eventLog_append,
|
|
188
208
|
replay: eventLog_replay,
|
|
189
209
|
replayStream: eventLog_replayStream,
|
|
190
|
-
appendStream: eventLog_appendStream
|
|
210
|
+
appendStream: eventLog_appendStream,
|
|
211
|
+
latestSnapshot: eventLog_latestSnapshot,
|
|
212
|
+
writeSnapshot: eventLog_writeSnapshot
|
|
191
213
|
};
|
|
192
214
|
|
|
193
215
|
let TestOps = {
|
|
@@ -214,6 +236,7 @@ let TestHandler = Aggregate_Callback$ReventlessCore.Make({
|
|
|
214
236
|
initialState: "NotCreated",
|
|
215
237
|
evolve: evolve,
|
|
216
238
|
decide: decide,
|
|
239
|
+
snapshot: undefined,
|
|
217
240
|
moduleUrl: moduleUrl$1
|
|
218
241
|
})({
|
|
219
242
|
Spec: {
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
open JestGlobals
|
|
8
8
|
open AggregateFixtures
|
|
9
9
|
|
|
10
|
-
let _ = beforeEach(() =>
|
|
10
|
+
let _ = beforeEach(() => {
|
|
11
|
+
mock.reset()
|
|
12
|
+
TestHandler.resetCache()
|
|
13
|
+
})
|
|
11
14
|
|
|
12
15
|
// Captures whatever flows through CommandTopic_Helpers.reportRejected during a test.
|
|
13
16
|
let capturedRejections: ref<array<(string, CommandTopic_Helpers.rejectedResult)>> = ref([])
|
|
@@ -5,7 +5,10 @@ import * as Stream from "effect/Stream";
|
|
|
5
5
|
import * as AggregateFixtures$ReventlessCore from "./AggregateFixtures.res.mjs";
|
|
6
6
|
import * as CommandTopic_Helpers$ReventlessCore from "../../src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
7
7
|
|
|
8
|
-
globalThis.beforeEach(() =>
|
|
8
|
+
globalThis.beforeEach(() => {
|
|
9
|
+
AggregateFixtures$ReventlessCore.mock.reset();
|
|
10
|
+
AggregateFixtures$ReventlessCore.TestHandler.resetCache();
|
|
11
|
+
});
|
|
9
12
|
|
|
10
13
|
let capturedRejections = {
|
|
11
14
|
contents: []
|
|
@@ -17,6 +17,7 @@ module ItemEventLogSpec = {
|
|
|
17
17
|
// ─────────────────────────────────────────────────────────────
|
|
18
18
|
|
|
19
19
|
let storedEvents: ref<dict<array<JSON.t>>> = ref(Dict.make())
|
|
20
|
+
let storedSnapshots: ref<dict<EventLog.snapshot>> = ref(Dict.make())
|
|
20
21
|
let failNextAppend = ref(false)
|
|
21
22
|
// Inject N transient failures: each call decrements the counter and returns ThrottlingException
|
|
22
23
|
let failNextAppendsWithTransient: ref<int> = ref(0)
|
|
@@ -28,10 +29,10 @@ let mockStorage: EventLog_Adapter.operations = {
|
|
|
28
29
|
appendCallCount := appendCallCount.contents + 1
|
|
29
30
|
if failNextAppend.contents {
|
|
30
31
|
failNextAppend := false
|
|
31
|
-
Error("mock storage failure")
|
|
32
|
+
Error(EventLog.StorageFailure("mock storage failure"))
|
|
32
33
|
} else if failNextAppendsWithTransient.contents > 0 {
|
|
33
34
|
failNextAppendsWithTransient := failNextAppendsWithTransient.contents - 1
|
|
34
|
-
Error("ThrottlingException: Rate exceeded")
|
|
35
|
+
Error(EventLog.StorageFailure("ThrottlingException: Rate exceeded"))
|
|
35
36
|
} else {
|
|
36
37
|
let existing = storedEvents.contents->Dict.get(id)->Option.getOr([])
|
|
37
38
|
storedEvents.contents->Dict.set(id, existing->Array.concat(jsons))
|
|
@@ -41,8 +42,12 @@ let mockStorage: EventLog_Adapter.operations = {
|
|
|
41
42
|
replay: async id => {
|
|
42
43
|
storedEvents.contents->Dict.get(id)->Option.getOr([])
|
|
43
44
|
},
|
|
44
|
-
replayStream: id =>
|
|
45
|
-
storedEvents.contents
|
|
45
|
+
replayStream: (id, ~fromSeq=0) =>
|
|
46
|
+
storedEvents.contents
|
|
47
|
+
->Dict.get(id)
|
|
48
|
+
->Option.getOr([])
|
|
49
|
+
->Array.slice(~start=fromSeq)
|
|
50
|
+
->Stream.fromIterable,
|
|
46
51
|
appendStream: (_startingSeqNr, id, stream) =>
|
|
47
52
|
stream->Stream.runForEach(json =>
|
|
48
53
|
Effect.sync(() => {
|
|
@@ -50,6 +55,11 @@ let mockStorage: EventLog_Adapter.operations = {
|
|
|
50
55
|
storedEvents.contents->Dict.set(id, existing->Array.concat([json]))
|
|
51
56
|
})
|
|
52
57
|
),
|
|
58
|
+
latestSnapshot: async id => Ok(storedSnapshots.contents->Dict.get(id)),
|
|
59
|
+
writeSnapshot: async (id, snap) => {
|
|
60
|
+
storedSnapshots.contents->Dict.set(id, snap)
|
|
61
|
+
Ok()
|
|
62
|
+
},
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -106,6 +116,7 @@ let makeEvent' = (id, event) => {
|
|
|
106
116
|
|
|
107
117
|
let reset = () => {
|
|
108
118
|
storedEvents := Dict.make()
|
|
119
|
+
storedSnapshots := Dict.make()
|
|
109
120
|
failNextAppend := false
|
|
110
121
|
failNextAppendsWithTransient := 0
|
|
111
122
|
appendCallCount := 0
|
|
@@ -29,6 +29,10 @@ let storedEvents = {
|
|
|
29
29
|
contents: {}
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
let storedSnapshots = {
|
|
33
|
+
contents: {}
|
|
34
|
+
};
|
|
35
|
+
|
|
32
36
|
let failNextAppend = {
|
|
33
37
|
contents: false
|
|
34
38
|
};
|
|
@@ -47,14 +51,20 @@ async function mockStorage_append(_seqNr, id, jsons) {
|
|
|
47
51
|
failNextAppend.contents = false;
|
|
48
52
|
return {
|
|
49
53
|
TAG: "Error",
|
|
50
|
-
_0:
|
|
54
|
+
_0: {
|
|
55
|
+
TAG: "StorageFailure",
|
|
56
|
+
_0: "mock storage failure"
|
|
57
|
+
}
|
|
51
58
|
};
|
|
52
59
|
}
|
|
53
60
|
if (failNextAppendsWithTransient.contents > 0) {
|
|
54
61
|
failNextAppendsWithTransient.contents = failNextAppendsWithTransient.contents - 1 | 0;
|
|
55
62
|
return {
|
|
56
63
|
TAG: "Error",
|
|
57
|
-
_0:
|
|
64
|
+
_0: {
|
|
65
|
+
TAG: "StorageFailure",
|
|
66
|
+
_0: "ThrottlingException: Rate exceeded"
|
|
67
|
+
}
|
|
58
68
|
};
|
|
59
69
|
}
|
|
60
70
|
let existing = Stdlib_Option.getOr(storedEvents.contents[id], []);
|
|
@@ -69,8 +79,9 @@ async function mockStorage_replay(id) {
|
|
|
69
79
|
return Stdlib_Option.getOr(storedEvents.contents[id], []);
|
|
70
80
|
}
|
|
71
81
|
|
|
72
|
-
function mockStorage_replayStream(id) {
|
|
73
|
-
|
|
82
|
+
function mockStorage_replayStream(id, fromSeqOpt) {
|
|
83
|
+
let fromSeq = fromSeqOpt !== undefined ? fromSeqOpt : 0;
|
|
84
|
+
return Stream.fromIterable(Stdlib_Option.getOr(storedEvents.contents[id], []).slice(fromSeq));
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
function mockStorage_appendStream(_startingSeqNr, id, stream) {
|
|
@@ -80,11 +91,28 @@ function mockStorage_appendStream(_startingSeqNr, id, stream) {
|
|
|
80
91
|
}));
|
|
81
92
|
}
|
|
82
93
|
|
|
94
|
+
async function mockStorage_latestSnapshot(id) {
|
|
95
|
+
return {
|
|
96
|
+
TAG: "Ok",
|
|
97
|
+
_0: storedSnapshots.contents[id]
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function mockStorage_writeSnapshot(id, snap) {
|
|
102
|
+
storedSnapshots.contents[id] = snap;
|
|
103
|
+
return {
|
|
104
|
+
TAG: "Ok",
|
|
105
|
+
_0: undefined
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
83
109
|
let mockStorage = {
|
|
84
110
|
append: mockStorage_append,
|
|
85
111
|
replay: mockStorage_replay,
|
|
86
112
|
replayStream: mockStorage_replayStream,
|
|
87
|
-
appendStream: mockStorage_appendStream
|
|
113
|
+
appendStream: mockStorage_appendStream,
|
|
114
|
+
latestSnapshot: mockStorage_latestSnapshot,
|
|
115
|
+
writeSnapshot: mockStorage_writeSnapshot
|
|
88
116
|
};
|
|
89
117
|
|
|
90
118
|
let capturedPublishes = {
|
|
@@ -143,6 +171,7 @@ function makeEvent$p(id, event) {
|
|
|
143
171
|
|
|
144
172
|
function reset() {
|
|
145
173
|
storedEvents.contents = {};
|
|
174
|
+
storedSnapshots.contents = {};
|
|
146
175
|
failNextAppend.contents = false;
|
|
147
176
|
failNextAppendsWithTransient.contents = 0;
|
|
148
177
|
appendCallCount.contents = 0;
|
|
@@ -152,6 +181,7 @@ function reset() {
|
|
|
152
181
|
export {
|
|
153
182
|
ItemEventLogSpec,
|
|
154
183
|
storedEvents,
|
|
184
|
+
storedSnapshots,
|
|
155
185
|
failNextAppend,
|
|
156
186
|
failNextAppendsWithTransient,
|
|
157
187
|
appendCallCount,
|
|
@@ -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:", () => {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Unit tests for Logger.debugLazy (B6): the message thunk must run only when
|
|
2
|
+
// debug output is actually enabled, so hot paths (per-projection-action logging)
|
|
3
|
+
// don't pay to build/serialize a string that is dropped at Info level.
|
|
4
|
+
|
|
5
|
+
open JestGlobals
|
|
6
|
+
|
|
7
|
+
describe("Logger.debugLazy", () => {
|
|
8
|
+
testSync("does NOT invoke the thunk at the default Info level", () => {
|
|
9
|
+
let called = ref(false)
|
|
10
|
+
let log = Logger.makeLogger(~minLevel=Logger.Info)
|
|
11
|
+
log.debugLazy(~comp="Test", () => {
|
|
12
|
+
called := true
|
|
13
|
+
"should never be built"
|
|
14
|
+
})
|
|
15
|
+
expect(called.contents)->toEqual(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
testSync("invokes the thunk when debug output is enabled", () => {
|
|
19
|
+
let called = ref(false)
|
|
20
|
+
let log = Logger.makeLogger(~minLevel=Logger.Debug)
|
|
21
|
+
log.debugLazy(~comp="Test", () => {
|
|
22
|
+
called := true
|
|
23
|
+
"built"
|
|
24
|
+
})
|
|
25
|
+
expect(called.contents)->toEqual(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
testSync("the silent logger never invokes the thunk", () => {
|
|
29
|
+
let called = ref(false)
|
|
30
|
+
Logger.silent.debugLazy(~comp="Test", () => {
|
|
31
|
+
called := true
|
|
32
|
+
"x"
|
|
33
|
+
})
|
|
34
|
+
expect(called.contents)->toEqual(false)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Logger$ReventlessCore from "../../src/util/Logger.res.mjs";
|
|
4
|
+
|
|
5
|
+
globalThis.describe("Logger.debugLazy", () => {
|
|
6
|
+
globalThis.test("does NOT invoke the thunk at the default Info level", () => {
|
|
7
|
+
let called = {
|
|
8
|
+
contents: false
|
|
9
|
+
};
|
|
10
|
+
let log = Logger$ReventlessCore.makeLogger("Info");
|
|
11
|
+
log.debugLazy("Test", () => {
|
|
12
|
+
called.contents = true;
|
|
13
|
+
return "should never be built";
|
|
14
|
+
});
|
|
15
|
+
globalThis.expect(called.contents).toEqual(false);
|
|
16
|
+
});
|
|
17
|
+
globalThis.test("invokes the thunk when debug output is enabled", () => {
|
|
18
|
+
let called = {
|
|
19
|
+
contents: false
|
|
20
|
+
};
|
|
21
|
+
let log = Logger$ReventlessCore.makeLogger("Debug");
|
|
22
|
+
log.debugLazy("Test", () => {
|
|
23
|
+
called.contents = true;
|
|
24
|
+
return "built";
|
|
25
|
+
});
|
|
26
|
+
globalThis.expect(called.contents).toEqual(true);
|
|
27
|
+
});
|
|
28
|
+
globalThis.test("the silent logger never invokes the thunk", () => {
|
|
29
|
+
let called = {
|
|
30
|
+
contents: false
|
|
31
|
+
};
|
|
32
|
+
Logger$ReventlessCore.silent.debugLazy("Test", () => {
|
|
33
|
+
called.contents = true;
|
|
34
|
+
return "x";
|
|
35
|
+
});
|
|
36
|
+
globalThis.expect(called.contents).toEqual(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/* Not a pure module */
|