@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
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.137 (2026-07-03)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core,local,aws:** EventLog snapshot storage surface, all backends (aggregate-snapshotting plan, steps 3+4) ([b6e50e2](https://github.com/ReventlessDev/reventless-core/commit/b6e50e2e5fe50b7372e54811f883fa91f6758dd1))
|
|
11
|
+
* **core:** in-process aggregate replay cache (aggregate-snapshotting plan, phase 1) ([89c75f3](https://github.com/ReventlessDev/reventless-core/commit/89c75f33116e929cb8f4208b33498bab42db45e2))
|
|
12
|
+
* **local:** DCB-axis projection checkpoints + startup catch-up (plan B5) ([8f9b11d](https://github.com/ReventlessDev/reventless-core/commit/8f9b11d24627bb6cc105d37af4974745444b80ba))
|
|
13
|
+
### Performance Improvements
|
|
14
|
+
|
|
15
|
+
* **core:** memoize message schemas, lazy projection logs, fail-closed slice cache (plan B6) ([b9e423c](https://github.com/ReventlessDev/reventless-core/commit/b9e423cb27932c4b6b5a123d3ae80d2239341d61))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 3.0.0-alpha.136 (2026-07-02)
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **core,local:** projection/publisher/bus correctness (plan A5,A7) ([86e27a5](https://github.com/ReventlessDev/reventless-core/commit/86e27a5d68873d7dd9e746f9b4126f4d5bad63c9))
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
# 3.0.0-alpha.135 (2026-06-29)
|
|
7
26
|
|
|
8
27
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.137",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -28,22 +28,22 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.
|
|
31
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.24",
|
|
32
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
|
|
33
|
-
"@reventlessdev/rescript-
|
|
34
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.3",
|
|
33
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.5",
|
|
35
34
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
|
|
35
|
+
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
|
|
37
37
|
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.11",
|
|
38
38
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.13",
|
|
39
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.
|
|
40
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
41
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
39
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.23",
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.64",
|
|
41
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.84"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "^12.3.0",
|
|
45
45
|
"sury-ppx": "11.0.0-alpha.2",
|
|
46
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
46
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.50"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"rescript": "^12.3.0"
|
package/src/Message.res
CHANGED
|
@@ -18,18 +18,51 @@ module type Service = {
|
|
|
18
18
|
|
|
19
19
|
include Reventless.Message
|
|
20
20
|
|
|
21
|
+
// Compose the wrapper schema once per (idSchema, payloadSchema) reference pair.
|
|
22
|
+
// Both are module-level constants per message type, and sury schemas are built
|
|
23
|
+
// to be reused across many parse/serialize calls — so the previous fresh
|
|
24
|
+
// `S.object` on every decode/encode was pure allocation on the hottest path in
|
|
25
|
+
// the framework. The cache is keyed by object identity (nested WeakMaps), so a
|
|
26
|
+
// dropped schema is still collectable. `build` runs only on a miss.
|
|
27
|
+
//
|
|
28
|
+
// The raw closure is typed monomorphically over an opaque type (concrete → no
|
|
29
|
+
// weak-var value-restriction problem); the locally-abstract-typed wrapper below
|
|
30
|
+
// is a syntactic function, so it generalizes and `Obj.magic` bridges the two.
|
|
31
|
+
// This keeps the public `toEventSchema'` / `toCommandSchema'` fully polymorphic.
|
|
32
|
+
type opaqueSchema
|
|
33
|
+
let _rawMemoize: (
|
|
34
|
+
opaqueSchema,
|
|
35
|
+
opaqueSchema,
|
|
36
|
+
(opaqueSchema, opaqueSchema) => opaqueSchema,
|
|
37
|
+
) => opaqueSchema = %raw(`(function(){
|
|
38
|
+
const outer = new WeakMap();
|
|
39
|
+
return function(a, b, build){
|
|
40
|
+
let inner = outer.get(a);
|
|
41
|
+
if (inner === undefined) { inner = new WeakMap(); outer.set(a, inner); }
|
|
42
|
+
let v = inner.get(b);
|
|
43
|
+
if (v === undefined) { v = build(a, b); inner.set(b, v); }
|
|
44
|
+
return v;
|
|
45
|
+
};
|
|
46
|
+
})()`)
|
|
47
|
+
let memoizeBySchemaPair = (type a b c, a: a, b: b, build: (a, b) => c): c =>
|
|
48
|
+
_rawMemoize(Obj.magic(a), Obj.magic(b), Obj.magic(build))->Obj.magic
|
|
49
|
+
|
|
21
50
|
let toEventSchema' = (idSchema, eventSchema) =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
51
|
+
memoizeBySchemaPair(idSchema, eventSchema, (idSchema, eventSchema) =>
|
|
52
|
+
S.object(s => {
|
|
53
|
+
id: s.field("id", idSchema),
|
|
54
|
+
meta: s.field("meta", metaSchema),
|
|
55
|
+
event: s.field("event", eventSchema),
|
|
56
|
+
})
|
|
57
|
+
)
|
|
27
58
|
let toCommandSchema' = (idSchema, commandSchema) =>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
59
|
+
memoizeBySchemaPair(idSchema, commandSchema, (idSchema, commandSchema) =>
|
|
60
|
+
S.object(s => {
|
|
61
|
+
id: s.field("id", idSchema),
|
|
62
|
+
meta: s.field("meta", metaSchema),
|
|
63
|
+
command: s.field("command", commandSchema),
|
|
64
|
+
})
|
|
65
|
+
)
|
|
33
66
|
|
|
34
67
|
let decodeEvent' = (json, idSchema, eventSchema) =>
|
|
35
68
|
json->S.parseJsonOrThrow(toEventSchema'(idSchema, eventSchema))
|
package/src/Message.res.mjs
CHANGED
|
@@ -12,20 +12,35 @@ import * as Logger$ReventlessCore from "./util/Logger.res.mjs";
|
|
|
12
12
|
|
|
13
13
|
let logger = Logger$ReventlessCore.fromEnv();
|
|
14
14
|
|
|
15
|
+
let _rawMemoize = ((function(){
|
|
16
|
+
const outer = new WeakMap();
|
|
17
|
+
return function(a, b, build){
|
|
18
|
+
let inner = outer.get(a);
|
|
19
|
+
if (inner === undefined) { inner = new WeakMap(); outer.set(a, inner); }
|
|
20
|
+
let v = inner.get(b);
|
|
21
|
+
if (v === undefined) { v = build(a, b); inner.set(b, v); }
|
|
22
|
+
return v;
|
|
23
|
+
};
|
|
24
|
+
})());
|
|
25
|
+
|
|
26
|
+
function memoizeBySchemaPair(a, b, build) {
|
|
27
|
+
return _rawMemoize(a, b, build);
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
function toEventSchema$p(idSchema, eventSchema) {
|
|
16
|
-
return S.object(s => ({
|
|
31
|
+
return _rawMemoize(idSchema, eventSchema, (idSchema, eventSchema) => S.object(s => ({
|
|
17
32
|
id: s.f("id", idSchema),
|
|
18
33
|
meta: s.f("meta", Message$Reventless.metaSchema),
|
|
19
34
|
event: s.f("event", eventSchema)
|
|
20
|
-
}));
|
|
35
|
+
})));
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
function toCommandSchema$p(idSchema, commandSchema) {
|
|
24
|
-
return S.object(s => ({
|
|
39
|
+
return _rawMemoize(idSchema, commandSchema, (idSchema, commandSchema) => S.object(s => ({
|
|
25
40
|
id: s.f("id", idSchema),
|
|
26
41
|
meta: s.f("meta", Message$Reventless.metaSchema),
|
|
27
42
|
command: s.f("command", commandSchema)
|
|
28
|
-
}));
|
|
43
|
+
})));
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
function decodeEvent$p(json, idSchema, eventSchema) {
|
|
@@ -385,6 +400,8 @@ export {
|
|
|
385
400
|
decode,
|
|
386
401
|
encode,
|
|
387
402
|
InvalidEvent,
|
|
403
|
+
_rawMemoize,
|
|
404
|
+
memoizeBySchemaPair,
|
|
388
405
|
toEventSchema$p,
|
|
389
406
|
toCommandSchema$p,
|
|
390
407
|
decodeEvent$p,
|
package/src/Projection.res
CHANGED
|
@@ -16,7 +16,10 @@ module Mapping = {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Lazy: the message thunk (and any `stateToString` it triggers) runs only when
|
|
20
|
+
// debug logging is enabled, so projection actions don't serialize state on the
|
|
21
|
+
// hot path at the default Info level.
|
|
22
|
+
let logAction = makeStr => log.debugLazy(~comp="Projection", makeStr)
|
|
20
23
|
|
|
21
24
|
// ── @displayName overlay ────────────────────────────────────────────────────
|
|
22
25
|
// When a read model's state schema carries DisplayName metadata, every state
|
|
@@ -86,10 +89,16 @@ let applyChanges = async (
|
|
|
86
89
|
let addedStates = afterStates->Array.filter(state => addedSubIds->Set.has(state->getSubId))
|
|
87
90
|
let addedCount = addedStates->Array.length
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
// Index the after-states by subId once instead of scanning them per
|
|
93
|
+
// before-state (was O(before × after)).
|
|
94
|
+
let afterBySubId = Dict.make()
|
|
95
|
+
afterStates->Array.forEach(after => afterBySubId->Dict.set(after->getSubId, after))
|
|
96
|
+
let changedStates = beforeStates->Array.filterMap(before =>
|
|
97
|
+
switch afterBySubId->Dict.get(before->getSubId) {
|
|
98
|
+
| Some(after) if after != before => Some(after)
|
|
99
|
+
| _ => None
|
|
100
|
+
}
|
|
101
|
+
)
|
|
93
102
|
let changedCount = changedStates->Array.length
|
|
94
103
|
|
|
95
104
|
let batchToSave =
|
|
@@ -99,16 +108,27 @@ let applyChanges = async (
|
|
|
99
108
|
let batchToDelete = deletedSubIds->Array.map(subId => (id, Some((subIdField, subId))))
|
|
100
109
|
let deletedCount = batchToDelete->Array.length
|
|
101
110
|
|
|
102
|
-
logAction(
|
|
103
|
-
`${action}(${id}): beforeStates:${beforeCount->Int.toString} afterStates:${afterCount->Int.toString} added:${addedCount->Int.toString} changed:${changedCount->Int.toString} deleted:${deletedCount->Int.toString}
|
|
111
|
+
logAction(() =>
|
|
112
|
+
`${action}(${id}): beforeStates:${beforeCount->Int.toString} afterStates:${afterCount->Int.toString} added:${addedCount->Int.toString} changed:${changedCount->Int.toString} deleted:${deletedCount->Int.toString}`
|
|
104
113
|
)
|
|
105
114
|
let result = await [deleteBatch(batchToDelete), saveBatch(batchToSave)]->Promise.all
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
// Propagate the first storage failure instead of swallowing it — a failed
|
|
116
|
+
// delete/save was previously reported as a successful projection.
|
|
117
|
+
switch result->Array.find(r =>
|
|
118
|
+
switch r {
|
|
119
|
+
| Error(_) => true
|
|
120
|
+
| Ok(_) => false
|
|
121
|
+
}
|
|
122
|
+
) {
|
|
123
|
+
| Some(err) => err
|
|
124
|
+
| None => Ok()
|
|
108
125
|
}
|
|
109
126
|
}
|
|
110
127
|
|
|
111
|
-
|
|
128
|
+
// Best-effort: a state that can't be JSON-serialized (e.g. carries a function)
|
|
129
|
+
// must not crash the projection just to produce a debug line.
|
|
130
|
+
let stateToString: 'a => string = state =>
|
|
131
|
+
state->JSON.stringifyAny->Option.getOr("<unserializable>")
|
|
112
132
|
let statesToString: array<'a> => string = states =>
|
|
113
133
|
states->Array.map(stateToString)->Array.joinUnsafe(", ")
|
|
114
134
|
|
|
@@ -132,17 +152,17 @@ let handleAction = async (
|
|
|
132
152
|
->Effect.runPromise
|
|
133
153
|
switch action {
|
|
134
154
|
| Ignore =>
|
|
135
|
-
logAction("Ignore")
|
|
155
|
+
logAction(() => "Ignore")
|
|
136
156
|
Ok()
|
|
137
157
|
|
|
138
158
|
| Create(id, state) =>
|
|
139
|
-
logAction(`Create(${id}, ${state->stateToString})`)
|
|
159
|
+
logAction(() => `Create(${id}, ${state->stateToString})`)
|
|
140
160
|
await save(id, state, Init, None)
|
|
141
161
|
| CreateMultiState(id, states) =>
|
|
142
|
-
logAction(
|
|
162
|
+
logAction(() =>
|
|
143
163
|
`CreateMultiState(${id}, ${states
|
|
144
164
|
->Array.map(state => state->stateToString)
|
|
145
|
-
->Array.joinUnsafe(", ")})
|
|
165
|
+
->Array.joinUnsafe(", ")})`
|
|
146
166
|
)
|
|
147
167
|
switch states {
|
|
148
168
|
| [] => Ok()
|
|
@@ -153,23 +173,23 @@ let handleAction = async (
|
|
|
153
173
|
}
|
|
154
174
|
| CreateMany(states) =>
|
|
155
175
|
let batch = states->Array.map(((id, state)) => (id, state, None))
|
|
156
|
-
|
|
157
|
-
batch
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
176
|
+
logAction(() =>
|
|
177
|
+
`CreateMany(${batch
|
|
178
|
+
->Array.map(((id, state, _)) => `(${id},${state->stateToString})`)
|
|
179
|
+
->Array.joinUnsafe(", ")})`
|
|
180
|
+
)
|
|
161
181
|
await saveBatch(batch) // TODO: think about using single saves with saveMode Init
|
|
162
182
|
|
|
163
183
|
| Set(id, state) =>
|
|
164
|
-
logAction(`Set(${id}, ${state->stateToString})`)
|
|
184
|
+
logAction(() => `Set(${id}, ${state->stateToString})`)
|
|
165
185
|
await save(id, state, Any, None)
|
|
166
186
|
| SetMany(ids, set) =>
|
|
167
187
|
let batch = ids->Array.map(id => (id, set(id), None))
|
|
168
|
-
|
|
169
|
-
batch
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
188
|
+
logAction(() =>
|
|
189
|
+
`SetMany(${batch
|
|
190
|
+
->Array.map(((id, state, _)) => `(${id},${state->stateToString})`)
|
|
191
|
+
->Array.joinUnsafe(", ")})`
|
|
192
|
+
)
|
|
173
193
|
await saveBatch(batch)
|
|
174
194
|
|
|
175
195
|
| Update(id, update) =>
|
|
@@ -177,39 +197,39 @@ let handleAction = async (
|
|
|
177
197
|
| Ok(states) =>
|
|
178
198
|
switch states {
|
|
179
199
|
| [] =>
|
|
180
|
-
logAction(`Update Error: No oldState for ${id})`)
|
|
200
|
+
logAction(() => `Update Error: No oldState for ${id})`)
|
|
181
201
|
Error(ReventlessInfra.QueryDb.StaleState)
|
|
182
202
|
| [oldState] =>
|
|
183
203
|
let newState = oldState->update
|
|
184
|
-
logAction(`Update(${id}, ${oldState->stateToString} => ${newState->stateToString})`)
|
|
204
|
+
logAction(() => `Update(${id}, ${oldState->stateToString} => ${newState->stateToString})`)
|
|
185
205
|
await save(id, newState, Overwrite, None)
|
|
186
206
|
| _ =>
|
|
187
|
-
logAction(`Update Error: Multiple oldStates for ${id})`)
|
|
207
|
+
logAction(() => `Update Error: Multiple oldStates for ${id})`)
|
|
188
208
|
Error(ReventlessInfra.QueryDb.StaleState)
|
|
189
209
|
}
|
|
190
210
|
| Error(err) => Error(err)
|
|
191
211
|
}
|
|
192
212
|
| UpdateWithDefault(id, default, update) =>
|
|
193
|
-
|
|
213
|
+
logAction(() => `UpdateWithDefault(${id}, loading ...`)
|
|
194
214
|
switch await loadAtMost(2, id) {
|
|
195
215
|
| Ok(states) =>
|
|
196
216
|
switch states {
|
|
197
217
|
| [] =>
|
|
198
|
-
logAction(`UpdateWithDefault(${id}, default: ${default->stateToString})`)
|
|
218
|
+
logAction(() => `UpdateWithDefault(${id}, default: ${default->stateToString})`)
|
|
199
219
|
await save(id, default, Init, None)
|
|
200
220
|
| [oldState] =>
|
|
201
221
|
let newState = oldState->update
|
|
202
|
-
logAction(
|
|
203
|
-
`UpdateWithDefault(${id}, ${oldState->stateToString} => ${newState->stateToString})
|
|
222
|
+
logAction(() =>
|
|
223
|
+
`UpdateWithDefault(${id}, ${oldState->stateToString} => ${newState->stateToString})`
|
|
204
224
|
)
|
|
205
225
|
await save(id, newState, Overwrite, None)
|
|
206
226
|
| _ =>
|
|
207
|
-
logAction(`UpdateWithDefault Error: Multiple oldStates for ${id})`)
|
|
227
|
+
logAction(() => `UpdateWithDefault Error: Multiple oldStates for ${id})`)
|
|
208
228
|
Error(ReventlessInfra.QueryDb.StaleState)
|
|
209
229
|
}
|
|
210
230
|
| Error(err) =>
|
|
211
|
-
logAction(
|
|
212
|
-
`UpdateWithDefault Error: Couldn't load oldState(s) for ${id}: ${err->QueryDb.storageErrorToString})
|
|
231
|
+
logAction(() =>
|
|
232
|
+
`UpdateWithDefault Error: Couldn't load oldState(s) for ${id}: ${err->QueryDb.storageErrorToString})`
|
|
213
233
|
)
|
|
214
234
|
Error(err)
|
|
215
235
|
}
|
|
@@ -221,25 +241,27 @@ let handleAction = async (
|
|
|
221
241
|
await applyChanges("UpdateMultiState", id, beforeStates, afterStates, operations, subIdConfig)
|
|
222
242
|
|
|
223
243
|
| (Error(err), Some(_)) =>
|
|
224
|
-
logAction(
|
|
225
|
-
`UpdateMultiState Error: Couldn't load states for ${id}: ${err->QueryDb.storageErrorToString})
|
|
244
|
+
logAction(() =>
|
|
245
|
+
`UpdateMultiState Error: Couldn't load states for ${id}: ${err->QueryDb.storageErrorToString})`
|
|
226
246
|
)
|
|
227
247
|
Error(err)
|
|
228
248
|
| (_, None) =>
|
|
229
|
-
logAction("UpdateMultiState Error: Missing SubIdConfig !")
|
|
249
|
+
logAction(() => "UpdateMultiState Error: Missing SubIdConfig !")
|
|
230
250
|
Error(ReventlessInfra.QueryDb.MissingSubIdConfig)
|
|
231
251
|
}
|
|
232
252
|
| Delete(id) =>
|
|
233
|
-
logAction(`Delete(${id})`)
|
|
253
|
+
logAction(() => `Delete(${id})`)
|
|
234
254
|
await delete(id, None)
|
|
235
255
|
| DeleteMany(ids) =>
|
|
236
|
-
logAction(`DeleteMany(${ids->Array.joinUnsafe(", ")})`)
|
|
256
|
+
logAction(() => `DeleteMany(${ids->Array.joinUnsafe(", ")})`)
|
|
237
257
|
await deleteBatch(ids->Array.map(id => (id, None)))
|
|
238
258
|
|
|
239
|
-
//
|
|
259
|
+
// An action reaching here is one this projection engine doesn't implement.
|
|
260
|
+
// Returning Ok() reported the read-model write as done while silently losing
|
|
261
|
+
// it; surface a storage error instead.
|
|
240
262
|
| _ =>
|
|
241
|
-
logAction("Error:
|
|
242
|
-
|
|
263
|
+
logAction(() => "Error: projection action not supported")
|
|
264
|
+
Error(ReventlessInfra.QueryDb.NotSavedToStorage("unsupported projection action"))
|
|
243
265
|
}
|
|
244
266
|
}
|
|
245
267
|
|
|
@@ -261,20 +283,27 @@ let actionsWithId = action =>
|
|
|
261
283
|
|
|
262
284
|
// TODO: add missing actions
|
|
263
285
|
| _ =>
|
|
264
|
-
logAction("Error: Action not yet supported !")
|
|
286
|
+
logAction(() => "Error: Action not yet supported !")
|
|
265
287
|
[]
|
|
266
288
|
}
|
|
267
289
|
|
|
268
290
|
let groupActionsById = actions => {
|
|
269
291
|
let allActionsWithId = actions->Array.map(action => action->actionsWithId)->Array.flat
|
|
270
|
-
|
|
271
|
-
|
|
292
|
+
// One pass into a dict keyed by id (was O(ids × actions): a filterMap over
|
|
293
|
+
// every action per unique id). The id ordering is unchanged — still the sorted
|
|
294
|
+
// Belt.Set order — and each group preserves the actions' original order.
|
|
295
|
+
let groups = Dict.make()
|
|
296
|
+
allActionsWithId->Array.forEach(((id, action)) =>
|
|
297
|
+
switch groups->Dict.get(id) {
|
|
298
|
+
| Some(arr) => arr->Array.push(action)
|
|
299
|
+
| None => groups->Dict.set(id, [action])
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
groups
|
|
303
|
+
->Dict.keysToArray
|
|
272
304
|
->Belt.Set.String.fromArray
|
|
273
305
|
->Belt.Set.String.toArray
|
|
274
|
-
->Array.map(id => (
|
|
275
|
-
id,
|
|
276
|
-
allActionsWithId->Array.filterMap(((actionId, action)) => actionId == id ? Some(action) : None),
|
|
277
|
-
))
|
|
306
|
+
->Array.map(id => (id, groups->Dict.getUnsafe(id)))
|
|
278
307
|
}
|
|
279
308
|
|
|
280
309
|
let optimizeActions = actions => {
|
|
@@ -285,11 +314,10 @@ let optimizeActions = actions => {
|
|
|
285
314
|
[action]
|
|
286
315
|
} else {
|
|
287
316
|
let lastAction = optimizedActions->Array.getUnsafe(optimizedActionsCount - 1)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
317
|
+
// Everything BEFORE the last action. `~end` is exclusive, so this must be
|
|
318
|
+
// `count - 1`; using `count` kept `lastAction` in the slice while the merged
|
|
319
|
+
// action was also appended, applying the last action twice.
|
|
320
|
+
let previousActions = optimizedActions->Array.slice(~start=0, ~end=optimizedActionsCount - 1)
|
|
293
321
|
|
|
294
322
|
switch (lastAction, action) {
|
|
295
323
|
// SINGLE STATES
|