@reventlessdev/reventless-core 3.0.0-alpha.136 → 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 +12 -0
- package/package.json +7 -7
- package/src/Message.res +43 -10
- package/src/Message.res.mjs +21 -4
- package/src/Projection.res +65 -46
- package/src/Projection.res.mjs +53 -49
- package/src/components/Aggregate/Aggregate_Callback.res +61 -6
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +37 -9
- 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 +20 -1
- 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 +11 -2
- package/src/components/EventLog/EventLog_Operations.res.mjs +6 -2
- 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/lifecycle/PluginBehavior.res +6 -0
- package/src/plugin/lifecycle/PluginBehavior.res.mjs +3 -0
- package/src/util/Logger.res +10 -0
- package/src/util/Logger.res.mjs +12 -2
- 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 +14 -3
- package/tests/aggregate/AggregateConflictTest.res.mjs +26 -3
- package/tests/aggregate/AggregateFixtures.res +10 -2
- package/tests/aggregate/AggregateFixtures.res.mjs +22 -2
- package/tests/aggregate/AggregateRejectionTest.res +4 -1
- package/tests/aggregate/AggregateRejectionTest.res.mjs +4 -1
- package/tests/eventlog/EventLogFixtures.res +13 -2
- package/tests/eventlog/EventLogFixtures.res.mjs +27 -3
- package/tests/logger/LoggerLazyTest.res +36 -0
- package/tests/logger/LoggerLazyTest.res.mjs +40 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Tests for the Aggregate_Callback in-process replay cache (Phase 1 of
|
|
2
|
+
// docs/plans/aggregate-snapshotting.md): a warm same-id command skips the
|
|
3
|
+
// event-log replay and decides on the cached (state, sequenceNr); the OCC
|
|
4
|
+
// append fences staleness — a stale cache conflicts, invalidates, and the
|
|
5
|
+
// retry replays cold.
|
|
6
|
+
//
|
|
7
|
+
// Unlike the other aggregate mocks, this one enforces optimistic concurrency
|
|
8
|
+
// (append conflicts unless seqNr matches the stored count), because the cache's
|
|
9
|
+
// correctness argument rests on exactly that fence. The Checkpoint command
|
|
10
|
+
// records the decision state's size as an event, making the cached fold state
|
|
11
|
+
// observable through ordinary domain behavior.
|
|
12
|
+
|
|
13
|
+
open JestGlobals
|
|
14
|
+
|
|
15
|
+
S.enableJson()
|
|
16
|
+
|
|
17
|
+
module AggSpec = {
|
|
18
|
+
module Id = Reventless.Id.StringPure
|
|
19
|
+
let name = "CacheTestAggregate"
|
|
20
|
+
|
|
21
|
+
@schema
|
|
22
|
+
type command =
|
|
23
|
+
| Add({name: string})
|
|
24
|
+
| Checkpoint
|
|
25
|
+
| Noop
|
|
26
|
+
|
|
27
|
+
@schema
|
|
28
|
+
type event =
|
|
29
|
+
| Added({name: string})
|
|
30
|
+
| CheckpointTaken({count: int})
|
|
31
|
+
|
|
32
|
+
@schema
|
|
33
|
+
type error = | Never
|
|
34
|
+
|
|
35
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module TestBehavior = {
|
|
39
|
+
module Spec = AggSpec
|
|
40
|
+
type state = {names: array<string>}
|
|
41
|
+
|
|
42
|
+
let initialState = {names: []}
|
|
43
|
+
|
|
44
|
+
// Hand-written behavior (no @@reventless.behavior PPX) — satisfy the
|
|
45
|
+
// Behavior.T snapshot field manually.
|
|
46
|
+
let snapshot = None
|
|
47
|
+
|
|
48
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
49
|
+
|
|
50
|
+
let evolve = (state: state, event: AggSpec.event): state =>
|
|
51
|
+
switch event {
|
|
52
|
+
| Added({name}) => {names: state.names->Array.concat([name])}
|
|
53
|
+
| CheckpointTaken(_) => state
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let decide = (state: state, command: AggSpec.command): result<
|
|
57
|
+
array<AggSpec.event>,
|
|
58
|
+
AggSpec.error,
|
|
59
|
+
> =>
|
|
60
|
+
switch command {
|
|
61
|
+
| Add({name}) => Ok([AggSpec.Added({name: name})])
|
|
62
|
+
| Checkpoint => Ok([AggSpec.CheckpointTaken({count: state.names->Array.length})])
|
|
63
|
+
| Noop => Ok([])
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let testMeta: Message.meta = {
|
|
68
|
+
service: "CacheTestAggregate",
|
|
69
|
+
time: "2024-01-01T00:00:00Z",
|
|
70
|
+
ip: "127.0.0.1",
|
|
71
|
+
user: "test-user",
|
|
72
|
+
msgId: "msg-1",
|
|
73
|
+
correlationId: "corr-1",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type mockEL = {
|
|
77
|
+
appendFn: (
|
|
78
|
+
int,
|
|
79
|
+
string,
|
|
80
|
+
array<Message.event'<string, AggSpec.event>>,
|
|
81
|
+
) => promise<result<unit, EventLog.appendError>>,
|
|
82
|
+
replayFn: string => promise<array<AggSpec.event>>,
|
|
83
|
+
replayStreamFn: string => Stream.t<AggSpec.event, string, unit>,
|
|
84
|
+
getAll: unit => array<Message.event'<string, AggSpec.event>>,
|
|
85
|
+
// Appends directly to storage, bypassing the handler — simulates a second
|
|
86
|
+
// writer the warm cache doesn't know about.
|
|
87
|
+
injectExternal: (string, AggSpec.event) => unit,
|
|
88
|
+
appendCallCount: ref<int>,
|
|
89
|
+
replayCallCount: ref<int>,
|
|
90
|
+
reset: unit => unit,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let makeMockEL = (): mockEL => {
|
|
94
|
+
let storedRef: ref<array<Message.event'<string, AggSpec.event>>> = ref([])
|
|
95
|
+
let appendCount = ref(0)
|
|
96
|
+
let replayCount = ref(0)
|
|
97
|
+
|
|
98
|
+
let eventsFor = id => storedRef.contents->Array.filter(e => e.id == id)
|
|
99
|
+
|
|
100
|
+
// Real OCC: the append succeeds only when seqNr matches the current count.
|
|
101
|
+
let appendFn = async (seqNr, id, newEvents) => {
|
|
102
|
+
appendCount := appendCount.contents + 1
|
|
103
|
+
if seqNr != eventsFor(id)->Array.length {
|
|
104
|
+
Error(EventLog.Conflict)
|
|
105
|
+
} else {
|
|
106
|
+
storedRef := storedRef.contents->Array.concat(newEvents)
|
|
107
|
+
Ok(())
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let replayFn = async id => {
|
|
112
|
+
replayCount := replayCount.contents + 1
|
|
113
|
+
eventsFor(id)->Array.map(e => e.event)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let replayStreamFn = id => {
|
|
117
|
+
replayCount := replayCount.contents + 1
|
|
118
|
+
eventsFor(id)->Array.map(e => e.event)->Stream.fromIterable
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
{
|
|
122
|
+
appendFn,
|
|
123
|
+
replayFn,
|
|
124
|
+
replayStreamFn,
|
|
125
|
+
getAll: () => storedRef.contents,
|
|
126
|
+
injectExternal: (id, event) =>
|
|
127
|
+
storedRef := storedRef.contents->Array.concat([{Message.id, meta: testMeta, event}]),
|
|
128
|
+
appendCallCount: appendCount,
|
|
129
|
+
replayCallCount: replayCount,
|
|
130
|
+
reset: () => {
|
|
131
|
+
storedRef := []
|
|
132
|
+
appendCount := 0
|
|
133
|
+
replayCount := 0
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let mock = makeMockEL()
|
|
139
|
+
|
|
140
|
+
module OuterEventLog = EventLog
|
|
141
|
+
|
|
142
|
+
module TestOps = {
|
|
143
|
+
module Spec = AggSpec
|
|
144
|
+
module EventLog = {
|
|
145
|
+
module Spec = {
|
|
146
|
+
module Id = AggSpec.Id
|
|
147
|
+
let name = "CacheMockEventLog"
|
|
148
|
+
@schema
|
|
149
|
+
type event = AggSpec.event
|
|
150
|
+
}
|
|
151
|
+
type operations = {
|
|
152
|
+
append: (
|
|
153
|
+
int,
|
|
154
|
+
string,
|
|
155
|
+
array<Message.event'<string, AggSpec.event>>,
|
|
156
|
+
) => promise<result<unit, EventLog.appendError>>,
|
|
157
|
+
replay: string => promise<array<AggSpec.event>>,
|
|
158
|
+
replayStream: (string, ~fromSeq: int=?) => Stream.t<AggSpec.event, string, unit>,
|
|
159
|
+
appendStream: (int, string, Stream.t<AggSpec.event, string, unit>) => Effect.t<
|
|
160
|
+
unit,
|
|
161
|
+
string,
|
|
162
|
+
unit,
|
|
163
|
+
>,
|
|
164
|
+
latestSnapshot: string => promise<result<option<EventLog.snapshot>, string>>,
|
|
165
|
+
writeSnapshot: (string, EventLog.snapshot) => promise<result<unit, string>>,
|
|
166
|
+
}
|
|
167
|
+
type component = Component.t<OuterEventLog.t, OuterEventLog.outputs, operations>
|
|
168
|
+
// Never called — satisfies module type only
|
|
169
|
+
let make = (~name as _: string, ~opts as _=?): component => Obj.magic(0)
|
|
170
|
+
}
|
|
171
|
+
let eventLog: EventLog.operations = {
|
|
172
|
+
append: mock.appendFn,
|
|
173
|
+
replay: mock.replayFn,
|
|
174
|
+
replayStream: (id, ~fromSeq as _=?) => mock.replayStreamFn(id),
|
|
175
|
+
appendStream: (_startingSeqNr, _id, stream) => stream->Stream.runDrain,
|
|
176
|
+
latestSnapshot: async _ => Ok(None),
|
|
177
|
+
writeSnapshot: async (_, _) => Ok(),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module TestHandler = Aggregate_Callback.Make(AggSpec, TestBehavior, TestOps)
|
|
182
|
+
|
|
183
|
+
let makeTopicItem = (reference, command): CommandTopic.topicItem<
|
|
184
|
+
Message.command'<string, AggSpec.command>,
|
|
185
|
+
> => {
|
|
186
|
+
command: {
|
|
187
|
+
id: "agg-1",
|
|
188
|
+
meta: testMeta,
|
|
189
|
+
command,
|
|
190
|
+
},
|
|
191
|
+
reference,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let run = async command =>
|
|
195
|
+
await Stream.fromIterable([makeTopicItem("ref", command)])
|
|
196
|
+
->TestHandler.handleCommands
|
|
197
|
+
->Effect.runPromise
|
|
198
|
+
|
|
199
|
+
let _ = beforeEach(() => {
|
|
200
|
+
mock.reset()
|
|
201
|
+
TestHandler.resetCache()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe("Aggregate_Callback — replay cache:", () => {
|
|
205
|
+
testPromise("warm same-id command skips the replay", async () => {
|
|
206
|
+
let r1 = await run(AggSpec.Add({name: "a"}))
|
|
207
|
+
let r2 = await run(AggSpec.Add({name: "b"}))
|
|
208
|
+
expect((r1, r2))->toEqual(([Ok("ref")], [Ok("ref")]))
|
|
209
|
+
// Only the first command replayed; the second decided on the cached state.
|
|
210
|
+
expect(mock.replayCallCount.contents)->toBe(1)
|
|
211
|
+
// Both appends landed at their correct OCC positions (no conflict retries).
|
|
212
|
+
expect((mock.appendCallCount.contents, mock.getAll()->Array.length))->toEqual((2, 2))
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
testPromise("cached fold state equals a cold replay of the log", async () => {
|
|
216
|
+
let _ = await run(AggSpec.Add({name: "a"}))
|
|
217
|
+
let _ = await run(AggSpec.Add({name: "b"}))
|
|
218
|
+
let _ = await run(AggSpec.Add({name: "c"}))
|
|
219
|
+
// Warm checkpoint: decided on the cached state (no replay since call 1).
|
|
220
|
+
let _ = await run(AggSpec.Checkpoint)
|
|
221
|
+
expect(mock.replayCallCount.contents)->toBe(1)
|
|
222
|
+
// Cold checkpoint: same aggregate replayed from the log.
|
|
223
|
+
TestHandler.resetCache()
|
|
224
|
+
let _ = await run(AggSpec.Checkpoint)
|
|
225
|
+
expect(mock.replayCallCount.contents)->toBe(2)
|
|
226
|
+
// Both checkpoints observed the same state — cached fold ≡ replayed fold.
|
|
227
|
+
let checkpoints =
|
|
228
|
+
mock.getAll()
|
|
229
|
+
->Array.filterMap(e =>
|
|
230
|
+
switch e.event {
|
|
231
|
+
| CheckpointTaken({count}) => Some(count)
|
|
232
|
+
| _ => None
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
expect(checkpoints)->toEqual([3, 3])
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
testPromise("stale cache conflicts, invalidates, and self-heals", async () => {
|
|
239
|
+
let _ = await run(AggSpec.Add({name: "a"}))
|
|
240
|
+
// A second writer appends behind the warm cache's back.
|
|
241
|
+
mock.injectExternal("agg-1", AggSpec.Added({name: "external"}))
|
|
242
|
+
// Warm path appends at the stale seq → OCC conflict → invalidate →
|
|
243
|
+
// cold retry reads the authoritative log and succeeds.
|
|
244
|
+
let results = await run(AggSpec.Add({name: "b"}))
|
|
245
|
+
expect(results)->toEqual([Ok("ref")])
|
|
246
|
+
// Replays: initial cold + post-conflict cold (the warm attempt skipped).
|
|
247
|
+
expect(mock.replayCallCount.contents)->toBe(2)
|
|
248
|
+
expect(mock.getAll()->Array.length)->toBe(3)
|
|
249
|
+
// The refreshed state includes the external event.
|
|
250
|
+
let _ = await run(AggSpec.Checkpoint)
|
|
251
|
+
let last = mock.getAll()->Array.getUnsafe(3)
|
|
252
|
+
expect(last.event)->toEqual(AggSpec.CheckpointTaken({count: 3}))
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
testPromise("Ok([]) keeps the read snapshot warm", async () => {
|
|
256
|
+
let _ = await run(AggSpec.Add({name: "a"}))
|
|
257
|
+
let _ = await run(AggSpec.Noop)
|
|
258
|
+
let _ = await run(AggSpec.Add({name: "b"}))
|
|
259
|
+
// Neither the Noop nor the third command replayed.
|
|
260
|
+
expect(mock.replayCallCount.contents)->toBe(1)
|
|
261
|
+
expect(mock.getAll()->Array.length)->toBe(2)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
testPromise("resetCache forces the next command to replay cold", async () => {
|
|
265
|
+
let _ = await run(AggSpec.Add({name: "a"}))
|
|
266
|
+
TestHandler.resetCache()
|
|
267
|
+
let _ = await run(AggSpec.Add({name: "b"}))
|
|
268
|
+
expect(mock.replayCallCount.contents)->toBe(2)
|
|
269
|
+
expect(mock.getAll()->Array.length)->toBe(2)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Stream from "effect/Stream";
|
|
8
|
+
import * as Aggregate_Callback$ReventlessCore from "../../src/components/Aggregate/Aggregate_Callback.res.mjs";
|
|
9
|
+
|
|
10
|
+
S.enableJson();
|
|
11
|
+
|
|
12
|
+
let name = "CacheTestAggregate";
|
|
13
|
+
|
|
14
|
+
let commandSchema = S.union([
|
|
15
|
+
S.schema(s => ({
|
|
16
|
+
TAG: "Add",
|
|
17
|
+
name: s.m(S.string)
|
|
18
|
+
})),
|
|
19
|
+
S.literal("Checkpoint"),
|
|
20
|
+
S.literal("Noop")
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
let eventSchema = S.union([
|
|
24
|
+
S.schema(s => ({
|
|
25
|
+
TAG: "Added",
|
|
26
|
+
name: s.m(S.string)
|
|
27
|
+
})),
|
|
28
|
+
S.schema(s => ({
|
|
29
|
+
TAG: "CheckpointTaken",
|
|
30
|
+
count: s.m(S.int)
|
|
31
|
+
}))
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
let errorSchema = S.literal("Never");
|
|
35
|
+
|
|
36
|
+
let moduleUrl = import.meta.url;
|
|
37
|
+
|
|
38
|
+
function commandAuthorization(param) {
|
|
39
|
+
return "AllowAuthenticated";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let AggSpec = {
|
|
43
|
+
Id: undefined,
|
|
44
|
+
name: name,
|
|
45
|
+
commandSchema: commandSchema,
|
|
46
|
+
eventSchema: eventSchema,
|
|
47
|
+
errorSchema: errorSchema,
|
|
48
|
+
moduleUrl: moduleUrl,
|
|
49
|
+
commandAuthorization: commandAuthorization
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let initialState = {
|
|
53
|
+
names: []
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let moduleUrl$1 = import.meta.url;
|
|
57
|
+
|
|
58
|
+
function evolve(state, event) {
|
|
59
|
+
if (event.TAG === "Added") {
|
|
60
|
+
return {
|
|
61
|
+
names: state.names.concat([event.name])
|
|
62
|
+
};
|
|
63
|
+
} else {
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function decide(state, command) {
|
|
69
|
+
if (typeof command !== "object") {
|
|
70
|
+
if (command === "Checkpoint") {
|
|
71
|
+
return {
|
|
72
|
+
TAG: "Ok",
|
|
73
|
+
_0: [{
|
|
74
|
+
TAG: "CheckpointTaken",
|
|
75
|
+
count: state.names.length
|
|
76
|
+
}]
|
|
77
|
+
};
|
|
78
|
+
} else {
|
|
79
|
+
return {
|
|
80
|
+
TAG: "Ok",
|
|
81
|
+
_0: []
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
return {
|
|
86
|
+
TAG: "Ok",
|
|
87
|
+
_0: [{
|
|
88
|
+
TAG: "Added",
|
|
89
|
+
name: command.name
|
|
90
|
+
}]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let TestBehavior = {
|
|
96
|
+
Spec: undefined,
|
|
97
|
+
initialState: initialState,
|
|
98
|
+
snapshot: undefined,
|
|
99
|
+
moduleUrl: moduleUrl$1,
|
|
100
|
+
evolve: evolve,
|
|
101
|
+
decide: decide
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
let testMeta = {
|
|
105
|
+
service: "CacheTestAggregate",
|
|
106
|
+
time: "2024-01-01T00:00:00Z",
|
|
107
|
+
ip: "127.0.0.1",
|
|
108
|
+
user: "test-user",
|
|
109
|
+
msgId: "msg-1",
|
|
110
|
+
correlationId: "corr-1"
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function makeMockEL() {
|
|
114
|
+
let storedRef = {
|
|
115
|
+
contents: []
|
|
116
|
+
};
|
|
117
|
+
let appendCount = {
|
|
118
|
+
contents: 0
|
|
119
|
+
};
|
|
120
|
+
let replayCount = {
|
|
121
|
+
contents: 0
|
|
122
|
+
};
|
|
123
|
+
let eventsFor = id => storedRef.contents.filter(e => e.id === id);
|
|
124
|
+
let appendFn = async (seqNr, id, newEvents) => {
|
|
125
|
+
appendCount.contents = appendCount.contents + 1 | 0;
|
|
126
|
+
if (seqNr !== eventsFor(id).length) {
|
|
127
|
+
return {
|
|
128
|
+
TAG: "Error",
|
|
129
|
+
_0: "Conflict"
|
|
130
|
+
};
|
|
131
|
+
} else {
|
|
132
|
+
storedRef.contents = storedRef.contents.concat(newEvents);
|
|
133
|
+
return {
|
|
134
|
+
TAG: "Ok",
|
|
135
|
+
_0: undefined
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
let replayFn = async id => {
|
|
140
|
+
replayCount.contents = replayCount.contents + 1 | 0;
|
|
141
|
+
return eventsFor(id).map(e => e.event);
|
|
142
|
+
};
|
|
143
|
+
let replayStreamFn = id => {
|
|
144
|
+
replayCount.contents = replayCount.contents + 1 | 0;
|
|
145
|
+
return Stream.fromIterable(eventsFor(id).map(e => e.event));
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
appendFn: appendFn,
|
|
149
|
+
replayFn: replayFn,
|
|
150
|
+
replayStreamFn: replayStreamFn,
|
|
151
|
+
getAll: () => storedRef.contents,
|
|
152
|
+
injectExternal: (id, event) => {
|
|
153
|
+
storedRef.contents = storedRef.contents.concat([{
|
|
154
|
+
id: id,
|
|
155
|
+
meta: testMeta,
|
|
156
|
+
event: event
|
|
157
|
+
}]);
|
|
158
|
+
},
|
|
159
|
+
appendCallCount: appendCount,
|
|
160
|
+
replayCallCount: replayCount,
|
|
161
|
+
reset: () => {
|
|
162
|
+
storedRef.contents = [];
|
|
163
|
+
appendCount.contents = 0;
|
|
164
|
+
replayCount.contents = 0;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let mock = makeMockEL();
|
|
170
|
+
|
|
171
|
+
let name$1 = "CacheMockEventLog";
|
|
172
|
+
|
|
173
|
+
let Spec = {
|
|
174
|
+
Id: undefined,
|
|
175
|
+
name: name$1,
|
|
176
|
+
eventSchema: eventSchema
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
function make(param, param$1) {
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let EventLog = {
|
|
184
|
+
Spec: Spec,
|
|
185
|
+
make: make
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
let eventLog_append = mock.appendFn;
|
|
189
|
+
|
|
190
|
+
let eventLog_replay = mock.replayFn;
|
|
191
|
+
|
|
192
|
+
function eventLog_replayStream(id, param) {
|
|
193
|
+
return mock.replayStreamFn(id);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function eventLog_appendStream(_startingSeqNr, _id, stream) {
|
|
197
|
+
return Stream.runDrain(stream);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function eventLog_latestSnapshot(param) {
|
|
201
|
+
return {
|
|
202
|
+
TAG: "Ok",
|
|
203
|
+
_0: undefined
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function eventLog_writeSnapshot(param, param$1) {
|
|
208
|
+
return {
|
|
209
|
+
TAG: "Ok",
|
|
210
|
+
_0: undefined
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let eventLog = {
|
|
215
|
+
append: eventLog_append,
|
|
216
|
+
replay: eventLog_replay,
|
|
217
|
+
replayStream: eventLog_replayStream,
|
|
218
|
+
appendStream: eventLog_appendStream,
|
|
219
|
+
latestSnapshot: eventLog_latestSnapshot,
|
|
220
|
+
writeSnapshot: eventLog_writeSnapshot
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
let TestOps = {
|
|
224
|
+
Spec: undefined,
|
|
225
|
+
EventLog: EventLog,
|
|
226
|
+
eventLog: eventLog
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
let TestHandler = Aggregate_Callback$ReventlessCore.Make({
|
|
230
|
+
Id: {
|
|
231
|
+
schema: Id$Reventless.StringPure.schema,
|
|
232
|
+
make: prim => prim,
|
|
233
|
+
makeFromString: prim => prim,
|
|
234
|
+
toString: prim => prim,
|
|
235
|
+
cmp: Id$Reventless.StringPure.cmp
|
|
236
|
+
},
|
|
237
|
+
name: name,
|
|
238
|
+
eventSchema: eventSchema,
|
|
239
|
+
errorSchema: errorSchema,
|
|
240
|
+
commandSchema: commandSchema,
|
|
241
|
+
moduleUrl: moduleUrl,
|
|
242
|
+
commandAuthorization: commandAuthorization
|
|
243
|
+
})({
|
|
244
|
+
initialState: initialState,
|
|
245
|
+
evolve: evolve,
|
|
246
|
+
decide: decide,
|
|
247
|
+
snapshot: undefined,
|
|
248
|
+
moduleUrl: moduleUrl$1
|
|
249
|
+
})({
|
|
250
|
+
Spec: {
|
|
251
|
+
Id: {
|
|
252
|
+
schema: Id$Reventless.StringPure.schema,
|
|
253
|
+
make: prim => prim,
|
|
254
|
+
makeFromString: prim => prim,
|
|
255
|
+
toString: prim => prim,
|
|
256
|
+
cmp: Id$Reventless.StringPure.cmp
|
|
257
|
+
},
|
|
258
|
+
name: name,
|
|
259
|
+
eventSchema: eventSchema,
|
|
260
|
+
errorSchema: errorSchema,
|
|
261
|
+
commandSchema: commandSchema,
|
|
262
|
+
moduleUrl: moduleUrl,
|
|
263
|
+
commandAuthorization: commandAuthorization
|
|
264
|
+
},
|
|
265
|
+
EventLog: {
|
|
266
|
+
Spec: {
|
|
267
|
+
Id: {
|
|
268
|
+
schema: Id$Reventless.StringPure.schema,
|
|
269
|
+
make: prim => prim,
|
|
270
|
+
makeFromString: prim => prim,
|
|
271
|
+
toString: prim => prim,
|
|
272
|
+
cmp: Id$Reventless.StringPure.cmp
|
|
273
|
+
},
|
|
274
|
+
name: name$1,
|
|
275
|
+
eventSchema: eventSchema
|
|
276
|
+
},
|
|
277
|
+
make: make
|
|
278
|
+
},
|
|
279
|
+
eventLog: eventLog
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
function makeTopicItem(reference, command) {
|
|
283
|
+
return {
|
|
284
|
+
command: {
|
|
285
|
+
id: "agg-1",
|
|
286
|
+
meta: testMeta,
|
|
287
|
+
command: command
|
|
288
|
+
},
|
|
289
|
+
reference: reference
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function run(command) {
|
|
294
|
+
return await Effect.runPromise(TestHandler.handleCommands(Stream.fromIterable([makeTopicItem("ref", command)])));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
globalThis.beforeEach(() => {
|
|
298
|
+
mock.reset();
|
|
299
|
+
TestHandler.resetCache();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
globalThis.describe("Aggregate_Callback — replay cache:", () => {
|
|
303
|
+
globalThis.test("warm same-id command skips the replay", async () => {
|
|
304
|
+
let r1 = await run({
|
|
305
|
+
TAG: "Add",
|
|
306
|
+
name: "a"
|
|
307
|
+
});
|
|
308
|
+
let r2 = await run({
|
|
309
|
+
TAG: "Add",
|
|
310
|
+
name: "b"
|
|
311
|
+
});
|
|
312
|
+
globalThis.expect([
|
|
313
|
+
r1,
|
|
314
|
+
r2
|
|
315
|
+
]).toEqual([
|
|
316
|
+
[{
|
|
317
|
+
TAG: "Ok",
|
|
318
|
+
_0: "ref"
|
|
319
|
+
}],
|
|
320
|
+
[{
|
|
321
|
+
TAG: "Ok",
|
|
322
|
+
_0: "ref"
|
|
323
|
+
}]
|
|
324
|
+
]);
|
|
325
|
+
globalThis.expect(mock.replayCallCount.contents).toBe(1);
|
|
326
|
+
globalThis.expect([
|
|
327
|
+
mock.appendCallCount.contents,
|
|
328
|
+
mock.getAll().length
|
|
329
|
+
]).toEqual([
|
|
330
|
+
2,
|
|
331
|
+
2
|
|
332
|
+
]);
|
|
333
|
+
});
|
|
334
|
+
globalThis.test("cached fold state equals a cold replay of the log", async () => {
|
|
335
|
+
await run({
|
|
336
|
+
TAG: "Add",
|
|
337
|
+
name: "a"
|
|
338
|
+
});
|
|
339
|
+
await run({
|
|
340
|
+
TAG: "Add",
|
|
341
|
+
name: "b"
|
|
342
|
+
});
|
|
343
|
+
await run({
|
|
344
|
+
TAG: "Add",
|
|
345
|
+
name: "c"
|
|
346
|
+
});
|
|
347
|
+
await run("Checkpoint");
|
|
348
|
+
globalThis.expect(mock.replayCallCount.contents).toBe(1);
|
|
349
|
+
TestHandler.resetCache();
|
|
350
|
+
await run("Checkpoint");
|
|
351
|
+
globalThis.expect(mock.replayCallCount.contents).toBe(2);
|
|
352
|
+
let checkpoints = Stdlib_Array.filterMap(mock.getAll(), e => {
|
|
353
|
+
let match = e.event;
|
|
354
|
+
if (match.TAG === "Added") {
|
|
355
|
+
return;
|
|
356
|
+
} else {
|
|
357
|
+
return match.count;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
globalThis.expect(checkpoints).toEqual([
|
|
361
|
+
3,
|
|
362
|
+
3
|
|
363
|
+
]);
|
|
364
|
+
});
|
|
365
|
+
globalThis.test("stale cache conflicts, invalidates, and self-heals", async () => {
|
|
366
|
+
await run({
|
|
367
|
+
TAG: "Add",
|
|
368
|
+
name: "a"
|
|
369
|
+
});
|
|
370
|
+
mock.injectExternal("agg-1", {
|
|
371
|
+
TAG: "Added",
|
|
372
|
+
name: "external"
|
|
373
|
+
});
|
|
374
|
+
let results = await run({
|
|
375
|
+
TAG: "Add",
|
|
376
|
+
name: "b"
|
|
377
|
+
});
|
|
378
|
+
globalThis.expect(results).toEqual([{
|
|
379
|
+
TAG: "Ok",
|
|
380
|
+
_0: "ref"
|
|
381
|
+
}]);
|
|
382
|
+
globalThis.expect(mock.replayCallCount.contents).toBe(2);
|
|
383
|
+
globalThis.expect(mock.getAll().length).toBe(3);
|
|
384
|
+
await run("Checkpoint");
|
|
385
|
+
let last = mock.getAll()[3];
|
|
386
|
+
globalThis.expect(last.event).toEqual({
|
|
387
|
+
TAG: "CheckpointTaken",
|
|
388
|
+
count: 3
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
globalThis.test("Ok([]) keeps the read snapshot warm", async () => {
|
|
392
|
+
await run({
|
|
393
|
+
TAG: "Add",
|
|
394
|
+
name: "a"
|
|
395
|
+
});
|
|
396
|
+
await run("Noop");
|
|
397
|
+
await run({
|
|
398
|
+
TAG: "Add",
|
|
399
|
+
name: "b"
|
|
400
|
+
});
|
|
401
|
+
globalThis.expect(mock.replayCallCount.contents).toBe(1);
|
|
402
|
+
globalThis.expect(mock.getAll().length).toBe(2);
|
|
403
|
+
});
|
|
404
|
+
globalThis.test("resetCache forces the next command to replay cold", async () => {
|
|
405
|
+
await run({
|
|
406
|
+
TAG: "Add",
|
|
407
|
+
name: "a"
|
|
408
|
+
});
|
|
409
|
+
TestHandler.resetCache();
|
|
410
|
+
await run({
|
|
411
|
+
TAG: "Add",
|
|
412
|
+
name: "b"
|
|
413
|
+
});
|
|
414
|
+
globalThis.expect(mock.replayCallCount.contents).toBe(2);
|
|
415
|
+
globalThis.expect(mock.getAll().length).toBe(2);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
let OuterEventLog;
|
|
420
|
+
|
|
421
|
+
export {
|
|
422
|
+
AggSpec,
|
|
423
|
+
TestBehavior,
|
|
424
|
+
testMeta,
|
|
425
|
+
makeMockEL,
|
|
426
|
+
mock,
|
|
427
|
+
OuterEventLog,
|
|
428
|
+
TestOps,
|
|
429
|
+
TestHandler,
|
|
430
|
+
makeTopicItem,
|
|
431
|
+
run,
|
|
432
|
+
}
|
|
433
|
+
/* Not a pure module */
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
open JestGlobals
|
|
2
2
|
open AggregateFixtures
|
|
3
3
|
|
|
4
|
-
let _ = beforeEach(() =>
|
|
4
|
+
let _ = beforeEach(() => {
|
|
5
|
+
mock.reset()
|
|
6
|
+
TestHandler.resetCache()
|
|
7
|
+
})
|
|
5
8
|
|
|
6
9
|
describe("Aggregate_Callback.handleCommands:", () => {
|
|
7
10
|
describe("single command — new aggregate", () => {
|