@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
@@ -4,15 +4,14 @@ import * as S from "sury/src/S.res.mjs";
4
4
  import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
5
5
  import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
6
6
  import * as Stdlib_Bool from "@rescript/runtime/lib/es6/Stdlib_Bool.js";
7
- import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
8
7
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
9
8
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
10
9
  import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
11
10
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
12
11
  import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
13
12
  import * as Effect from "effect/Effect";
14
- import * as Stream$1 from "effect/Stream";
15
13
  import * as Pulumi from "@pulumi/pulumi";
14
+ import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
16
15
  import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
17
16
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
18
17
  import * as Plugin$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin.res.mjs";
@@ -47,12 +46,15 @@ import * as Scheduler_Builder$ReventlessCore from "@reventlessdev/reventless-cor
47
46
  import * as Aggregate_Builder$ReventlessLocal from "./components/Aggregate_Builder.res.mjs";
48
47
  import * as LocalClonerRunner$ReventlessLocal from "./adapter/Cloner/LocalClonerRunner.res.mjs";
49
48
  import * as PluginBaseFragment$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/api/PluginBaseFragment.res.mjs";
49
+ import * as ProjectionPending$ReventlessLocal from "./adapter/ProjectionPending.res.mjs";
50
50
  import * as ReadModel_Builder$ReventlessLocal from "./components/ReadModel_Builder.res.mjs";
51
51
  import * as PlatformMCP_Server$ReventlessLocal from "./adapter/PlatformMCP_Server.res.mjs";
52
52
  import * as Auth_GraphqlContext$ReventlessLocal from "./adapter/Auth/Auth_GraphqlContext.res.mjs";
53
53
  import * as PluginsReadModelSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs";
54
54
  import * as DomainGraphQL_Server$ReventlessLocal from "./adapter/DomainGraphQL_Server.res.mjs";
55
+ import * as EventPublish_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventLog/EventPublish_Callback.res.mjs";
55
56
  import * as LocalGraphQL_Adapter$ReventlessLocal from "./adapter/Api/LocalGraphQL_Adapter.res.mjs";
57
+ import * as ProjectionCheckpoint$ReventlessLocal from "./adapter/ProjectionCheckpoint.res.mjs";
56
58
  import * as ExtensionPointMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
57
59
  import * as EventLogStorage_Sqlite$ReventlessLocal from "./adapter/EventLog/EventLogStorage_Sqlite.res.mjs";
58
60
  import * as ExtensionPoint_Builder$ReventlessLocal from "./components/ExtensionPoint_Builder.res.mjs";
@@ -63,6 +65,7 @@ import * as AutomationSlice_Builder$ReventlessLocal from "./components/Automatio
63
65
  import * as LocalRuntimeEnvironment$ReventlessLocal from "./adapter/Runtime/LocalRuntimeEnvironment.res.mjs";
64
66
  import * as LocalScheduledPublisher$ReventlessLocal from "./adapter/Scheduler/LocalScheduledPublisher.res.mjs";
65
67
  import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
68
+ import * as QueryDbStorage_InMemory$ReventlessLocal from "./adapter/QueryDb/QueryDbStorage_InMemory.res.mjs";
66
69
  import * as LocalCommandTopicChannel$ReventlessLocal from "./adapter/CommandTopic/LocalCommandTopicChannel.res.mjs";
67
70
  import * as LocalEventTopicPublisher$ReventlessLocal from "./adapter/EventTopic/LocalEventTopicPublisher.res.mjs";
68
71
  import * as StateChangeSlice_Builder$ReventlessLocal from "./components/StateChangeSlice_Builder.res.mjs";
@@ -94,6 +97,14 @@ let platformMCPRef = {
94
97
  contents: undefined
95
98
  };
96
99
 
100
+ function decodePluginEventEnvelope(eventJson) {
101
+ try {
102
+ return Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(eventJson), d => d["event"]), payload => S.parseJsonOrThrow(payload, PluginSpec$ReventlessCore.eventSchema));
103
+ } catch (exn) {
104
+ return;
105
+ }
106
+ }
107
+
97
108
  function MakeWithConfig(Config) {
98
109
  TestRunner$ReventlessLocal.setup();
99
110
  let match = Config.backend;
@@ -107,6 +118,12 @@ function MakeWithConfig(Config) {
107
118
  let db = SqliteDriver$ReventlessLocal.openDb(path);
108
119
  BackendState$ReventlessLocal.setSqlite(db, path);
109
120
  LocalBus$ReventlessLocal.seedEventTapSeq(EventLogStorage_Sqlite$ReventlessLocal.countAll(db) + DcbEventLogStorage_Sqlite$ReventlessLocal.countAll(db) | 0);
121
+ ProjectionPending$ReventlessLocal.enableTracking();
122
+ EventPublish_Callback$ReventlessCore.registerAfterPublish(async publishedEvent => {
123
+ let metas = publishedEvent.metas;
124
+ let msgIds = metas !== undefined ? metas.map(m => m.msgId) : Stdlib_Array.filterMap(publishedEvent.eventsJson, json => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["msgId"]), Stdlib_JSON.Decode.string));
125
+ return ProjectionCheckpoint$ReventlessLocal.completePublished(db, msgIds);
126
+ });
110
127
  }
111
128
  let Bus = LocalBus$ReventlessLocal.Impl({
112
129
  capacity: undefined,
@@ -268,7 +285,15 @@ function MakeWithConfig(Config) {
268
285
  let paginate = (events, getPosition) => {
269
286
  let filtered;
270
287
  if (after !== undefined) {
271
- let idx = events.findIndex(e => Stdlib_Option.mapOr(getPosition(e), false, p => p > after));
288
+ let idx = events.findIndex(e => Stdlib_Option.mapOr(getPosition(e), false, p => {
289
+ let match = Stdlib_Int.fromString(p, undefined);
290
+ let match$1 = Stdlib_Int.fromString(after, undefined);
291
+ if (match !== undefined && match$1 !== undefined) {
292
+ return match > match$1;
293
+ } else {
294
+ return p > after;
295
+ }
296
+ }));
272
297
  filtered = idx >= 0 ? events.slice(idx, events.length) : [];
273
298
  } else {
274
299
  filtered = events;
@@ -630,6 +655,7 @@ function MakeWithConfig(Config) {
630
655
  initialState: PluginBehavior$ReventlessCore.initialState,
631
656
  evolve: PluginBehavior$ReventlessCore.evolve,
632
657
  decide: PluginBehavior$ReventlessCore.decide,
658
+ snapshot: undefined,
633
659
  moduleUrl: PluginBehavior$ReventlessCore.moduleUrl
634
660
  })(NoEventMappings$ReventlessInfra.Make({
635
661
  name: PluginSpec$ReventlessCore.name,
@@ -704,89 +730,14 @@ function MakeWithConfig(Config) {
704
730
  let uiFragmentQueryDbOpsRef = {
705
731
  contents: undefined
706
732
  };
733
+ let UIFragmentStorage = QueryDbStorage_InMemory$ReventlessLocal.Make(Bus);
707
734
  let ensureUIFragmentRegistryQueryDbStore = () => {
708
735
  let ops = uiFragmentQueryDbOpsRef.contents;
709
736
  if (ops !== undefined) {
710
737
  return ops;
711
738
  }
712
- let store = {
713
- contents: {}
714
- };
715
- let allItems = {
716
- contents: []
717
- };
718
- let syncAll = () => {
719
- allItems.contents = Object.entries(store.contents).flatMap(param => {
720
- let id = param[0];
721
- return param[1].map(item => {
722
- let obj = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(item), {});
723
- if (Stdlib_Option.isSome(obj["id"])) {
724
- return item;
725
- }
726
- let copy = {};
727
- Object.entries(obj).forEach(param => {
728
- copy[param[0]] = param[1];
729
- });
730
- copy["id"] = id;
731
- return copy;
732
- });
733
- });
734
- };
735
- let ops_load = async id => ({
736
- TAG: "Ok",
737
- _0: Stdlib_Option.getOr(store.contents[id], [])
738
- });
739
- let ops_loadStream = id => Stream$1.fromIterable(Stdlib_Option.getOr(store.contents[id], []));
740
- let ops_save = async (id, state, param, param$1) => {
741
- store.contents[id] = [state];
742
- syncAll();
743
- return {
744
- TAG: "Ok",
745
- _0: undefined
746
- };
747
- };
748
- let ops_saveBatch = async batch => {
749
- batch.forEach(param => {
750
- store.contents[param[0]] = [param[1]];
751
- });
752
- syncAll();
753
- return {
754
- TAG: "Ok",
755
- _0: undefined
756
- };
757
- };
758
- let ops_count = async (param, param$1, inc) => ({
759
- TAG: "Ok",
760
- _0: inc
761
- });
762
- let ops_delete = async (id, param) => {
763
- Stdlib_Dict.$$delete(store.contents, id);
764
- syncAll();
765
- return {
766
- TAG: "Ok",
767
- _0: undefined
768
- };
769
- };
770
- let ops_deleteBatch = async ids => {
771
- ids.forEach(param => Stdlib_Dict.$$delete(store.contents, param[0]));
772
- syncAll();
773
- return {
774
- TAG: "Ok",
775
- _0: undefined
776
- };
777
- };
778
- let ops$1 = {
779
- load: ops_load,
780
- loadStream: ops_loadStream,
781
- save: ops_save,
782
- saveBatch: ops_saveBatch,
783
- count: ops_count,
784
- delete: ops_delete,
785
- deleteBatch: ops_deleteBatch
786
- };
787
- Bus.registerQueryDb(UIFragmentRegistryReadModelSpec$ReventlessCore.name, ops$1);
788
- Bus.registerQueryDbScan(UIFragmentRegistryReadModelSpec$ReventlessCore.name, () => allItems.contents);
789
- Bus.registerQueryDbStream(UIFragmentRegistryReadModelSpec$ReventlessCore.name, () => Stream$1.fromIterable(allItems.contents));
739
+ UIFragmentStorage.make(UIFragmentRegistryReadModelSpec$ReventlessCore.name, [], undefined, undefined, undefined, undefined, {});
740
+ let ops$1 = Stdlib_Option.getOrThrow(Bus.getQueryDb(UIFragmentRegistryReadModelSpec$ReventlessCore.name), undefined);
790
741
  uiFragmentQueryDbOpsRef.contents = ops$1;
791
742
  return ops$1;
792
743
  };
@@ -855,33 +806,31 @@ function MakeWithConfig(Config) {
855
806
  ]
856
807
  ]));
857
808
  Bus.subscribeToEvents(pluginEventTopicKey, async (_service, _meta, eventJson) => {
858
- let def;
859
- try {
860
- def = S.convertOrThrow(eventJson, PluginSpec$ReventlessCore.eventSchema);
861
- } catch (exn) {
809
+ let match = decodePluginEventEnvelope(eventJson);
810
+ if (match === undefined) {
862
811
  return;
863
812
  }
864
- switch (def.TAG) {
813
+ switch (match.TAG) {
865
814
  case "VersionDisconnected" :
866
- return publishStatus(def._0.name, "Disconnected");
815
+ return publishStatus(match._0.name, "Disconnected");
867
816
  case "VersionDeactivated" :
868
- return publishStatus(def._0.name, "Inactive");
817
+ return publishStatus(match._0.name, "Inactive");
869
818
  case "VersionRetired" :
870
- return publishStatus(def._0.name, "Retired");
819
+ return publishStatus(match._0.name, "Retired");
871
820
  case "VersionDetected" :
872
821
  case "VersionSuperseded" :
873
822
  case "IncompatiblePluginDetected" :
874
823
  return;
875
824
  case "UIFragmentRegistered" :
876
- let data = def._0;
825
+ let data = match._0;
877
826
  return publishUIFragment(Plugin$ReventlessCore.name(data.pluginId), "Registered", S.reverseConvertToJsonOrThrow(data.manifest, Plugin$Reventless.uiFragmentManifestSchema));
878
827
  case "UIFragmentUpdated" :
879
- let data$1 = def._0;
828
+ let data$1 = match._0;
880
829
  return publishUIFragment(Plugin$ReventlessCore.name(data$1.pluginId), "Updated", S.reverseConvertToJsonOrThrow(data$1.newManifest, Plugin$Reventless.uiFragmentManifestSchema));
881
830
  case "UIFragmentDeregistered" :
882
- return publishUIFragment(Plugin$ReventlessCore.name(def._0.pluginId), "Deregistered", null);
831
+ return publishUIFragment(Plugin$ReventlessCore.name(match._0.pluginId), "Deregistered", null);
883
832
  default:
884
- return publishStatus(def._0.name, "Connected");
833
+ return publishStatus(match._0.name, "Connected");
885
834
  }
886
835
  });
887
836
  };
@@ -1149,6 +1098,11 @@ function MakeWithConfig(Config) {
1149
1098
  match.resetOnStart ? ", resetOnStart" : ""
1150
1099
  ) + `)`;
1151
1100
  log.info("Platform", undefined, tmp);
1101
+ let projectionCatchup = Stdlib_Option.map(BackendState$ReventlessLocal.getDb(), db => [
1102
+ db,
1103
+ ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Aggregate"),
1104
+ ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Dcb")
1105
+ ]);
1152
1106
  let scheduler = makeScheduler();
1153
1107
  hooks_scheduler.contents = scheduler;
1154
1108
  hooks_api.contents = {
@@ -1207,9 +1161,22 @@ function MakeWithConfig(Config) {
1207
1161
  adminResources: []
1208
1162
  });
1209
1163
  subscribeToPluginEvents();
1210
- connectPlugin(plugins$1);
1211
- seedUIFragmentRegistryQueryDb(plugins$1);
1212
- seedPluginStructuresStore(plugins$1);
1164
+ let seedAdminStores = () => {
1165
+ connectPlugin(plugins$1);
1166
+ seedUIFragmentRegistryQueryDb(plugins$1);
1167
+ seedPluginStructuresStore(plugins$1);
1168
+ };
1169
+ if (projectionCatchup !== undefined) {
1170
+ Stdlib_Promise.$$catch(ProjectionCheckpoint$ReventlessLocal.startupCatchup(projectionCatchup[0], projectionCatchup[1], projectionCatchup[2], () => Bus.projectionCatchupHandlers()), e => {
1171
+ console.error("[Platform] projection catch-up failed:", e);
1172
+ return Promise.resolve();
1173
+ }).then(() => {
1174
+ seedAdminStores();
1175
+ return Promise.resolve();
1176
+ });
1177
+ } else {
1178
+ seedAdminStores();
1179
+ }
1213
1180
  pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure;
1214
1181
  CommandGeneratorResolvers_GraphQL$ReventlessLocal.setPluginStatusGate(field => {
1215
1182
  let parts = field.split("_");
@@ -1766,6 +1733,12 @@ function Make($star) {
1766
1733
  let db = SqliteDriver$ReventlessLocal.openDb(path);
1767
1734
  BackendState$ReventlessLocal.setSqlite(db, path);
1768
1735
  LocalBus$ReventlessLocal.seedEventTapSeq(EventLogStorage_Sqlite$ReventlessLocal.countAll(db) + DcbEventLogStorage_Sqlite$ReventlessLocal.countAll(db) | 0);
1736
+ ProjectionPending$ReventlessLocal.enableTracking();
1737
+ EventPublish_Callback$ReventlessCore.registerAfterPublish(async publishedEvent => {
1738
+ let metas = publishedEvent.metas;
1739
+ let msgIds = metas !== undefined ? metas.map(m => m.msgId) : Stdlib_Array.filterMap(publishedEvent.eventsJson, json => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["msgId"]), Stdlib_JSON.Decode.string));
1740
+ return ProjectionCheckpoint$ReventlessLocal.completePublished(db, msgIds);
1741
+ });
1769
1742
  }
1770
1743
  let Bus = LocalBus$ReventlessLocal.Impl({
1771
1744
  capacity: undefined,
@@ -1921,7 +1894,15 @@ function Make($star) {
1921
1894
  let paginate = (events, getPosition) => {
1922
1895
  let filtered;
1923
1896
  if (after !== undefined) {
1924
- let idx = events.findIndex(e => Stdlib_Option.mapOr(getPosition(e), false, p => p > after));
1897
+ let idx = events.findIndex(e => Stdlib_Option.mapOr(getPosition(e), false, p => {
1898
+ let match = Stdlib_Int.fromString(p, undefined);
1899
+ let match$1 = Stdlib_Int.fromString(after, undefined);
1900
+ if (match !== undefined && match$1 !== undefined) {
1901
+ return match > match$1;
1902
+ } else {
1903
+ return p > after;
1904
+ }
1905
+ }));
1925
1906
  filtered = idx >= 0 ? events.slice(idx, events.length) : [];
1926
1907
  } else {
1927
1908
  filtered = events;
@@ -2282,6 +2263,7 @@ function Make($star) {
2282
2263
  initialState: PluginBehavior$ReventlessCore.initialState,
2283
2264
  evolve: PluginBehavior$ReventlessCore.evolve,
2284
2265
  decide: PluginBehavior$ReventlessCore.decide,
2266
+ snapshot: undefined,
2285
2267
  moduleUrl: PluginBehavior$ReventlessCore.moduleUrl
2286
2268
  })(NoEventMappings$ReventlessInfra.Make({
2287
2269
  name: PluginSpec$ReventlessCore.name,
@@ -2356,89 +2338,14 @@ function Make($star) {
2356
2338
  let uiFragmentQueryDbOpsRef = {
2357
2339
  contents: undefined
2358
2340
  };
2341
+ let UIFragmentStorage = QueryDbStorage_InMemory$ReventlessLocal.Make(Bus);
2359
2342
  let ensureUIFragmentRegistryQueryDbStore = () => {
2360
2343
  let ops = uiFragmentQueryDbOpsRef.contents;
2361
2344
  if (ops !== undefined) {
2362
2345
  return ops;
2363
2346
  }
2364
- let store = {
2365
- contents: {}
2366
- };
2367
- let allItems = {
2368
- contents: []
2369
- };
2370
- let syncAll = () => {
2371
- allItems.contents = Object.entries(store.contents).flatMap(param => {
2372
- let id = param[0];
2373
- return param[1].map(item => {
2374
- let obj = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(item), {});
2375
- if (Stdlib_Option.isSome(obj["id"])) {
2376
- return item;
2377
- }
2378
- let copy = {};
2379
- Object.entries(obj).forEach(param => {
2380
- copy[param[0]] = param[1];
2381
- });
2382
- copy["id"] = id;
2383
- return copy;
2384
- });
2385
- });
2386
- };
2387
- let ops_load = async id => ({
2388
- TAG: "Ok",
2389
- _0: Stdlib_Option.getOr(store.contents[id], [])
2390
- });
2391
- let ops_loadStream = id => Stream$1.fromIterable(Stdlib_Option.getOr(store.contents[id], []));
2392
- let ops_save = async (id, state, param, param$1) => {
2393
- store.contents[id] = [state];
2394
- syncAll();
2395
- return {
2396
- TAG: "Ok",
2397
- _0: undefined
2398
- };
2399
- };
2400
- let ops_saveBatch = async batch => {
2401
- batch.forEach(param => {
2402
- store.contents[param[0]] = [param[1]];
2403
- });
2404
- syncAll();
2405
- return {
2406
- TAG: "Ok",
2407
- _0: undefined
2408
- };
2409
- };
2410
- let ops_count = async (param, param$1, inc) => ({
2411
- TAG: "Ok",
2412
- _0: inc
2413
- });
2414
- let ops_delete = async (id, param) => {
2415
- Stdlib_Dict.$$delete(store.contents, id);
2416
- syncAll();
2417
- return {
2418
- TAG: "Ok",
2419
- _0: undefined
2420
- };
2421
- };
2422
- let ops_deleteBatch = async ids => {
2423
- ids.forEach(param => Stdlib_Dict.$$delete(store.contents, param[0]));
2424
- syncAll();
2425
- return {
2426
- TAG: "Ok",
2427
- _0: undefined
2428
- };
2429
- };
2430
- let ops$1 = {
2431
- load: ops_load,
2432
- loadStream: ops_loadStream,
2433
- save: ops_save,
2434
- saveBatch: ops_saveBatch,
2435
- count: ops_count,
2436
- delete: ops_delete,
2437
- deleteBatch: ops_deleteBatch
2438
- };
2439
- Bus.registerQueryDb(UIFragmentRegistryReadModelSpec$ReventlessCore.name, ops$1);
2440
- Bus.registerQueryDbScan(UIFragmentRegistryReadModelSpec$ReventlessCore.name, () => allItems.contents);
2441
- Bus.registerQueryDbStream(UIFragmentRegistryReadModelSpec$ReventlessCore.name, () => Stream$1.fromIterable(allItems.contents));
2347
+ UIFragmentStorage.make(UIFragmentRegistryReadModelSpec$ReventlessCore.name, [], undefined, undefined, undefined, undefined, {});
2348
+ let ops$1 = Stdlib_Option.getOrThrow(Bus.getQueryDb(UIFragmentRegistryReadModelSpec$ReventlessCore.name), undefined);
2442
2349
  uiFragmentQueryDbOpsRef.contents = ops$1;
2443
2350
  return ops$1;
2444
2351
  };
@@ -2507,33 +2414,31 @@ function Make($star) {
2507
2414
  ]
2508
2415
  ]));
2509
2416
  Bus.subscribeToEvents(pluginEventTopicKey, async (_service, _meta, eventJson) => {
2510
- let def;
2511
- try {
2512
- def = S.convertOrThrow(eventJson, PluginSpec$ReventlessCore.eventSchema);
2513
- } catch (exn) {
2417
+ let match = decodePluginEventEnvelope(eventJson);
2418
+ if (match === undefined) {
2514
2419
  return;
2515
2420
  }
2516
- switch (def.TAG) {
2421
+ switch (match.TAG) {
2517
2422
  case "VersionDisconnected" :
2518
- return publishStatus(def._0.name, "Disconnected");
2423
+ return publishStatus(match._0.name, "Disconnected");
2519
2424
  case "VersionDeactivated" :
2520
- return publishStatus(def._0.name, "Inactive");
2425
+ return publishStatus(match._0.name, "Inactive");
2521
2426
  case "VersionRetired" :
2522
- return publishStatus(def._0.name, "Retired");
2427
+ return publishStatus(match._0.name, "Retired");
2523
2428
  case "VersionDetected" :
2524
2429
  case "VersionSuperseded" :
2525
2430
  case "IncompatiblePluginDetected" :
2526
2431
  return;
2527
2432
  case "UIFragmentRegistered" :
2528
- let data = def._0;
2433
+ let data = match._0;
2529
2434
  return publishUIFragment(Plugin$ReventlessCore.name(data.pluginId), "Registered", S.reverseConvertToJsonOrThrow(data.manifest, Plugin$Reventless.uiFragmentManifestSchema));
2530
2435
  case "UIFragmentUpdated" :
2531
- let data$1 = def._0;
2436
+ let data$1 = match._0;
2532
2437
  return publishUIFragment(Plugin$ReventlessCore.name(data$1.pluginId), "Updated", S.reverseConvertToJsonOrThrow(data$1.newManifest, Plugin$Reventless.uiFragmentManifestSchema));
2533
2438
  case "UIFragmentDeregistered" :
2534
- return publishUIFragment(Plugin$ReventlessCore.name(def._0.pluginId), "Deregistered", null);
2439
+ return publishUIFragment(Plugin$ReventlessCore.name(match._0.pluginId), "Deregistered", null);
2535
2440
  default:
2536
- return publishStatus(def._0.name, "Connected");
2441
+ return publishStatus(match._0.name, "Connected");
2537
2442
  }
2538
2443
  });
2539
2444
  };
@@ -2796,6 +2701,11 @@ function Make($star) {
2796
2701
  backend.resetOnStart ? ", resetOnStart" : ""
2797
2702
  ) + `)`;
2798
2703
  log.info("Platform", undefined, tmp);
2704
+ let projectionCatchup = Stdlib_Option.map(BackendState$ReventlessLocal.getDb(), db => [
2705
+ db,
2706
+ ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Aggregate"),
2707
+ ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Dcb")
2708
+ ]);
2799
2709
  let scheduler = makeScheduler();
2800
2710
  hooks_scheduler.contents = scheduler;
2801
2711
  hooks_api.contents = {
@@ -2854,9 +2764,22 @@ function Make($star) {
2854
2764
  adminResources: []
2855
2765
  });
2856
2766
  subscribeToPluginEvents();
2857
- connectPlugin(plugins$1);
2858
- seedUIFragmentRegistryQueryDb(plugins$1);
2859
- seedPluginStructuresStore(plugins$1);
2767
+ let seedAdminStores = () => {
2768
+ connectPlugin(plugins$1);
2769
+ seedUIFragmentRegistryQueryDb(plugins$1);
2770
+ seedPluginStructuresStore(plugins$1);
2771
+ };
2772
+ if (projectionCatchup !== undefined) {
2773
+ Stdlib_Promise.$$catch(ProjectionCheckpoint$ReventlessLocal.startupCatchup(projectionCatchup[0], projectionCatchup[1], projectionCatchup[2], () => Bus.projectionCatchupHandlers()), e => {
2774
+ console.error("[Platform] projection catch-up failed:", e);
2775
+ return Promise.resolve();
2776
+ }).then(() => {
2777
+ seedAdminStores();
2778
+ return Promise.resolve();
2779
+ });
2780
+ } else {
2781
+ seedAdminStores();
2782
+ }
2860
2783
  pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure;
2861
2784
  CommandGeneratorResolvers_GraphQL$ReventlessLocal.setPluginStatusGate(field => {
2862
2785
  let parts = field.split("_");
@@ -3391,6 +3314,7 @@ export {
3391
3314
  platformGraphQLRef,
3392
3315
  getPlatformGraphQL,
3393
3316
  platformMCPRef,
3317
+ decodePluginEventEnvelope,
3394
3318
  MakeWithConfig,
3395
3319
  Make,
3396
3320
  }
@@ -25,18 +25,108 @@ let matchesQuery = (event: DcbEventLog_Adapter.rawSequencedEvent, query: Reventl
25
25
  })
26
26
  }
27
27
 
28
+ // Composite key for the per-(key,value) tag posting list. NUL can't appear in a
29
+ // tag key or value, so it's an unambiguous separator.
30
+ let tagPostingKey = (key: string, value: string) => key ++ "\x00" ++ value
31
+
32
+ // Intersect two ascending, duplicate-free index arrays (two-pointer merge).
33
+ let intersectSorted = (a: array<int>, b: array<int>): array<int> => {
34
+ let out = []
35
+ let i = ref(0)
36
+ let j = ref(0)
37
+ while i.contents < a->Array.length && j.contents < b->Array.length {
38
+ let av = a->Array.getUnsafe(i.contents)
39
+ let bv = b->Array.getUnsafe(j.contents)
40
+ if av == bv {
41
+ out->Array.push(av)
42
+ i := i.contents + 1
43
+ j := j.contents + 1
44
+ } else if av < bv {
45
+ i := i.contents + 1
46
+ } else {
47
+ j := j.contents + 1
48
+ }
49
+ }
50
+ out
51
+ }
52
+
28
53
  let makeStorage = (~name as _name, ~indexes as _, ~partitionTag as _, ~opts as _) => {
29
54
  let events: ref<array<DcbEventLog_Adapter.rawSequencedEvent>> = ref([])
30
55
  let position = ref(0)
56
+ // Posting lists: tag "key\0value" / eventType → ascending array indices into
57
+ // `events`. Events are only ever appended (never removed) in position order, so
58
+ // every list stays sorted and duplicate-free by construction. They turn the
59
+ // per-read / per-conditional-append full scan (O(n) → O(n²) over a session)
60
+ // into a lookup + intersection over the (usually tiny) candidate set.
61
+ let byTag: Dict.t<array<int>> = Dict.make()
62
+ let byType: Dict.t<array<int>> = Dict.make()
63
+ let pushInto = (dict: Dict.t<array<int>>, key: string, idx: int) =>
64
+ switch dict->Dict.get(key) {
65
+ | Some(list) => list->Array.push(idx)
66
+ | None => dict->Dict.set(key, [idx])
67
+ }
68
+ let indexEvent = (idx: int, ev: DcbEventLog_Adapter.rawSequencedEvent) => {
69
+ pushInto(byType, ev.eventType, idx)
70
+ ev.tags->Array.forEach(t => pushInto(byTag, tagPostingKey(t.key, t.value), idx))
71
+ }
31
72
 
32
- let read = async (~query, ~after=?) => {
33
- let filtered = events.contents->Array.filter(event => {
73
+ // Ascending, duplicate-free superset of the indices that could satisfy the
74
+ // query (an OR of clauses). Narrowing only — the authoritative `matchesQuery`
75
+ // predicate is still applied to each candidate, so the result is identical to
76
+ // the old linear scan even if this over-includes. Callers guarantee query≠[].
77
+ let candidateIndices = (query: Reventless.DcbTag.query): array<int> => {
78
+ let acc: Dict.t<int> = Dict.make()
79
+ let addAll = (arr: array<int>) => arr->Array.forEach(i => acc->Dict.set(Int.toString(i), i))
80
+ query->Array.forEach(clause => {
81
+ switch clause.tags {
82
+ | Some(tags) if tags->Array.length > 0 =>
83
+ // Events carrying ALL of the clause's tags = intersection of the tags'
84
+ // posting lists; a missing tag ⇒ empty ⇒ this clause adds nothing.
85
+ let lists = tags->Array.map(t =>
86
+ byTag->Dict.get(tagPostingKey(t.key, t.value))->Option.getOr([])
87
+ )
88
+ if lists->Array.some(l => l->Array.length == 0) {
89
+ ()
90
+ } else {
91
+ let sorted = lists->Array.toSorted((a, b) => Int.toFloat(a->Array.length - b->Array.length))
92
+ let acc0 = sorted->Array.getUnsafe(0)
93
+ let folded =
94
+ sorted->Array.slice(~start=1, ~end=sorted->Array.length)->Array.reduce(acc0, intersectSorted)
95
+ addAll(folded)
96
+ }
97
+ | _ =>
98
+ // No tag constraint: narrow by event type when present (union of the
99
+ // types' posting lists — disjoint since an event has one type), else the
100
+ // clause is unconstrained and matches everything.
101
+ switch clause.eventTypes {
102
+ | Some(types) => types->Array.forEach(t => addAll(byType->Dict.get(t)->Option.getOr([])))
103
+ | None => events.contents->Array.forEachWithIndex((_, i) => acc->Dict.set(Int.toString(i), i))
104
+ }
105
+ }
106
+ })
107
+ acc->Dict.valuesToArray->Array.toSorted((a, b) => Int.toFloat(a - b))
108
+ }
109
+
110
+ let matchingEvents = (~query, ~after): array<DcbEventLog_Adapter.rawSequencedEvent> => {
111
+ let pass = (ev: DcbEventLog_Adapter.rawSequencedEvent) => {
34
112
  let afterMatch = switch after {
35
- | Some(afterPos) => event.position->posToInt > afterPos->posToInt
113
+ | Some(afterPos) => ev.position->posToInt > afterPos->posToInt
36
114
  | None => true
37
115
  }
38
- afterMatch && matchesQuery(event, query)
39
- })
116
+ afterMatch && matchesQuery(ev, query)
117
+ }
118
+ if query->Array.length == 0 {
119
+ events.contents->Array.filter(pass)
120
+ } else {
121
+ candidateIndices(query)->Array.filterMap(i => {
122
+ let ev = events.contents->Array.getUnsafe(i)
123
+ pass(ev) ? Some(ev) : None
124
+ })
125
+ }
126
+ }
127
+
128
+ let read = async (~query, ~after=?) => {
129
+ let filtered = matchingEvents(~query, ~after)
40
130
  let headPosition =
41
131
  events.contents
42
132
  ->Array.get(events.contents->Array.length - 1)
@@ -50,30 +140,27 @@ let makeStorage = (~name as _name, ~indexes as _, ~partitionTag as _, ~opts as _
50
140
  let append = async (newEvents, ~condition=?) => {
51
141
  let conflictDetected = switch condition {
52
142
  | Some(cond: Reventless.DcbTag.appendCondition) =>
53
- events.contents->Array.some(event => {
54
- let afterMatch = switch cond.after {
55
- | Some(pos) => event.position->posToInt > pos->posToInt
56
- | None => true
57
- }
58
- afterMatch && matchesQuery(event, cond.query)
59
- })
143
+ matchingEvents(~query=cond.query, ~after=cond.after)->Array.length > 0
60
144
  | None => false
61
145
  }
62
146
  if conflictDetected {
63
147
  Error("conflict: condition check failed")
64
148
  } else {
149
+ let base = events.contents->Array.length
65
150
  let storedEvents =
66
- newEvents->Array.map((event: DcbEventLog_Adapter.rawStoredEvent) => {
151
+ newEvents->Array.mapWithIndex((event: DcbEventLog_Adapter.rawStoredEvent, j) => {
67
152
  position := position.contents + 1
68
153
  let pos = position.contents->Int.toString
69
- {
70
- DcbEventLog_Adapter.position: pos,
154
+ let stored: DcbEventLog_Adapter.rawSequencedEvent = {
155
+ position: pos,
71
156
  eventType: event.eventType,
72
157
  data: event.data,
73
158
  tags: event.tags,
74
159
  meta: event.meta,
75
160
  recordedAt: Message.nowAsISOString(),
76
161
  }
162
+ indexEvent(base + j, stored)
163
+ stored
77
164
  })
78
165
  events := events.contents->Array.concat(storedEvents)
79
166
  Ok(position.contents->Int.toString)
@@ -113,8 +200,6 @@ module Make = (Bus: LocalBus.T) => {
113
200
  | Some(db) =>
114
201
  let (storageName, read, storage) = DcbEventLogStorage_Sqlite.makeStorage(
115
202
  ~db,
116
- ~bus=(_, _) => (),
117
- ~publishToTopic=(~topicName as _, ~json as _) => (),
118
203
  ~name,
119
204
  ~indexes,
120
205
  ~partitionTag,