@reventlessdev/reventless-local 3.0.0-alpha.114 → 3.0.0-alpha.116

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 (65) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res +116 -67
  4. package/src/Platform.res.mjs +114 -190
  5. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +102 -17
  6. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +105 -22
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +123 -40
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +96 -24
  9. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +33 -0
  10. package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +44 -0
  11. package/src/adapter/EventLog/EventLogStorage_InMemory.res +24 -3
  12. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +21 -4
  13. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +148 -27
  14. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +144 -34
  15. package/src/adapter/LocalBus.res +85 -3
  16. package/src/adapter/LocalBus.res.mjs +220 -66
  17. package/src/adapter/ProjectionCheckpoint.res +350 -0
  18. package/src/adapter/ProjectionCheckpoint.res.mjs +327 -0
  19. package/src/adapter/ProjectionPending.res +60 -0
  20. package/src/adapter/ProjectionPending.res.mjs +64 -0
  21. package/src/adapter/QueryDb/QueryDbListQuery.res +261 -0
  22. package/src/adapter/QueryDb/QueryDbListQuery.res.mjs +212 -0
  23. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +54 -286
  24. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +28 -190
  25. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +128 -10
  26. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +123 -23
  27. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +187 -12
  28. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +120 -7
  29. package/src/adapter/SqliteDriver.res +17 -2
  30. package/src/adapter/SqliteDriver.res.mjs +10 -2
  31. package/src/components/DcbEventLog_Builder.res +7 -1
  32. package/src/components/DcbEventLog_Builder.res.mjs +3 -3
  33. package/src/components/ReadModel_Builder.res +3 -1
  34. package/src/components/ReadModel_Builder.res.mjs +13 -4
  35. package/src/components/StateViewSlice_Builder.res +4 -1
  36. package/src/components/StateViewSlice_Builder.res.mjs +9 -3
  37. package/src/test/Mocks/MockEventLogStorage.res +20 -3
  38. package/src/test/Mocks/MockEventLogStorage.res.mjs +25 -3
  39. package/tests/PluginEventDecodeTest.res +51 -0
  40. package/tests/PluginEventDecodeTest.res.mjs +63 -0
  41. package/tests/adapter/BackendParityTest.res +64 -0
  42. package/tests/adapter/BackendParityTest.res.mjs +45 -0
  43. package/tests/adapter/DcbEventLogStorageSqliteTest.res +73 -0
  44. package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +86 -0
  45. package/tests/adapter/DcbEventLogStorageTest.res +46 -0
  46. package/tests/adapter/DcbEventLogStorageTest.res.mjs +63 -0
  47. package/tests/adapter/EventLogSnapshotParityTest.res +81 -0
  48. package/tests/adapter/EventLogSnapshotParityTest.res.mjs +141 -0
  49. package/tests/adapter/EventLogStorageSqliteTest.res +132 -2
  50. package/tests/adapter/EventLogStorageSqliteTest.res.mjs +195 -1
  51. package/tests/adapter/LocalBusPubSubTest.res +31 -0
  52. package/tests/adapter/LocalBusPubSubTest.res.mjs +28 -0
  53. package/tests/adapter/ProjectionCheckpointTest.res +422 -0
  54. package/tests/adapter/ProjectionCheckpointTest.res.mjs +401 -0
  55. package/tests/adapter/QueryDbGsiTtlTest.res +78 -0
  56. package/tests/adapter/QueryDbGsiTtlTest.res.mjs +77 -0
  57. package/tests/adapter/QueryDbListPushdownParityTest.res +197 -0
  58. package/tests/adapter/QueryDbListPushdownParityTest.res.mjs +292 -0
  59. package/tests/adapter/QueryDbListResolverTest.res +76 -0
  60. package/tests/adapter/QueryDbListResolverTest.res.mjs +84 -0
  61. package/tests/components/aggregate/AggregateFixtures.res +4 -0
  62. package/tests/components/aggregate/AggregateFixtures.res.mjs +1 -0
  63. package/tests/components/eventlog/EventLogAppendStreamTest.res.mjs +4 -4
  64. package/tests/components/eventlog/EventLogStreamTest.res.mjs +8 -8
  65. package/tests/plugin/PluginBehavior_GWT.res.mjs +1 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,36 @@
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.116 (2026-07-03)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **local:** decode plugin events from the bus envelope in subscribeToPluginEvents ([0f24dc1](https://github.com/ReventlessDev/reventless-core/commit/0f24dc129bf2631b94c300d7d5f6ceba3a9bb9f7))
11
+ ### Features
12
+
13
+ * **core,local,aws:** EventLog snapshot storage surface, all backends (aggregate-snapshotting plan, steps 3+4) ([b6e50e2](https://github.com/ReventlessDev/reventless-core/commit/b6e50e2e5fe50b7372e54811f883fa91f6758dd1))
14
+ * **local:** DCB-axis projection checkpoints + startup catch-up (plan B5) ([8f9b11d](https://github.com/ReventlessDev/reventless-core/commit/8f9b11d24627bb6cc105d37af4974745444b80ba))
15
+ * **local:** projection checkpoints + startup catch-up under SQLite (plan B5) ([d28f30e](https://github.com/ReventlessDev/reventless-core/commit/d28f30e25d43cbf378b3aab091b1979dbb449a4f))
16
+ ### Performance Improvements
17
+
18
+ * **local:** DCB in-memory posting lists for tag/type queries (plan B4) ([7859fed](https://github.com/ReventlessDev/reventless-core/commit/7859fede8ad6ab44f657d4d9dd44fba08e85e3aa))
19
+ * **local:** DCB SQLite batched tag reads + EXISTS conflict check (plan B4) ([c9d647b](https://github.com/ReventlessDev/reventless-core/commit/c9d647b73adba4e054edbeba83dfb9c8111e4dfb))
20
+ * **local:** EventLog SQLite MAX(seq_nr)+1 + batched appendStream (plan B4) ([7b24fb1](https://github.com/ReventlessDev/reventless-core/commit/7b24fb12b78b79854d3c345a74f1eab68a28e745))
21
+ * **local:** SQLite connection-list push-down with parity harness (plan B4 complete) ([d78d428](https://github.com/ReventlessDev/reventless-core/commit/d78d4282c923c64cbde3432cd906845705a1e97b))
22
+ * **local:** SQLite pragmas, lazy in-memory scan, indexed lookup push-down (plan B4) ([b0ff78a](https://github.com/ReventlessDev/reventless-core/commit/b0ff78a5c02030eef9fc5bb1ee8b92c65114fa45))
23
+
24
+
25
+ # 3.0.0-alpha.115 (2026-07-02)
26
+
27
+ ### Bug Fixes
28
+
29
+ * **core,local:** projection/publisher/bus correctness (plan A5,A7) ([86e27a5](https://github.com/ReventlessDev/reventless-core/commit/86e27a5d68873d7dd9e746f9b4126f4d5bad63c9))
30
+ * **local:** conflict classification, rollback masking, MCP pagination (plan A6) ([b3e6d24](https://github.com/ReventlessDev/reventless-core/commit/b3e6d242ddf2df066ca94b0db2266275f1453fbf))
31
+ * **local:** InMemory QueryDb TTL parity + backend-aware UIFragment store (plan A6) ([c31727f](https://github.com/ReventlessDev/reventless-core/commit/c31727fa3fe98b4f15b55300fdf8b80221fabbee))
32
+ * **local:** QueryDb count returns a running total, not the increment (plan A6) ([4ac3822](https://github.com/ReventlessDev/reventless-core/commit/4ac3822ac310d62bb4b1c4c021a85f45883f4785))
33
+ * **local:** wire standalone DCB log through the backend-aware storage (plan A6) ([40734e9](https://github.com/ReventlessDev/reventless-core/commit/40734e902436dfce623061416e9edeafa822f1d3))
34
+
35
+
6
36
  # 3.0.0-alpha.114 (2026-06-29)
7
37
 
8
38
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.114",
3
+ "version": "3.0.0-alpha.116",
4
4
  "description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -31,20 +31,20 @@
31
31
  "graphql-yoga": "^5.21.0",
32
32
  "sury": "11.0.0-alpha.4",
33
33
  "ws": "^8.18.0",
34
- "@reventlessdev/rescript-effect": "0.1.0-alpha.23",
34
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.24",
35
35
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.17",
36
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.5",
36
37
  "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.16",
37
38
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
38
- "@reventlessdev/rescript-jest": "1.0.0-alpha.3",
39
- "@reventlessdev/reventless-core": "3.0.0-alpha.135",
40
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.75",
41
- "@reventlessdev/reventless-infra": "3.0.0-alpha.82",
42
- "@reventlessdev/reventless-spec": "3.0.0-alpha.62"
39
+ "@reventlessdev/reventless-core": "3.0.0-alpha.137",
40
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.77",
41
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.84",
42
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.64"
43
43
  },
44
44
  "devDependencies": {
45
45
  "rescript": "^12.3.0",
46
46
  "sury-ppx": "11.0.0-alpha.2",
47
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.48"
47
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.50"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "rescript": "^12.3.0"
package/src/Platform.res CHANGED
@@ -28,6 +28,22 @@ let getPlatformGraphQL = () => platformGraphQLRef.contents
28
28
  // Populated by makePlatform when splitApi=true.
29
29
  let platformMCPRef: ref<option<MCP_ServerInstance.t>> = ref(None)
30
30
 
31
+ // Decode a Plugin aggregate event from the published bus envelope.
32
+ // The bus delivers the EventTopic envelope {id, meta, event} — the event
33
+ // payload lives under "event". (The previous code fed the WHOLE envelope to
34
+ // the event schema in convert mode, which "succeeded" and then threw a
35
+ // TypeError on every variant's payload access, so the Source-C status /
36
+ // UI-fragment subscription emissions never fired.) Top-level, not inside the
37
+ // functor, so the decode is unit-testable.
38
+ let decodePluginEventEnvelope = (eventJson: JSON.t): option<ReventlessCore.PluginSpec.event> =>
39
+ switch eventJson
40
+ ->JSON.Decode.object
41
+ ->Option.flatMap(d => d->Dict.get("event"))
42
+ ->Option.map(payload => payload->S.parseJsonOrThrow(ReventlessCore.PluginSpec.eventSchema)) {
43
+ | result => result
44
+ | exception _ => None
45
+ }
46
+
31
47
  // Configurable platform — set silent=true to suppress diagnostic warnings in tests,
32
48
  // splitApi=true to serve core and plugin APIs on separate ports,
33
49
  // backend=Backend.Sqlite({...}) to opt into file-backed SQLite persistence.
@@ -76,6 +92,29 @@ module MakeWithConfig = (
76
92
  LocalBus.seedEventTapSeq(
77
93
  EventLogStorage_Sqlite.countAll(db) + DcbEventLogStorage_Sqlite.countAll(db),
78
94
  )
95
+ // Projection checkpoints (plan B5): the event-log storage tracks each
96
+ // appended batch as pending; this hook resolves it once the publish cycle
97
+ // completes (publishEvent returns only after every subscriber processed the
98
+ // events), advancing the persisted per-read-model checkpoints to the
99
+ // low-watermark. Startup catch-up in makePlatform replays anything below a
100
+ // read model's checkpoint that a crash left unprojected.
101
+ ProjectionPending.enableTracking()
102
+ ReventlessCore.EventPublish_Callback.registerAfterPublish(async publishedEvent => {
103
+ // Aggregate EventLog batches carry flat stored events (msgId at the top
104
+ // level of each eventsJson entry); DcbEventLog batches carry bare event
105
+ // payloads and expose the per-event metas separately.
106
+ let msgIds = switch publishedEvent.metas {
107
+ | Some(metas) => metas->Array.map(m => m.msgId)
108
+ | None =>
109
+ publishedEvent.eventsJson->Array.filterMap(json =>
110
+ json
111
+ ->JSON.Decode.object
112
+ ->Option.flatMap(d => d->Dict.get("msgId"))
113
+ ->Option.flatMap(JSON.Decode.string)
114
+ )
115
+ }
116
+ ProjectionCheckpoint.completePublished(db, msgIds)
117
+ })
79
118
  }
80
119
 
81
120
  module Bus = LocalBus.Impl({
@@ -279,10 +318,18 @@ module MakeWithConfig = (
279
318
  ])->JSON.Encode.object
280
319
 
281
320
  let paginate = (events: array<JSON.t>, getPosition: JSON.t => option<string>) => {
321
+ // Positions are numeric strings; compare them as ints, not lexically
322
+ // ("10" > "9" is false as strings) — otherwise cursor pages skip or
323
+ // duplicate events once positions cross a digit boundary.
324
+ let positionGt = (p, afterPos) =>
325
+ switch (Int.fromString(p), Int.fromString(afterPos)) {
326
+ | (Some(pi), Some(ai)) => pi > ai
327
+ | _ => p > afterPos
328
+ }
282
329
  let filtered = switch after {
283
330
  | Some(afterPos) =>
284
331
  let idx =
285
- events->Array.findIndex(e => getPosition(e)->Option.mapOr(false, p => p > afterPos))
332
+ events->Array.findIndex(e => getPosition(e)->Option.mapOr(false, p => positionGt(p, afterPos)))
286
333
  if idx >= 0 {
287
334
  events->Array.slice(~start=idx, ~end=events->Array.length)
288
335
  } else {
@@ -749,60 +796,27 @@ module MakeWithConfig = (
749
796
  // UIFragmentRegistry QueryDb store ops — initialized lazily on first seed call.
750
797
  let uiFragmentQueryDbOpsRef: ref<option<ReventlessCore.QueryDb_Adapter.operations>> = ref(None)
751
798
 
799
+ // Backend-aware storage for the UIFragment registry — the same functor every
800
+ // other read model uses. Replaces a hand-rolled memory-only store that ignored
801
+ // BackendState, so the registry now persists under SQLite too.
802
+ module UIFragmentStorage = QueryDbStorage_InMemory.Make(Bus)
803
+
752
804
  let ensureUIFragmentRegistryQueryDbStore = () => {
753
805
  switch uiFragmentQueryDbOpsRef.contents {
754
806
  | Some(ops) => ops
755
807
  | None =>
756
808
  let name = ReventlessCore.UIFragmentRegistryReadModelSpec.name
757
- let store: ref<dict<array<JSON.t>>> = ref(Dict.make())
758
- let allItems: ref<array<JSON.t>> = ref([])
759
- let syncAll = () => {
760
- allItems.contents =
761
- store.contents
762
- ->Dict.toArray
763
- ->Array.flatMap(((id, items)) =>
764
- items->Array.map(item => {
765
- let obj = item->JSON.Decode.object->Option.getOr(Dict.make())
766
- if !(obj->Dict.get("id")->Option.isSome) {
767
- let copy = Dict.make()
768
- obj->Dict.toArray->Array.forEach(((k, v)) => copy->Dict.set(k, v))
769
- copy->Dict.set("id", JSON.Encode.string(id))
770
- JSON.Encode.object(copy)
771
- } else {
772
- item
773
- }
774
- })
775
- )
776
- }
777
- let ops: ReventlessCore.QueryDb_Adapter.operations = {
778
- load: async id => Ok(store.contents->Dict.get(id)->Option.getOr([])),
779
- loadStream: id =>
780
- store.contents->Dict.get(id)->Option.getOr([])->Stream.fromIterable,
781
- save: async (id, state, _, _) => {
782
- store.contents->Dict.set(id, [state])
783
- syncAll()
784
- Ok()
785
- },
786
- saveBatch: async batch => {
787
- batch->Array.forEach(((id, state, _)) => store.contents->Dict.set(id, [state]))
788
- syncAll()
789
- Ok()
790
- },
791
- count: async (_, _, inc) => Ok(inc),
792
- delete: async (id, _) => {
793
- store.contents->Dict.delete(id)
794
- syncAll()
795
- Ok()
796
- },
797
- deleteBatch: async ids => {
798
- ids->Array.forEach(((id, _)) => store.contents->Dict.delete(id))
799
- syncAll()
800
- Ok()
801
- },
802
- }
803
- Bus.registerQueryDb(name, ops)
804
- Bus.registerQueryDbScan(name, () => allItems.contents)
805
- Bus.registerQueryDbStream(name, () => allItems.contents->Stream.fromIterable)
809
+ // `make` registers the ops (and scan/stream) with the Bus for the active
810
+ // backend; retrieve them synchronously via getQueryDb rather than resolving
811
+ // the Pulumi Output.
812
+ let _ = UIFragmentStorage.make(
813
+ ~name,
814
+ ~indexes=[],
815
+ ~api=(),
816
+ ~apiRole=(),
817
+ ~opts=({}: Pulumi.CustomResourceOptions.t),
818
+ )
819
+ let ops = Bus.getQueryDb(name)->Option.getOrThrow
806
820
  uiFragmentQueryDbOpsRef := Some(ops)
807
821
  ops
808
822
  }
@@ -885,36 +899,36 @@ module MakeWithConfig = (
885
899
  let manifestJson = (m: Reventless.Plugin.uiFragmentManifest) =>
886
900
  m->S.reverseConvertToJsonOrThrow(Reventless.Plugin.uiFragmentManifestSchema)
887
901
  Bus.subscribeToEvents(pluginEventTopicKey, async (_service, _meta, eventJson) => {
888
- switch eventJson->S.convertOrThrow(ReventlessCore.PluginSpec.eventSchema) {
889
- | VersionConnected(def)
890
- | VersionActivated(def)
891
- | VersionPromoted(def) =>
902
+ switch decodePluginEventEnvelope(eventJson) {
903
+ | Some(VersionConnected(def))
904
+ | Some(VersionActivated(def))
905
+ | Some(VersionPromoted(def)) =>
892
906
  publishStatus(~name=def.name, ~status="Connected")
893
- | VersionDisconnected(def) => publishStatus(~name=def.name, ~status="Disconnected")
894
- | VersionDeactivated(def) => publishStatus(~name=def.name, ~status="Inactive")
895
- | VersionRetired(def) => publishStatus(~name=def.name, ~status="Retired")
896
- | UIFragmentRegistered(data) =>
907
+ | Some(VersionDisconnected(def)) => publishStatus(~name=def.name, ~status="Disconnected")
908
+ | Some(VersionDeactivated(def)) => publishStatus(~name=def.name, ~status="Inactive")
909
+ | Some(VersionRetired(def)) => publishStatus(~name=def.name, ~status="Retired")
910
+ | Some(UIFragmentRegistered(data)) =>
897
911
  publishUIFragment(
898
912
  ~name=ReventlessCore.Plugin.name(data.pluginId),
899
913
  ~changeKind="Registered",
900
914
  ~manifest=manifestJson(data.manifest),
901
915
  )
902
- | UIFragmentUpdated(data) =>
916
+ | Some(UIFragmentUpdated(data)) =>
903
917
  publishUIFragment(
904
918
  ~name=ReventlessCore.Plugin.name(data.pluginId),
905
919
  ~changeKind="Updated",
906
920
  ~manifest=manifestJson(data.newManifest),
907
921
  )
908
- | UIFragmentDeregistered(data) =>
922
+ | Some(UIFragmentDeregistered(data)) =>
909
923
  publishUIFragment(
910
924
  ~name=ReventlessCore.Plugin.name(data.pluginId),
911
925
  ~changeKind="Deregistered",
912
926
  ~manifest=JSON.Encode.null,
913
927
  )
914
- | VersionDetected(_)
915
- | VersionSuperseded(_)
916
- | IncompatiblePluginDetected(_) => ()
917
- | exception _ => ()
928
+ | Some(VersionDetected(_))
929
+ | Some(VersionSuperseded(_))
930
+ | Some(IncompatiblePluginDetected(_))
931
+ | None => ()
918
932
  }
919
933
  })
920
934
  }
@@ -1264,6 +1278,16 @@ module MakeWithConfig = (
1264
1278
  `backend: sqlite (${path}${resetOnStart ? ", resetOnStart" : ""})`
1265
1279
  },
1266
1280
  )
1281
+ // Projection catch-up bound (plan B5): the highest persisted event position
1282
+ // BEFORE this session appends anything. Catch-up replays only (checkpoint,
1283
+ // bound] — this session's own events (Connect dispatches, user commands)
1284
+ // are live-delivered and must not be redelivered.
1285
+ let projectionCatchup = BackendState.getDb()->Option.map(db => (
1286
+ db,
1287
+ ProjectionCheckpoint.maxPosition(db, ProjectionPending.Aggregate),
1288
+ ProjectionCheckpoint.maxPosition(db, ProjectionPending.Dcb),
1289
+ ))
1290
+
1267
1291
  // Create scheduler and populate platform context refs.
1268
1292
  let scheduler = makeScheduler()
1269
1293
  hooks.scheduler := Some(scheduler)
@@ -1353,9 +1377,34 @@ module MakeWithConfig = (
1353
1377
  // Drive the admin Plugin read models via synthetic Connect dispatch through
1354
1378
  // LocalPluginAggregate (replaces the old direct-write seed). The real
1355
1379
  // PluginsProjection folds the emitted events into the "Plugins" QueryDb store.
1356
- connectPlugin(~pluginComponents=plugins)
1357
- seedUIFragmentRegistryQueryDb(~pluginComponents=plugins)
1358
- seedPluginStructuresStore(~pluginComponents=plugins)
1380
+ //
1381
+ // Under SQLite this trio is sequenced AFTER the projection catch-up: the
1382
+ // catch-up replays PRIOR sessions' stored events (e.g. the admin plugin
1383
+ // lifecycle history) into the projections, and this session's Connect
1384
+ // events must fold on top of — never race and be overwritten by — that
1385
+ // replay. All three calls are internally fire-and-forget Output.apply
1386
+ // chains, so deferring their invocation changes no synchronous contract.
1387
+ let seedAdminStores = () => {
1388
+ connectPlugin(~pluginComponents=plugins)
1389
+ seedUIFragmentRegistryQueryDb(~pluginComponents=plugins)
1390
+ seedPluginStructuresStore(~pluginComponents=plugins)
1391
+ }
1392
+ switch projectionCatchup {
1393
+ | Some((db, upperBound, dcbUpperBound)) =>
1394
+ let _ =
1395
+ ProjectionCheckpoint.startupCatchup(~db, ~upperBound, ~dcbUpperBound, ~handlers=() =>
1396
+ Bus.projectionCatchupHandlers()
1397
+ )
1398
+ ->Promise.catch(e => {
1399
+ Console.error2("[Platform] projection catch-up failed:", e)
1400
+ Promise.resolve()
1401
+ })
1402
+ ->Promise.then(() => {
1403
+ seedAdminStores()
1404
+ Promise.resolve()
1405
+ })
1406
+ | None => seedAdminStores()
1407
+ }
1359
1408
 
1360
1409
  // Seed the built-in admin plugin's structure so its Auto UI (Plugin list with
1361
1410
  // Activate/Deactivate buttons) renders alongside the user plugins. Admin.construct