@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.
- package/CHANGELOG.md +30 -0
- package/package.json +8 -8
- package/src/Platform.res +116 -67
- package/src/Platform.res.mjs +114 -190
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +102 -17
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +105 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +123 -40
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +96 -24
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +33 -0
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +44 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +24 -3
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +21 -4
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +148 -27
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +144 -34
- package/src/adapter/LocalBus.res +85 -3
- package/src/adapter/LocalBus.res.mjs +220 -66
- package/src/adapter/ProjectionCheckpoint.res +350 -0
- package/src/adapter/ProjectionCheckpoint.res.mjs +327 -0
- package/src/adapter/ProjectionPending.res +60 -0
- package/src/adapter/ProjectionPending.res.mjs +64 -0
- package/src/adapter/QueryDb/QueryDbListQuery.res +261 -0
- package/src/adapter/QueryDb/QueryDbListQuery.res.mjs +212 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +54 -286
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +28 -190
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +128 -10
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +123 -23
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +187 -12
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +120 -7
- package/src/adapter/SqliteDriver.res +17 -2
- package/src/adapter/SqliteDriver.res.mjs +10 -2
- package/src/components/DcbEventLog_Builder.res +7 -1
- package/src/components/DcbEventLog_Builder.res.mjs +3 -3
- package/src/components/ReadModel_Builder.res +3 -1
- package/src/components/ReadModel_Builder.res.mjs +13 -4
- package/src/components/StateViewSlice_Builder.res +4 -1
- package/src/components/StateViewSlice_Builder.res.mjs +9 -3
- package/src/test/Mocks/MockEventLogStorage.res +20 -3
- package/src/test/Mocks/MockEventLogStorage.res.mjs +25 -3
- package/tests/PluginEventDecodeTest.res +51 -0
- package/tests/PluginEventDecodeTest.res.mjs +63 -0
- package/tests/adapter/BackendParityTest.res +64 -0
- package/tests/adapter/BackendParityTest.res.mjs +45 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +73 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +86 -0
- package/tests/adapter/DcbEventLogStorageTest.res +46 -0
- package/tests/adapter/DcbEventLogStorageTest.res.mjs +63 -0
- package/tests/adapter/EventLogSnapshotParityTest.res +81 -0
- package/tests/adapter/EventLogSnapshotParityTest.res.mjs +141 -0
- package/tests/adapter/EventLogStorageSqliteTest.res +132 -2
- package/tests/adapter/EventLogStorageSqliteTest.res.mjs +195 -1
- package/tests/adapter/LocalBusPubSubTest.res +31 -0
- package/tests/adapter/LocalBusPubSubTest.res.mjs +28 -0
- package/tests/adapter/ProjectionCheckpointTest.res +422 -0
- package/tests/adapter/ProjectionCheckpointTest.res.mjs +401 -0
- package/tests/adapter/QueryDbGsiTtlTest.res +78 -0
- package/tests/adapter/QueryDbGsiTtlTest.res.mjs +77 -0
- package/tests/adapter/QueryDbListPushdownParityTest.res +197 -0
- package/tests/adapter/QueryDbListPushdownParityTest.res.mjs +292 -0
- package/tests/adapter/QueryDbListResolverTest.res +76 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +84 -0
- package/tests/components/aggregate/AggregateFixtures.res +4 -0
- package/tests/components/aggregate/AggregateFixtures.res.mjs +1 -0
- package/tests/components/eventlog/EventLogAppendStreamTest.res.mjs +4 -4
- package/tests/components/eventlog/EventLogStreamTest.res.mjs +8 -8
- 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.
|
|
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.
|
|
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/
|
|
39
|
-
"@reventlessdev/reventless-
|
|
40
|
-
"@reventlessdev/reventless-
|
|
41
|
-
"@reventlessdev/reventless-
|
|
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.
|
|
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
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
|
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
|
-
|
|
|
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
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|