@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +7 -7
  3. package/src/Message.res +43 -10
  4. package/src/Message.res.mjs +21 -4
  5. package/src/Projection.res +65 -46
  6. package/src/Projection.res.mjs +53 -49
  7. package/src/components/Aggregate/Aggregate_Callback.res +61 -6
  8. package/src/components/Aggregate/Aggregate_Callback.res.mjs +37 -9
  9. package/src/components/DcbEventLog/DcbEventLog_Operations.res +3 -0
  10. package/src/components/DcbEventLog/DcbEventLog_Operations.res.mjs +3 -1
  11. package/src/components/EventLog/EventLog.res +20 -1
  12. package/src/components/EventLog/EventLog_Adapter.res +3 -1
  13. package/src/components/EventLog/EventLog_Builder.res +4 -0
  14. package/src/components/EventLog/EventLog_Builder.res.mjs +3 -1
  15. package/src/components/EventLog/EventLog_Operations.res +11 -2
  16. package/src/components/EventLog/EventLog_Operations.res.mjs +6 -2
  17. package/src/components/EventLog/EventPublish_Callback.res +7 -0
  18. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +26 -6
  19. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +21 -6
  20. package/src/plugin/lifecycle/PluginBehavior.res +6 -0
  21. package/src/plugin/lifecycle/PluginBehavior.res.mjs +3 -0
  22. package/src/util/Logger.res +10 -0
  23. package/src/util/Logger.res.mjs +12 -2
  24. package/tests/aggregate/AggregateCacheTest.res +271 -0
  25. package/tests/aggregate/AggregateCacheTest.res.mjs +433 -0
  26. package/tests/aggregate/AggregateCallbackTest.res +4 -1
  27. package/tests/aggregate/AggregateCallbackTest.res.mjs +4 -1
  28. package/tests/aggregate/AggregateCausationTest.res +4 -1
  29. package/tests/aggregate/AggregateCausationTest.res.mjs +4 -1
  30. package/tests/aggregate/AggregateConflictTest.res +14 -3
  31. package/tests/aggregate/AggregateConflictTest.res.mjs +26 -3
  32. package/tests/aggregate/AggregateFixtures.res +10 -2
  33. package/tests/aggregate/AggregateFixtures.res.mjs +22 -2
  34. package/tests/aggregate/AggregateRejectionTest.res +4 -1
  35. package/tests/aggregate/AggregateRejectionTest.res.mjs +4 -1
  36. package/tests/eventlog/EventLogFixtures.res +13 -2
  37. package/tests/eventlog/EventLogFixtures.res.mjs +27 -3
  38. package/tests/logger/LoggerLazyTest.res +36 -0
  39. package/tests/logger/LoggerLazyTest.res.mjs +40 -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(() => AggregateFixtures$ReventlessCore.mock.reset());
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(() => mock.reset())
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(() => AggregateFixtures$ReventlessCore.mock.reset());
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 =>
@@ -116,8 +120,10 @@ module TestOps = {
116
120
  type operations = {
117
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(() => mock.reset())
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
@@ -145,17 +146,35 @@ let eventLog_append = mock.appendFn;
145
146
 
146
147
  let eventLog_replay = mock.replayFn;
147
148
 
148
- let eventLog_replayStream = mock.replayStreamFn;
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(() => mock.reset());
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 =>
@@ -131,8 +135,10 @@ module TestOps = {
131
135
  array<Message.event'<string, AggSpec.event>>,
132
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
@@ -180,17 +181,35 @@ let eventLog_append = mock.appendFn;
180
181
 
181
182
  let eventLog_replay = mock.replayFn;
182
183
 
183
- let eventLog_replayStream = mock.replayStreamFn;
184
+ function eventLog_replayStream(id, param) {
185
+ return mock.replayStreamFn(id);
186
+ }
184
187
 
185
188
  function eventLog_appendStream(_startingSeqNr, _id, stream) {
186
189
  return Stream.runDrain(stream);
187
190
  }
188
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
+
189
206
  let eventLog = {
190
207
  append: eventLog_append,
191
208
  replay: eventLog_replay,
192
209
  replayStream: eventLog_replayStream,
193
- appendStream: eventLog_appendStream
210
+ appendStream: eventLog_appendStream,
211
+ latestSnapshot: eventLog_latestSnapshot,
212
+ writeSnapshot: eventLog_writeSnapshot
194
213
  };
195
214
 
196
215
  let TestOps = {
@@ -217,6 +236,7 @@ let TestHandler = Aggregate_Callback$ReventlessCore.Make({
217
236
  initialState: "NotCreated",
218
237
  evolve: evolve,
219
238
  decide: decide,
239
+ snapshot: undefined,
220
240
  moduleUrl: moduleUrl$1
221
241
  })({
222
242
  Spec: {
@@ -7,7 +7,10 @@
7
7
  open JestGlobals
8
8
  open AggregateFixtures
9
9
 
10
- let _ = beforeEach(() => mock.reset())
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(() => AggregateFixtures$ReventlessCore.mock.reset());
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)
@@ -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->Dict.get(id)->Option.getOr([])->Stream.fromIterable,
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
  };
@@ -75,8 +79,9 @@ async function mockStorage_replay(id) {
75
79
  return Stdlib_Option.getOr(storedEvents.contents[id], []);
76
80
  }
77
81
 
78
- function mockStorage_replayStream(id) {
79
- return Stream.fromIterable(Stdlib_Option.getOr(storedEvents.contents[id], []));
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));
80
85
  }
81
86
 
82
87
  function mockStorage_appendStream(_startingSeqNr, id, stream) {
@@ -86,11 +91,28 @@ function mockStorage_appendStream(_startingSeqNr, id, stream) {
86
91
  }));
87
92
  }
88
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
+
89
109
  let mockStorage = {
90
110
  append: mockStorage_append,
91
111
  replay: mockStorage_replay,
92
112
  replayStream: mockStorage_replayStream,
93
- appendStream: mockStorage_appendStream
113
+ appendStream: mockStorage_appendStream,
114
+ latestSnapshot: mockStorage_latestSnapshot,
115
+ writeSnapshot: mockStorage_writeSnapshot
94
116
  };
95
117
 
96
118
  let capturedPublishes = {
@@ -149,6 +171,7 @@ function makeEvent$p(id, event) {
149
171
 
150
172
  function reset() {
151
173
  storedEvents.contents = {};
174
+ storedSnapshots.contents = {};
152
175
  failNextAppend.contents = false;
153
176
  failNextAppendsWithTransient.contents = 0;
154
177
  appendCallCount.contents = 0;
@@ -158,6 +181,7 @@ function reset() {
158
181
  export {
159
182
  ItemEventLogSpec,
160
183
  storedEvents,
184
+ storedSnapshots,
161
185
  failNextAppend,
162
186
  failNextAppendsWithTransient,
163
187
  appendCallCount,
@@ -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 */