@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
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.136",
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-fast-csv": "1.2.0-alpha.11",
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-hash-object": "1.2.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
- "@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.22",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.63"
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.48"
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
- S.object(s => {
23
- id: s.field("id", idSchema),
24
- meta: s.field("meta", metaSchema),
25
- event: s.field("event", eventSchema),
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
- S.object(s => {
29
- id: s.field("id", idSchema),
30
- meta: s.field("meta", metaSchema),
31
- command: s.field("command", commandSchema),
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))
@@ -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,
@@ -16,7 +16,10 @@ module Mapping = {
16
16
  }
17
17
  }
18
18
 
19
- let logAction = str => log.debug(~comp="Projection", str)
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
- let changedStates = beforeStates->Array.filterMap(before => {
90
- let beforeSubId = before->getSubId
91
- afterStates->Array.find(after => after->getSubId == beforeSubId && after != before)
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
- let stateToString: 'a => string = state => state->JSON.stringifyAny->Option.getOrThrow
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
- let statesStr =
165
- batch
166
- ->Array.map(((id, state, _)) => `(${id},${state->stateToString})`)
167
- ->Array.joinUnsafe(", ")
168
- logAction(`CreateMany(${statesStr})`)
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
- let statesStr =
177
- batch
178
- ->Array.map(((id, state, _)) => `(${id},${state->stateToString})`)
179
- ->Array.joinUnsafe(", ")
180
- logAction(`SetMany(${statesStr})`)
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
- log.debug(~comp="Projection", `UpdateWithDefault(${id}, loading ...`)
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
- let ids = allActionsWithId->Array.map(((id, _)) => id)
281
- ids
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 => {
@@ -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(str) {
34
- log.debug("Projection", undefined, str);
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 beforeSubId = getSubId(before);
153
- return afterStates.find(after => {
154
- if (getSubId(after) === beforeSubId) {
155
- return Primitive_object.notequal(after, before);
156
- } else {
157
- return false;
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
- logAction(action + `(` + id + `): beforeStates:` + beforeCount.toString() + ` afterStates:` + afterCount.toString() + ` added:` + addedCount.toString() + ` changed:` + changedCount.toString() + ` deleted:` + deletedCount.toString());
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.getOrThrow(JSON.stringify(state), undefined);
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
- logAction("Ignore");
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
- logAction(`Create(` + id + `, ` + stateToString(state) + `)`);
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
- let statesStr = batch.map(param => `(` + param[0] + `,` + stateToString(param[1]) + `)`).join(", ");
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
- logAction(`Update Error: Multiple oldStates for ` + id$1 + `)`);
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
- logAction(`Update Error: No oldState for ` + id$1 + `)`);
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
- logAction(`Update(` + id$1 + `, ` + stateToString(oldState) + ` => ` + stateToString(newState) + `)`);
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.debug("Projection", undefined, `UpdateWithDefault(` + id$2 + `, loading ...`);
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
- logAction(`UpdateWithDefault Error: Multiple oldStates for ` + id$2 + `)`);
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
- logAction(`UpdateWithDefault(` + id$2 + `, default: ` + stateToString($$default) + `)`);
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
- logAction(`UpdateWithDefault(` + id$2 + `, ` + stateToString(oldState$1) + ` => ` + stateToString(newState$1) + `)`);
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
- logAction(`UpdateWithDefault Error: Couldn't load oldState(s) for ` + id$2 + `: ` + QueryDb$ReventlessCore.storageErrorToString(err) + `)`);
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
- logAction(`Set(` + id$3 + `, ` + stateToString(state$1) + `)`);
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
- let statesStr$1 = batch$1.map(param => `(` + param[0] + `,` + stateToString(param[1]) + `)`).join(", ");
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
- logAction(`Delete(` + id$4 + `)`);
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
- logAction(`DeleteMany(` + ids.join(", ") + `)`);
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
- logAction(`CreateMultiState(` + id$5 + `, ` + states$4.map(stateToString).join(", ") + `)`);
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
- logAction("UpdateMultiState Error: Missing SubIdConfig !");
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
- logAction(`UpdateMultiState Error: Couldn't load states for ` + id$6 + `: ` + QueryDb$ReventlessCore.storageErrorToString(err$1) + `)`);
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
- logAction("UpdateMultiState Error: Missing SubIdConfig !");
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
- logAction("Error: projection action not supported");
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
- logAction("Error: Action not yet supported !");
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 ids = allActionsWithId.map(param => param[0]);
450
- return Belt_SetString.toArray(Belt_SetString.fromArray(ids)).map(id => [
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
- Stdlib_Array.filterMap(allActionsWithId, param => {
453
- if (param[0] === id) {
454
- return param[1];
455
- }
456
- })
460
+ groups[id]
457
461
  ]);
458
462
  }
459
463