@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
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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
|
+
|
|
6
18
|
# 3.0.0-alpha.136 (2026-07-02)
|
|
7
19
|
|
|
8
20
|
### Bug Fixes
|
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": {
|
|
@@ -29,21 +29,21 @@
|
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.24",
|
|
32
|
-
"@reventlessdev/rescript-
|
|
32
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
|
|
33
33
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.5",
|
|
34
34
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
|
|
35
|
-
"@reventlessdev/rescript-
|
|
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
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.83",
|
|
39
38
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.13",
|
|
40
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.
|
|
41
|
-
"@reventlessdev/reventless-spec": "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,8 +108,8 @@ 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
115
|
// Propagate the first storage failure instead of swallowing it — a failed
|
|
@@ -116,7 +125,10 @@ let applyChanges = async (
|
|
|
116
125
|
}
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
|
|
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>")
|
|
120
132
|
let statesToString: array<'a> => string = states =>
|
|
121
133
|
states->Array.map(stateToString)->Array.joinUnsafe(", ")
|
|
122
134
|
|
|
@@ -140,17 +152,17 @@ let handleAction = async (
|
|
|
140
152
|
->Effect.runPromise
|
|
141
153
|
switch action {
|
|
142
154
|
| Ignore =>
|
|
143
|
-
logAction("Ignore")
|
|
155
|
+
logAction(() => "Ignore")
|
|
144
156
|
Ok()
|
|
145
157
|
|
|
146
158
|
| Create(id, state) =>
|
|
147
|
-
logAction(`Create(${id}, ${state->stateToString})`)
|
|
159
|
+
logAction(() => `Create(${id}, ${state->stateToString})`)
|
|
148
160
|
await save(id, state, Init, None)
|
|
149
161
|
| CreateMultiState(id, states) =>
|
|
150
|
-
logAction(
|
|
162
|
+
logAction(() =>
|
|
151
163
|
`CreateMultiState(${id}, ${states
|
|
152
164
|
->Array.map(state => state->stateToString)
|
|
153
|
-
->Array.joinUnsafe(", ")})
|
|
165
|
+
->Array.joinUnsafe(", ")})`
|
|
154
166
|
)
|
|
155
167
|
switch states {
|
|
156
168
|
| [] => Ok()
|
|
@@ -161,23 +173,23 @@ let handleAction = async (
|
|
|
161
173
|
}
|
|
162
174
|
| CreateMany(states) =>
|
|
163
175
|
let batch = states->Array.map(((id, state)) => (id, state, None))
|
|
164
|
-
|
|
165
|
-
batch
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
logAction(() =>
|
|
177
|
+
`CreateMany(${batch
|
|
178
|
+
->Array.map(((id, state, _)) => `(${id},${state->stateToString})`)
|
|
179
|
+
->Array.joinUnsafe(", ")})`
|
|
180
|
+
)
|
|
169
181
|
await saveBatch(batch) // TODO: think about using single saves with saveMode Init
|
|
170
182
|
|
|
171
183
|
| Set(id, state) =>
|
|
172
|
-
logAction(`Set(${id}, ${state->stateToString})`)
|
|
184
|
+
logAction(() => `Set(${id}, ${state->stateToString})`)
|
|
173
185
|
await save(id, state, Any, None)
|
|
174
186
|
| SetMany(ids, set) =>
|
|
175
187
|
let batch = ids->Array.map(id => (id, set(id), None))
|
|
176
|
-
|
|
177
|
-
batch
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
188
|
+
logAction(() =>
|
|
189
|
+
`SetMany(${batch
|
|
190
|
+
->Array.map(((id, state, _)) => `(${id},${state->stateToString})`)
|
|
191
|
+
->Array.joinUnsafe(", ")})`
|
|
192
|
+
)
|
|
181
193
|
await saveBatch(batch)
|
|
182
194
|
|
|
183
195
|
| Update(id, update) =>
|
|
@@ -185,39 +197,39 @@ let handleAction = async (
|
|
|
185
197
|
| Ok(states) =>
|
|
186
198
|
switch states {
|
|
187
199
|
| [] =>
|
|
188
|
-
logAction(`Update Error: No oldState for ${id})`)
|
|
200
|
+
logAction(() => `Update Error: No oldState for ${id})`)
|
|
189
201
|
Error(ReventlessInfra.QueryDb.StaleState)
|
|
190
202
|
| [oldState] =>
|
|
191
203
|
let newState = oldState->update
|
|
192
|
-
logAction(`Update(${id}, ${oldState->stateToString} => ${newState->stateToString})`)
|
|
204
|
+
logAction(() => `Update(${id}, ${oldState->stateToString} => ${newState->stateToString})`)
|
|
193
205
|
await save(id, newState, Overwrite, None)
|
|
194
206
|
| _ =>
|
|
195
|
-
logAction(`Update Error: Multiple oldStates for ${id})`)
|
|
207
|
+
logAction(() => `Update Error: Multiple oldStates for ${id})`)
|
|
196
208
|
Error(ReventlessInfra.QueryDb.StaleState)
|
|
197
209
|
}
|
|
198
210
|
| Error(err) => Error(err)
|
|
199
211
|
}
|
|
200
212
|
| UpdateWithDefault(id, default, update) =>
|
|
201
|
-
|
|
213
|
+
logAction(() => `UpdateWithDefault(${id}, loading ...`)
|
|
202
214
|
switch await loadAtMost(2, id) {
|
|
203
215
|
| Ok(states) =>
|
|
204
216
|
switch states {
|
|
205
217
|
| [] =>
|
|
206
|
-
logAction(`UpdateWithDefault(${id}, default: ${default->stateToString})`)
|
|
218
|
+
logAction(() => `UpdateWithDefault(${id}, default: ${default->stateToString})`)
|
|
207
219
|
await save(id, default, Init, None)
|
|
208
220
|
| [oldState] =>
|
|
209
221
|
let newState = oldState->update
|
|
210
|
-
logAction(
|
|
211
|
-
`UpdateWithDefault(${id}, ${oldState->stateToString} => ${newState->stateToString})
|
|
222
|
+
logAction(() =>
|
|
223
|
+
`UpdateWithDefault(${id}, ${oldState->stateToString} => ${newState->stateToString})`
|
|
212
224
|
)
|
|
213
225
|
await save(id, newState, Overwrite, None)
|
|
214
226
|
| _ =>
|
|
215
|
-
logAction(`UpdateWithDefault Error: Multiple oldStates for ${id})`)
|
|
227
|
+
logAction(() => `UpdateWithDefault Error: Multiple oldStates for ${id})`)
|
|
216
228
|
Error(ReventlessInfra.QueryDb.StaleState)
|
|
217
229
|
}
|
|
218
230
|
| Error(err) =>
|
|
219
|
-
logAction(
|
|
220
|
-
`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})`
|
|
221
233
|
)
|
|
222
234
|
Error(err)
|
|
223
235
|
}
|
|
@@ -229,26 +241,26 @@ let handleAction = async (
|
|
|
229
241
|
await applyChanges("UpdateMultiState", id, beforeStates, afterStates, operations, subIdConfig)
|
|
230
242
|
|
|
231
243
|
| (Error(err), Some(_)) =>
|
|
232
|
-
logAction(
|
|
233
|
-
`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})`
|
|
234
246
|
)
|
|
235
247
|
Error(err)
|
|
236
248
|
| (_, None) =>
|
|
237
|
-
logAction("UpdateMultiState Error: Missing SubIdConfig !")
|
|
249
|
+
logAction(() => "UpdateMultiState Error: Missing SubIdConfig !")
|
|
238
250
|
Error(ReventlessInfra.QueryDb.MissingSubIdConfig)
|
|
239
251
|
}
|
|
240
252
|
| Delete(id) =>
|
|
241
|
-
logAction(`Delete(${id})`)
|
|
253
|
+
logAction(() => `Delete(${id})`)
|
|
242
254
|
await delete(id, None)
|
|
243
255
|
| DeleteMany(ids) =>
|
|
244
|
-
logAction(`DeleteMany(${ids->Array.joinUnsafe(", ")})`)
|
|
256
|
+
logAction(() => `DeleteMany(${ids->Array.joinUnsafe(", ")})`)
|
|
245
257
|
await deleteBatch(ids->Array.map(id => (id, None)))
|
|
246
258
|
|
|
247
259
|
// An action reaching here is one this projection engine doesn't implement.
|
|
248
260
|
// Returning Ok() reported the read-model write as done while silently losing
|
|
249
261
|
// it; surface a storage error instead.
|
|
250
262
|
| _ =>
|
|
251
|
-
logAction("Error: projection action not supported")
|
|
263
|
+
logAction(() => "Error: projection action not supported")
|
|
252
264
|
Error(ReventlessInfra.QueryDb.NotSavedToStorage("unsupported projection action"))
|
|
253
265
|
}
|
|
254
266
|
}
|
|
@@ -271,20 +283,27 @@ let actionsWithId = action =>
|
|
|
271
283
|
|
|
272
284
|
// TODO: add missing actions
|
|
273
285
|
| _ =>
|
|
274
|
-
logAction("Error: Action not yet supported !")
|
|
286
|
+
logAction(() => "Error: Action not yet supported !")
|
|
275
287
|
[]
|
|
276
288
|
}
|
|
277
289
|
|
|
278
290
|
let groupActionsById = actions => {
|
|
279
291
|
let allActionsWithId = actions->Array.map(action => action->actionsWithId)->Array.flat
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
282
304
|
->Belt.Set.String.fromArray
|
|
283
305
|
->Belt.Set.String.toArray
|
|
284
|
-
->Array.map(id => (
|
|
285
|
-
id,
|
|
286
|
-
allActionsWithId->Array.filterMap(((actionId, action)) => actionId == id ? Some(action) : None),
|
|
287
|
-
))
|
|
306
|
+
->Array.map(id => (id, groups->Dict.getUnsafe(id)))
|
|
288
307
|
}
|
|
289
308
|
|
|
290
309
|
let optimizeActions = actions => {
|
package/src/Projection.res.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import * as Stream$1 from "effect/Stream";
|
|
|
10
10
|
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
11
11
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
12
12
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
13
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
13
14
|
import * as Logger$ReventlessCore from "./util/Logger.res.mjs";
|
|
14
15
|
import * as DisplayName$Reventless from "@reventlessdev/reventless-spec/src/components/DisplayName.res.mjs";
|
|
15
16
|
import * as Message$ReventlessCore from "./Message.res.mjs";
|
|
@@ -30,8 +31,8 @@ let Mapping = {
|
|
|
30
31
|
MakeGenericSource: MakeGenericSource
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
function logAction(
|
|
34
|
-
log.
|
|
34
|
+
function logAction(makeStr) {
|
|
35
|
+
log.debugLazy("Projection", makeStr);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function overlayDisplayName(state, stateSchema, spec) {
|
|
@@ -148,15 +149,19 @@ async function applyChanges(action, id, beforeStates, afterStates, param, param$
|
|
|
148
149
|
let addedSubIds = Belt_SetString.diff(afterSubIds, beforeSubIds);
|
|
149
150
|
let addedStates = afterStates.filter(state => Belt_SetString.has(addedSubIds, getSubId(state)));
|
|
150
151
|
let addedCount = addedStates.length;
|
|
152
|
+
let afterBySubId = {};
|
|
153
|
+
afterStates.forEach(after => {
|
|
154
|
+
afterBySubId[getSubId(after)] = after;
|
|
155
|
+
});
|
|
151
156
|
let changedStates = Stdlib_Array.filterMap(beforeStates, before => {
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
157
|
+
let after = afterBySubId[getSubId(before)];
|
|
158
|
+
if (after === undefined) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let after$1 = Primitive_option.valFromOption(after);
|
|
162
|
+
if (Primitive_object.notequal(after$1, before)) {
|
|
163
|
+
return Primitive_option.some(after$1);
|
|
164
|
+
}
|
|
160
165
|
});
|
|
161
166
|
let changedCount = changedStates.length;
|
|
162
167
|
let batchToSave = changedStates.concat(addedStates).map(state => [
|
|
@@ -173,7 +178,7 @@ async function applyChanges(action, id, beforeStates, afterStates, param, param$
|
|
|
173
178
|
]
|
|
174
179
|
]);
|
|
175
180
|
let deletedCount = batchToDelete.length;
|
|
176
|
-
|
|
181
|
+
log.debugLazy("Projection", () => action + `(` + id + `): beforeStates:` + beforeCount.toString() + ` afterStates:` + afterCount.toString() + ` added:` + addedCount.toString() + ` changed:` + changedCount.toString() + ` deleted:` + deletedCount.toString());
|
|
177
182
|
let result = await Promise.all([
|
|
178
183
|
param.deleteBatch(batchToDelete),
|
|
179
184
|
param.saveBatch(batchToSave)
|
|
@@ -190,7 +195,7 @@ async function applyChanges(action, id, beforeStates, afterStates, param, param$
|
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
function stateToString(state) {
|
|
193
|
-
return Stdlib_Option.
|
|
198
|
+
return Stdlib_Option.getOr(JSON.stringify(state), "<unserializable>");
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
function statesToString(states) {
|
|
@@ -216,7 +221,7 @@ async function handleAction(action, operations, subIdConfig) {
|
|
|
216
221
|
_0: e
|
|
217
222
|
})));
|
|
218
223
|
if (typeof action !== "object") {
|
|
219
|
-
|
|
224
|
+
log.debugLazy("Projection", () => "Ignore");
|
|
220
225
|
return {
|
|
221
226
|
TAG: "Ok",
|
|
222
227
|
_0: undefined
|
|
@@ -226,7 +231,7 @@ async function handleAction(action, operations, subIdConfig) {
|
|
|
226
231
|
case "Create" :
|
|
227
232
|
let state = action._1;
|
|
228
233
|
let id = action._0;
|
|
229
|
-
|
|
234
|
+
log.debugLazy("Projection", () => `Create(` + id + `, ` + stateToString(state) + `)`);
|
|
230
235
|
return await save(id, state, "Init", undefined);
|
|
231
236
|
case "CreateMany" :
|
|
232
237
|
let batch = action._0.map(param => [
|
|
@@ -234,8 +239,7 @@ async function handleAction(action, operations, subIdConfig) {
|
|
|
234
239
|
param[1],
|
|
235
240
|
undefined
|
|
236
241
|
]);
|
|
237
|
-
|
|
238
|
-
logAction(`CreateMany(` + statesStr + `)`);
|
|
242
|
+
log.debugLazy("Projection", () => `CreateMany(` + batch.map(param => `(` + param[0] + `,` + stateToString(param[1]) + `)`).join(", ") + `)`);
|
|
239
243
|
return await saveBatch(batch);
|
|
240
244
|
case "Update" :
|
|
241
245
|
let id$1 = action._0;
|
|
@@ -250,13 +254,13 @@ async function handleAction(action, operations, subIdConfig) {
|
|
|
250
254
|
let len = states$1.length;
|
|
251
255
|
if (len !== 1) {
|
|
252
256
|
if (len !== 0) {
|
|
253
|
-
|
|
257
|
+
log.debugLazy("Projection", () => `Update Error: Multiple oldStates for ` + id$1 + `)`);
|
|
254
258
|
return {
|
|
255
259
|
TAG: "Error",
|
|
256
260
|
_0: "StaleState"
|
|
257
261
|
};
|
|
258
262
|
} else {
|
|
259
|
-
|
|
263
|
+
log.debugLazy("Projection", () => `Update Error: No oldState for ` + id$1 + `)`);
|
|
260
264
|
return {
|
|
261
265
|
TAG: "Error",
|
|
262
266
|
_0: "StaleState"
|
|
@@ -265,35 +269,35 @@ async function handleAction(action, operations, subIdConfig) {
|
|
|
265
269
|
}
|
|
266
270
|
let oldState = states$1[0];
|
|
267
271
|
let newState = action._1(oldState);
|
|
268
|
-
|
|
272
|
+
log.debugLazy("Projection", () => `Update(` + id$1 + `, ` + stateToString(oldState) + ` => ` + stateToString(newState) + `)`);
|
|
269
273
|
return await save(id$1, newState, "Overwrite", undefined);
|
|
270
274
|
case "UpdateWithDefault" :
|
|
271
275
|
let $$default = action._1;
|
|
272
276
|
let id$2 = action._0;
|
|
273
|
-
log.
|
|
277
|
+
log.debugLazy("Projection", () => `UpdateWithDefault(` + id$2 + `, loading ...`);
|
|
274
278
|
let states$2 = await loadAtMost(2, id$2);
|
|
275
279
|
if (states$2.TAG === "Ok") {
|
|
276
280
|
let states$3 = states$2._0;
|
|
277
281
|
let len$1 = states$3.length;
|
|
278
282
|
if (len$1 !== 1) {
|
|
279
283
|
if (len$1 !== 0) {
|
|
280
|
-
|
|
284
|
+
log.debugLazy("Projection", () => `UpdateWithDefault Error: Multiple oldStates for ` + id$2 + `)`);
|
|
281
285
|
return {
|
|
282
286
|
TAG: "Error",
|
|
283
287
|
_0: "StaleState"
|
|
284
288
|
};
|
|
285
289
|
} else {
|
|
286
|
-
|
|
290
|
+
log.debugLazy("Projection", () => `UpdateWithDefault(` + id$2 + `, default: ` + stateToString($$default) + `)`);
|
|
287
291
|
return await save(id$2, $$default, "Init", undefined);
|
|
288
292
|
}
|
|
289
293
|
}
|
|
290
294
|
let oldState$1 = states$3[0];
|
|
291
295
|
let newState$1 = action._2(oldState$1);
|
|
292
|
-
|
|
296
|
+
log.debugLazy("Projection", () => `UpdateWithDefault(` + id$2 + `, ` + stateToString(oldState$1) + ` => ` + stateToString(newState$1) + `)`);
|
|
293
297
|
return await save(id$2, newState$1, "Overwrite", undefined);
|
|
294
298
|
}
|
|
295
299
|
let err = states$2._0;
|
|
296
|
-
|
|
300
|
+
log.debugLazy("Projection", () => `UpdateWithDefault Error: Couldn't load oldState(s) for ` + id$2 + `: ` + QueryDb$ReventlessCore.storageErrorToString(err) + `)`);
|
|
297
301
|
return {
|
|
298
302
|
TAG: "Error",
|
|
299
303
|
_0: err
|
|
@@ -301,7 +305,7 @@ async function handleAction(action, operations, subIdConfig) {
|
|
|
301
305
|
case "Set" :
|
|
302
306
|
let state$1 = action._1;
|
|
303
307
|
let id$3 = action._0;
|
|
304
|
-
|
|
308
|
+
log.debugLazy("Projection", () => `Set(` + id$3 + `, ` + stateToString(state$1) + `)`);
|
|
305
309
|
return await save(id$3, state$1, "Any", undefined);
|
|
306
310
|
case "SetMany" :
|
|
307
311
|
let set = action._1;
|
|
@@ -310,16 +314,15 @@ async function handleAction(action, operations, subIdConfig) {
|
|
|
310
314
|
set(id),
|
|
311
315
|
undefined
|
|
312
316
|
]);
|
|
313
|
-
|
|
314
|
-
logAction(`SetMany(` + statesStr$1 + `)`);
|
|
317
|
+
log.debugLazy("Projection", () => `SetMany(` + batch$1.map(param => `(` + param[0] + `,` + stateToString(param[1]) + `)`).join(", ") + `)`);
|
|
315
318
|
return await saveBatch(batch$1);
|
|
316
319
|
case "Delete" :
|
|
317
320
|
let id$4 = action._0;
|
|
318
|
-
|
|
321
|
+
log.debugLazy("Projection", () => `Delete(` + id$4 + `)`);
|
|
319
322
|
return await operations.delete(id$4, undefined);
|
|
320
323
|
case "DeleteMany" :
|
|
321
324
|
let ids = action._0;
|
|
322
|
-
|
|
325
|
+
log.debugLazy("Projection", () => `DeleteMany(` + ids.join(", ") + `)`);
|
|
323
326
|
return await operations.deleteBatch(ids.map(id => [
|
|
324
327
|
id,
|
|
325
328
|
undefined
|
|
@@ -327,7 +330,7 @@ async function handleAction(action, operations, subIdConfig) {
|
|
|
327
330
|
case "CreateMultiState" :
|
|
328
331
|
let states$4 = action._1;
|
|
329
332
|
let id$5 = action._0;
|
|
330
|
-
|
|
333
|
+
log.debugLazy("Projection", () => `CreateMultiState(` + id$5 + `, ` + states$4.map(stateToString).join(", ") + `)`);
|
|
331
334
|
let len$2 = states$4.length;
|
|
332
335
|
if (len$2 !== 1) {
|
|
333
336
|
if (len$2 === 0) {
|
|
@@ -354,27 +357,21 @@ async function handleAction(action, operations, subIdConfig) {
|
|
|
354
357
|
let afterStates = action._1(states$5);
|
|
355
358
|
return await applyChanges("UpdateMultiState", id$6, states$5, afterStates, operations, subIdConfig);
|
|
356
359
|
}
|
|
357
|
-
|
|
358
|
-
return {
|
|
359
|
-
TAG: "Error",
|
|
360
|
-
_0: "MissingSubIdConfig"
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
if (subIdConfig !== undefined) {
|
|
360
|
+
} else if (subIdConfig !== undefined) {
|
|
364
361
|
let err$1 = match._0;
|
|
365
|
-
|
|
362
|
+
log.debugLazy("Projection", () => `UpdateMultiState Error: Couldn't load states for ` + id$6 + `: ` + QueryDb$ReventlessCore.storageErrorToString(err$1) + `)`);
|
|
366
363
|
return {
|
|
367
364
|
TAG: "Error",
|
|
368
365
|
_0: err$1
|
|
369
366
|
};
|
|
370
367
|
}
|
|
371
|
-
|
|
368
|
+
log.debugLazy("Projection", () => "UpdateMultiState Error: Missing SubIdConfig !");
|
|
372
369
|
return {
|
|
373
370
|
TAG: "Error",
|
|
374
371
|
_0: "MissingSubIdConfig"
|
|
375
372
|
};
|
|
376
373
|
default:
|
|
377
|
-
|
|
374
|
+
log.debugLazy("Projection", () => "Error: projection action not supported");
|
|
378
375
|
return {
|
|
379
376
|
TAG: "Error",
|
|
380
377
|
_0: {
|
|
@@ -424,8 +421,7 @@ function actionsWithId(action) {
|
|
|
424
421
|
case "UpdateManyWithDefault" :
|
|
425
422
|
case "DeleteIf" :
|
|
426
423
|
case "DeleteManyIf" :
|
|
427
|
-
|
|
428
|
-
return [];
|
|
424
|
+
break;
|
|
429
425
|
case "UpdateManyMultiStates" :
|
|
430
426
|
let update = action._1;
|
|
431
427
|
return action._0.map(id => [
|
|
@@ -442,18 +438,26 @@ function actionsWithId(action) {
|
|
|
442
438
|
action
|
|
443
439
|
]];
|
|
444
440
|
}
|
|
441
|
+
log.debugLazy("Projection", () => "Error: Action not yet supported !");
|
|
442
|
+
return [];
|
|
445
443
|
}
|
|
446
444
|
|
|
447
445
|
function groupActionsById(actions) {
|
|
448
446
|
let allActionsWithId = actions.map(actionsWithId).flat();
|
|
449
|
-
let
|
|
450
|
-
|
|
447
|
+
let groups = {};
|
|
448
|
+
allActionsWithId.forEach(param => {
|
|
449
|
+
let action = param[1];
|
|
450
|
+
let id = param[0];
|
|
451
|
+
let arr = groups[id];
|
|
452
|
+
if (arr !== undefined) {
|
|
453
|
+
arr.push(action);
|
|
454
|
+
} else {
|
|
455
|
+
groups[id] = [action];
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
return Belt_SetString.toArray(Belt_SetString.fromArray(Object.keys(groups))).map(id => [
|
|
451
459
|
id,
|
|
452
|
-
|
|
453
|
-
if (param[0] === id) {
|
|
454
|
-
return param[1];
|
|
455
|
-
}
|
|
456
|
-
})
|
|
460
|
+
groups[id]
|
|
457
461
|
]);
|
|
458
462
|
}
|
|
459
463
|
|