@reventlessdev/reventless-local 3.0.0-alpha.200 → 3.0.0-alpha.201

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 (40) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/package.json +10 -9
  3. package/rescript.json +2 -1
  4. package/src/Platform.res +70 -37
  5. package/src/Platform.res.mjs +48 -34
  6. package/src/adapter/BackendState.res +13 -0
  7. package/src/adapter/BackendState.res.mjs +17 -1
  8. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +1 -1
  9. package/src/adapter/DomainGraphQL_Server.res +5 -5
  10. package/src/adapter/DomainGraphQL_Server.res.mjs +1 -1
  11. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +1 -1
  12. package/src/adapter/LocalBus.res +6 -46
  13. package/src/adapter/LocalBus.res.mjs +0 -29
  14. package/src/adapter/LocalStateChangeDescriptor.res +93 -0
  15. package/src/adapter/LocalStateChangeDescriptor.res.mjs +61 -0
  16. package/src/adapter/LocalUploadResolvers.res +16 -3
  17. package/src/adapter/LocalUploadResolvers.res.mjs +6 -4
  18. package/src/adapter/ObjectStore/LocalObjectStore.res +157 -0
  19. package/src/adapter/ObjectStore/LocalObjectStore.res.mjs +129 -0
  20. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +215 -0
  21. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res.mjs +286 -0
  22. package/src/adapter/ObjectStore/ObjectStoreStorage_InMemory.res +33 -0
  23. package/src/adapter/ObjectStore/ObjectStoreStorage_InMemory.res.mjs +47 -0
  24. package/src/adapter/QueryDb/LocalQueryDbStorage.res.mjs +1 -1
  25. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +4 -2
  26. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +3 -3
  27. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +4 -2
  28. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +3 -3
  29. package/src/reset/LocalSeedReset.res +552 -0
  30. package/src/reset/LocalSeedReset.res.mjs +533 -0
  31. package/tests/adapter/BackendParityTest.res +51 -0
  32. package/tests/adapter/BackendParityTest.res.mjs +44 -0
  33. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +6 -4
  34. package/tests/adapter/GraphQL_SubscriptionResolversTest.res.mjs +4 -3
  35. package/tests/adapter/ObjectStorePersistenceTest.res +243 -0
  36. package/tests/adapter/ObjectStorePersistenceTest.res.mjs +159 -0
  37. package/tests/reset/LocalSeedResetTest.res +252 -0
  38. package/tests/reset/LocalSeedResetTest.res.mjs +280 -0
  39. package/src/adapter/LocalObjectStore.res +0 -70
  40. package/src/adapter/LocalObjectStore.res.mjs +0 -60
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
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.201 (2026-08-05)
7
+
8
+ ### Features
9
+
10
+ * **local,aws:** carry the changed row in the live-update descriptor ([9272e2e](https://github.com/ReventlessDev/reventless-core/commit/9272e2e18e9e1d740f9a2c5aa83eccbcb41feff7))
11
+ * **local:** offload large pluginDefinition fields to the object store ([84c5fab](https://github.com/ReventlessDev/reventless-core/commit/84c5fab339856b5d64f0c05110b1fa7f38646f5e))
12
+ * **local:** persist the object store beside the SQLite database ([f37e4a1](https://github.com/ReventlessDev/reventless-core/commit/f37e4a1ad3191a97f14f3db7e2ead0e2b27b46c2))
13
+ * **local:** scoped seed:reset for the local platform ([a4dd003](https://github.com/ReventlessDev/reventless-core/commit/a4dd0033b7d01be23be91bac3f01d38ebeab7d45))
14
+
15
+
6
16
  # 3.0.0-alpha.200 (2026-08-04)
7
17
 
8
18
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.200",
3
+ "version": "3.0.0-alpha.201",
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
  "bin": {
@@ -37,15 +37,16 @@
37
37
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
38
38
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
39
39
  "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
40
- "@reventlessdev/rescript-node": "2.0.0-alpha.1",
41
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
42
40
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.26",
43
- "@reventlessdev/reventless-core": "3.0.0-alpha.212",
44
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.60",
45
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.159",
46
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.76",
47
- "@reventlessdev/reventless-spec": "3.0.0-alpha.100",
48
- "@reventlessdev/reventless-infra": "3.0.0-alpha.126"
41
+ "@reventlessdev/rescript-node": "2.0.0-alpha.2",
42
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
43
+ "@reventlessdev/reventless-core": "3.0.0-alpha.213",
44
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.61",
45
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.127",
46
+ "@reventlessdev/reventless-seed": "1.0.0-alpha.9",
47
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.160",
48
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.101",
49
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.77"
49
50
  },
50
51
  "devDependencies": {
51
52
  "rescript": "12.3.0",
package/rescript.json CHANGED
@@ -35,7 +35,8 @@
35
35
  "@reventlessdev/reventless-spec",
36
36
  "@reventlessdev/reventless-core",
37
37
  "@reventlessdev/reventless-gwt",
38
- "@reventlessdev/reventless-postgres"
38
+ "@reventlessdev/reventless-postgres",
39
+ "@reventlessdev/reventless-seed"
39
40
  ],
40
41
  "compiler-flags": [],
41
42
  "suffix": ".res.mjs"
package/src/Platform.res CHANGED
@@ -118,6 +118,14 @@ module MakeWithConfig = (
118
118
  }
119
119
  let db = SqliteDriver.openDb(~path)
120
120
  BackendState.setSqlite(~db, ~path)
121
+ // The object store persists beside the database under this backend (see
122
+ // LocalObjectStore), so a reset that wipes the events wipes the bytes they
123
+ // reference too — otherwise the session starts with orphaned uploads and
124
+ // offload payloads no event points at. Runs after setSqlite because the
125
+ // store reads the active backend to find its root.
126
+ if resetOnStart {
127
+ LocalObjectStore.reset()
128
+ }
121
129
  // Persistent stores keep their event numbering across restarts and app
122
130
  // switches: seed the in-memory event-tap counter from the rows already on
123
131
  // disk so the timeline's #N continues instead of restarting at 1. (After a
@@ -837,6 +845,20 @@ module MakeWithConfig = (
837
845
  type mcpSupported = | @as(true) McpSupported | @as(false) McpNotSupported
838
846
  let mcpSupported = McpSupported
839
847
 
848
+ // Content-address the two large pluginDefinition fields into the process-local
849
+ // object store, exactly as the AWS platform does into its offload bucket. Local
850
+ // has no bucket, but it does have an object store — and leaving the fields
851
+ // inline here meant every connect handshake carried a whole plugin structure
852
+ // through the command log, and that local never exercised the offload path the
853
+ // deployed platform takes. Registered once at platform setup, before any plugin
854
+ // is built, since the hook fires during graph construction.
855
+ ReventlessCore.Plugin_Helpers.registerOffload((~store, ~bytes) => {
856
+ let hash = NodeCrypto.sha256Hex(bytes)
857
+ let key = "sha256/" ++ hash
858
+ LocalObjectStore.putOffload(~key, ~bytes)
859
+ {Reventless.Offload.store, key, hash, bytes: bytes->String.length}
860
+ })
861
+
840
862
  // ---------------------------------------------------------------------------
841
863
  // Shared helpers — used by both makePlatform and deployPlugin.
842
864
  // ---------------------------------------------------------------------------
@@ -852,6 +874,10 @@ module MakeWithConfig = (
852
874
  // In-memory store for plugin structures, keyed by plugin ID.
853
875
  let pluginStructuresStore: ref<dict<Reventless.Plugin.pluginStructure>> = ref(Dict.make())
854
876
 
877
+ // The object stores a plugin's fields declared, as `{plugin}.{store}` keys.
878
+ let declaredStoresOf = (structure: Reventless.Plugin.pluginStructure) =>
879
+ structure.requiredStores->Option.getOr([])
880
+
855
881
  let seedPluginStructuresStore = (~pluginComponents: array<ReventlessCore.Plugin.component>) => {
856
882
  pluginComponents->Array.forEach(plugin => {
857
883
  let outputs: ReventlessInfra.Plugin.outputs = plugin->ReventlessCore.Component.outputs
@@ -860,45 +886,42 @@ module MakeWithConfig = (
860
886
  ->Pulumi.Output.all2
861
887
  ->Pulumi.Output.apply(((id, ps)) => {
862
888
  switch ps {
863
- | Some(def) => pluginStructuresStore.contents->Dict.set(id, def)
889
+ | Some(def) =>
890
+ pluginStructuresStore.contents->Dict.set(id, def)
891
+ // Inside the apply, not after the forEach: the structure only exists
892
+ // once the Output resolves, so anything reading
893
+ // `pluginStructuresStore` synchronously below reads an empty dict.
894
+ declaredStoresOf(def)->Array.forEach(qualified =>
895
+ // Registering the store is what lets an uploaded object be attributed
896
+ // to the plugin that declared it, the way an S3 key's prefix does —
897
+ // under the served `uploads/` prefix locally, for the reason
898
+ // LocalObjectStore.localPrefixFor gives.
899
+ LocalObjectStore.registerStore(
900
+ ~qualified,
901
+ ~prefix=LocalObjectStore.localPrefixFor(~qualified),
902
+ )
903
+ )
904
+ // Two plugins declaring one store name is refused by the deployed
905
+ // platform before it provisions anything. Local provisions no stores, so
906
+ // nothing here breaks — but this is the only place a developer meets the
907
+ // rule while the fix is still free: once a store has deployed, its minted
908
+ // refs are in an append-only event log and renaming it strands them.
909
+ // Hence a warning at composition, and the authoritative refusal at deploy.
910
+ //
911
+ // Checked against every store registered so far rather than this
912
+ // plugin's own, since a collision is by definition between two plugins.
913
+ ReventlessCore.StorePrefixCollision.collisionsFor(
914
+ ~stores=LocalObjectStore.declaredStoreList()->Array.map(((qualified, prefix)) => {
915
+ ReventlessCore.StorePrefixCollision.qualified,
916
+ prefix,
917
+ }),
918
+ )->Array.forEach(c =>
919
+ log.warn(~comp="Platform:plugins", ReventlessCore.StorePrefixCollision.collisionMessage(c))
920
+ )
864
921
  | None => ()
865
922
  }
866
923
  })
867
924
  })
868
-
869
- // Two plugins declaring one store name is refused by the deployed platform
870
- // before it provisions anything. Local provisions no stores, so nothing here
871
- // breaks — but this is the only place a developer meets the rule while the
872
- // fix is still free: once a store has deployed, its minted refs are in an
873
- // append-only event log and renaming it strands them. Hence a warning at
874
- // composition, and the authoritative refusal at deploy.
875
- let declared =
876
- pluginStructuresStore.contents
877
- ->Dict.valuesToArray
878
- ->Array.flatMap(structure =>
879
- structure.requiredStores
880
- ->Option.getOr([])
881
- ->Array.map(qualified => {
882
- ReventlessCore.StorePrefixCollision.qualified,
883
- // Local provisions no stores, so it has no prefix of its own — it
884
- // restates the deployed rule (`{plugin}/{store}`) so the warning it
885
- // gives matches the refusal a deploy would give. Reporting a collision
886
- // the deployed platform does not have would be worse than silence.
887
- prefix: switch qualified->String.indexOfOpt(".") {
888
- | Some(i) =>
889
- qualified->String.slice(~start=0, ~end=i) ++
890
- "/" ++
891
- qualified->String.slice(~start=i + 1, ~end=qualified->String.length)
892
- | None => qualified
893
- },
894
- })
895
- )
896
- ReventlessCore.StorePrefixCollision.collisionsFor(~stores=declared)->Array.forEach(c =>
897
- log.warn(
898
- ~comp="Platform:plugins",
899
- ReventlessCore.StorePrefixCollision.collisionMessage(c),
900
- )
901
- )
902
925
  }
903
926
 
904
927
  // Bus keys for the admin Plugin aggregate (name-keyed). ComponentType.toName maps
@@ -1113,9 +1136,19 @@ module MakeWithConfig = (
1113
1136
  },
1114
1137
  ),
1115
1138
  extensionProtocols: [],
1116
- apiSchemaFragment: apiSchemaFragment->Option.map(f => Reventless.Offload.Inline(f)),
1139
+ apiSchemaFragment: apiSchemaFragment->Option.map(f =>
1140
+ f->ReventlessCore.Plugin_Helpers.offloadPayload(
1141
+ ~schema=Reventless.Plugin.apiSchemaFragmentSchema,
1142
+ ~store="pluginApiFragments",
1143
+ )
1144
+ ),
1117
1145
  apiTarget: None,
1118
- structure: pluginStructure->Option.map(s => Reventless.Offload.Inline(s)),
1146
+ structure: pluginStructure->Option.map(s =>
1147
+ s->ReventlessCore.Plugin_Helpers.offloadPayload(
1148
+ ~schema=Reventless.Plugin.pluginStructureSchema,
1149
+ ~store="pluginStructures",
1150
+ )
1151
+ ),
1119
1152
  dcbEventLog: None,
1120
1153
  kind: Domain,
1121
1154
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as S from "sury/src/S.res.mjs";
4
4
  import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
5
+ import * as NodeCrypto from "@reventlessdev/rescript-node/src/NodeCrypto.res.mjs";
5
6
  import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
6
7
  import * as Stdlib_Bool from "@rescript/runtime/lib/es6/Stdlib_Bool.js";
7
8
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
@@ -40,6 +41,7 @@ import * as LocalPluginSpec$ReventlessLocal from "./adapter/LocalPluginSpec.res.
40
41
  import * as NoEventMappings$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/NoEventMappings.res.mjs";
41
42
  import * as DomainMCP_Server$ReventlessLocal from "./adapter/DomainMCP_Server.res.mjs";
42
43
  import * as ExtensionMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionMapping.res.mjs";
44
+ import * as LocalObjectStore$ReventlessLocal from "./adapter/ObjectStore/LocalObjectStore.res.mjs";
43
45
  import * as LocalQueryEngine$ReventlessLocal from "./adapter/QueryEngine/LocalQueryEngine.res.mjs";
44
46
  import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
45
47
  import * as PluginsProjection$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginsProjection.res.mjs";
@@ -127,12 +129,16 @@ function MakeWithConfig(Config) {
127
129
  if (typeof match !== "object") {
128
130
  BackendState$ReventlessLocal.setMemory();
129
131
  } else if (match.TAG === "Sqlite") {
132
+ let resetOnStart = match.resetOnStart;
130
133
  let path = match.path;
131
- if (match.resetOnStart) {
134
+ if (resetOnStart) {
132
135
  Backend$ReventlessLocal.removeFileIfExists(path);
133
136
  }
134
137
  let db = SqliteDriver$ReventlessLocal.openDb(path);
135
138
  BackendState$ReventlessLocal.setSqlite(db, path);
139
+ if (resetOnStart) {
140
+ LocalObjectStore$ReventlessLocal.reset();
141
+ }
136
142
  LocalBus$ReventlessLocal.seedEventTapSeq(EventLogStorage_Sqlite$ReventlessLocal.countAll(db) + DcbEventLogStorage_Sqlite$ReventlessLocal.countAll(db) | 0);
137
143
  ProjectionPending$ReventlessLocal.enableTracking();
138
144
  EventPublish_Callback$ReventlessCore.registerAfterPublish(async publishedEvent => {
@@ -810,6 +816,17 @@ function MakeWithConfig(Config) {
810
816
  let domainMcpPort = resolvePort("REVENTLESS_DOMAIN_MCP_PORT", 3001);
811
817
  let platformPort = resolvePort("REVENTLESS_PLATFORM_PORT", 4001);
812
818
  let platformMcpPort = resolvePort("REVENTLESS_PLATFORM_MCP_PORT", 3002);
819
+ Plugin_Helpers$ReventlessCore.registerOffload((store, bytes) => {
820
+ let hash = NodeCrypto.sha256Hex(bytes);
821
+ let key = "sha256/" + hash;
822
+ LocalObjectStore$ReventlessLocal.putOffload(key, bytes);
823
+ return {
824
+ store: store,
825
+ key: key,
826
+ hash: hash,
827
+ bytes: bytes.length
828
+ };
829
+ });
813
830
  let pluginStructuresStore = {
814
831
  contents: {}
815
832
  };
@@ -823,18 +840,15 @@ function MakeWithConfig(Config) {
823
840
  let ps = param[1];
824
841
  if (ps !== undefined) {
825
842
  pluginStructuresStore.contents[param[0]] = ps;
843
+ Stdlib_Option.getOr(ps.requiredStores, []).forEach(qualified => LocalObjectStore$ReventlessLocal.registerStore(qualified, LocalObjectStore$ReventlessLocal.localPrefixFor(qualified)));
844
+ StorePrefixCollision$ReventlessCore.collisionsFor(LocalObjectStore$ReventlessLocal.declaredStoreList().map(param => ({
845
+ qualified: param[0],
846
+ prefix: param[1]
847
+ }))).forEach(c => log.warn("Platform:plugins", undefined, StorePrefixCollision$ReventlessCore.collisionMessage(c)));
826
848
  return;
827
849
  }
828
850
  });
829
851
  });
830
- let declared = Object.values(pluginStructuresStore.contents).flatMap(structure => Stdlib_Option.getOr(structure.requiredStores, []).map(qualified => {
831
- let i = Stdlib_String.indexOfOpt(qualified, ".");
832
- return {
833
- qualified: qualified,
834
- prefix: i !== undefined ? qualified.slice(0, i) + "/" + qualified.slice(i + 1 | 0, qualified.length) : qualified
835
- };
836
- }));
837
- StorePrefixCollision$ReventlessCore.collisionsFor(declared).forEach(c => log.warn("Platform:plugins", undefined, StorePrefixCollision$ReventlessCore.collisionMessage(c)));
838
852
  };
839
853
  let pluginCmdTopicKey = PluginSpec$ReventlessCore.name + "AggrCmdTopic";
840
854
  let pluginEventTopicKey = PluginSpec$ReventlessCore.name + "AggrEventTopic";
@@ -971,15 +985,9 @@ function MakeWithConfig(Config) {
971
985
  }),
972
986
  eventCollector: param[2].name,
973
987
  extensionProtocols: [],
974
- apiSchemaFragment: Stdlib_Option.map(param[5], f => ({
975
- TAG: "Inline",
976
- _0: f
977
- })),
988
+ apiSchemaFragment: Stdlib_Option.map(param[5], f => Plugin_Helpers$ReventlessCore.offloadPayload(f, Plugin$Reventless.apiSchemaFragmentSchema, "pluginApiFragments")),
978
989
  apiTarget: undefined,
979
- structure: Stdlib_Option.map(param[7], s => ({
980
- TAG: "Inline",
981
- _0: s
982
- })),
990
+ structure: Stdlib_Option.map(param[7], s => Plugin_Helpers$ReventlessCore.offloadPayload(s, Plugin$Reventless.pluginStructureSchema, "pluginStructures")),
983
991
  dcbEventLog: undefined,
984
992
  kind: "Domain"
985
993
  };
@@ -1823,12 +1831,16 @@ function Make($star) {
1823
1831
  if (typeof backend !== "object") {
1824
1832
  BackendState$ReventlessLocal.setMemory();
1825
1833
  } else if (backend.TAG === "Sqlite") {
1834
+ let resetOnStart = backend.resetOnStart;
1826
1835
  let path = backend.path;
1827
- if (backend.resetOnStart) {
1836
+ if (resetOnStart) {
1828
1837
  Backend$ReventlessLocal.removeFileIfExists(path);
1829
1838
  }
1830
1839
  let db = SqliteDriver$ReventlessLocal.openDb(path);
1831
1840
  BackendState$ReventlessLocal.setSqlite(db, path);
1841
+ if (resetOnStart) {
1842
+ LocalObjectStore$ReventlessLocal.reset();
1843
+ }
1832
1844
  LocalBus$ReventlessLocal.seedEventTapSeq(EventLogStorage_Sqlite$ReventlessLocal.countAll(db) + DcbEventLogStorage_Sqlite$ReventlessLocal.countAll(db) | 0);
1833
1845
  ProjectionPending$ReventlessLocal.enableTracking();
1834
1846
  EventPublish_Callback$ReventlessCore.registerAfterPublish(async publishedEvent => {
@@ -2500,6 +2512,17 @@ function Make($star) {
2500
2512
  let domainMcpPort = resolvePort("REVENTLESS_DOMAIN_MCP_PORT", 3001);
2501
2513
  let platformPort = resolvePort("REVENTLESS_PLATFORM_PORT", 4001);
2502
2514
  let platformMcpPort = resolvePort("REVENTLESS_PLATFORM_MCP_PORT", 3002);
2515
+ Plugin_Helpers$ReventlessCore.registerOffload((store, bytes) => {
2516
+ let hash = NodeCrypto.sha256Hex(bytes);
2517
+ let key = "sha256/" + hash;
2518
+ LocalObjectStore$ReventlessLocal.putOffload(key, bytes);
2519
+ return {
2520
+ store: store,
2521
+ key: key,
2522
+ hash: hash,
2523
+ bytes: bytes.length
2524
+ };
2525
+ });
2503
2526
  let pluginStructuresStore = {
2504
2527
  contents: {}
2505
2528
  };
@@ -2513,18 +2536,15 @@ function Make($star) {
2513
2536
  let ps = param[1];
2514
2537
  if (ps !== undefined) {
2515
2538
  pluginStructuresStore.contents[param[0]] = ps;
2539
+ Stdlib_Option.getOr(ps.requiredStores, []).forEach(qualified => LocalObjectStore$ReventlessLocal.registerStore(qualified, LocalObjectStore$ReventlessLocal.localPrefixFor(qualified)));
2540
+ StorePrefixCollision$ReventlessCore.collisionsFor(LocalObjectStore$ReventlessLocal.declaredStoreList().map(param => ({
2541
+ qualified: param[0],
2542
+ prefix: param[1]
2543
+ }))).forEach(c => log.warn("Platform:plugins", undefined, StorePrefixCollision$ReventlessCore.collisionMessage(c)));
2516
2544
  return;
2517
2545
  }
2518
2546
  });
2519
2547
  });
2520
- let declared = Object.values(pluginStructuresStore.contents).flatMap(structure => Stdlib_Option.getOr(structure.requiredStores, []).map(qualified => {
2521
- let i = Stdlib_String.indexOfOpt(qualified, ".");
2522
- return {
2523
- qualified: qualified,
2524
- prefix: i !== undefined ? qualified.slice(0, i) + "/" + qualified.slice(i + 1 | 0, qualified.length) : qualified
2525
- };
2526
- }));
2527
- StorePrefixCollision$ReventlessCore.collisionsFor(declared).forEach(c => log.warn("Platform:plugins", undefined, StorePrefixCollision$ReventlessCore.collisionMessage(c)));
2528
2548
  };
2529
2549
  let pluginCmdTopicKey = PluginSpec$ReventlessCore.name + "AggrCmdTopic";
2530
2550
  let pluginEventTopicKey = PluginSpec$ReventlessCore.name + "AggrEventTopic";
@@ -2661,15 +2681,9 @@ function Make($star) {
2661
2681
  }),
2662
2682
  eventCollector: param[2].name,
2663
2683
  extensionProtocols: [],
2664
- apiSchemaFragment: Stdlib_Option.map(param[5], f => ({
2665
- TAG: "Inline",
2666
- _0: f
2667
- })),
2684
+ apiSchemaFragment: Stdlib_Option.map(param[5], f => Plugin_Helpers$ReventlessCore.offloadPayload(f, Plugin$Reventless.apiSchemaFragmentSchema, "pluginApiFragments")),
2668
2685
  apiTarget: undefined,
2669
- structure: Stdlib_Option.map(param[7], s => ({
2670
- TAG: "Inline",
2671
- _0: s
2672
- })),
2686
+ structure: Stdlib_Option.map(param[7], s => Plugin_Helpers$ReventlessCore.offloadPayload(s, Plugin$Reventless.pluginStructureSchema, "pluginStructures")),
2673
2687
  dcbEventLog: undefined,
2674
2688
  kind: "Domain"
2675
2689
  };
@@ -37,3 +37,16 @@ let getPostgresPool = () =>
37
37
  | Postgres({pool}) => Some(pool)
38
38
  | Memory | Sqlite(_) => None
39
39
  }
40
+
41
+ // Directory the object store persists under, derived from the SQLite file's own
42
+ // directory (`./.reventless/local.db` → `./.reventless`) so uploaded bytes and
43
+ // offloaded payloads live and die with the events that reference them. None for
44
+ // the backends with nothing durable on this disk: Memory, an in-process
45
+ // `:memory:` SQLite, and Postgres — durable, but off-machine, and a connection
46
+ // string names no local directory to anchor to, so its object store stays
47
+ // in-process.
48
+ let getObjectStoreRoot = () =>
49
+ switch current.contents {
50
+ | Sqlite({path}) if path != ":memory:" => Some(NodePath.dirname(path))
51
+ | Sqlite(_) | Memory | Postgres(_) => None
52
+ }
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Nodepath from "node:path";
3
4
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
4
5
 
5
6
  let current = {
@@ -43,6 +44,20 @@ function getPostgresPool() {
43
44
  }
44
45
  }
45
46
 
47
+ function getObjectStoreRoot() {
48
+ let match = current.contents;
49
+ if (typeof match !== "object") {
50
+ return;
51
+ }
52
+ if (match.TAG !== "Sqlite") {
53
+ return;
54
+ }
55
+ let path = match.path;
56
+ if (path !== ":memory:") {
57
+ return Nodepath.dirname(path);
58
+ }
59
+ }
60
+
46
61
  export {
47
62
  current,
48
63
  setMemory,
@@ -50,5 +65,6 @@ export {
50
65
  setPostgres,
51
66
  getSqliteDb,
52
67
  getPostgresPool,
68
+ getObjectStoreRoot,
53
69
  }
54
- /* No side effect */
70
+ /* node:path Not a pure module */
@@ -32,4 +32,4 @@ function Make(Bus) {
32
32
  export {
33
33
  Make,
34
34
  }
35
- /* DcbEventLogStorage_Sqlite-ReventlessLocal Not a pure module */
35
+ /* BackendState-ReventlessLocal Not a pure module */
@@ -27,7 +27,7 @@ type nodeResponse
27
27
  @send external end_: (nodeResponse, string) => unit = "end"
28
28
  @send external endEmpty: (nodeResponse, @as(json`null`) _) => unit = "end"
29
29
  // Binary response body — the served-object GET streams stored bytes verbatim.
30
- @send external endBuf: (nodeResponse, LocalObjectStore.buffer) => unit = "end"
30
+ @send external endBuf: (nodeResponse, NodeBuffer.t) => unit = "end"
31
31
 
32
32
  // Streaming body collector — request data fires once per chunk, end once at EOF.
33
33
  @send external _onData: (nodeRequest, @as("data") _, string => unit) => unit = "on"
@@ -35,7 +35,7 @@ type nodeResponse
35
35
  @send external _setEncoding: (nodeRequest, string) => unit = "setEncoding"
36
36
  // Binary chunk stream — used by the served-object PUT so raw bytes aren't
37
37
  // mangled by utf8 decoding (setEncoding is deliberately NOT called).
38
- @send external _onDataBuf: (nodeRequest, @as("data") _, LocalObjectStore.buffer => unit) => unit = "on"
38
+ @send external _onDataBuf: (nodeRequest, @as("data") _, NodeBuffer.t => unit) => unit = "on"
39
39
 
40
40
 
41
41
  let readBody = (req: nodeRequest, onBody: string => unit): unit => {
@@ -47,10 +47,10 @@ let readBody = (req: nodeRequest, onBody: string => unit): unit => {
47
47
 
48
48
  // Binary body collector — accumulates raw Buffer chunks and concatenates once
49
49
  // at EOF. Kept separate from `readBody` (utf8) so file uploads stay byte-exact.
50
- let readBodyBuf = (req: nodeRequest, onBody: LocalObjectStore.buffer => unit): unit => {
51
- let chunks: array<LocalObjectStore.buffer> = []
50
+ let readBodyBuf = (req: nodeRequest, onBody: NodeBuffer.t => unit): unit => {
51
+ let chunks: array<NodeBuffer.t> = []
52
52
  req->_onDataBuf(chunk => chunks->Array.push(chunk))
53
- req->_onEnd(() => onBody(LocalObjectStore.concatBuffers(chunks)))
53
+ req->_onEnd(() => onBody(NodeBuffer.concat(chunks)))
54
54
  }
55
55
 
56
56
  type requestHandler = (nodeRequest, nodeResponse) => unit
@@ -17,7 +17,7 @@ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_excep
17
17
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
18
18
  import * as LocalAuth$ReventlessLocal from "./Auth/LocalAuth.res.mjs";
19
19
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
20
- import * as LocalObjectStore$ReventlessLocal from "./LocalObjectStore.res.mjs";
20
+ import * as LocalObjectStore$ReventlessLocal from "./ObjectStore/LocalObjectStore.res.mjs";
21
21
  import * as LocalEvents_Server$ReventlessLocal from "./Api/LocalEvents_Server.res.mjs";
22
22
  import * as Auth_GraphqlContext$ReventlessLocal from "./Auth/Auth_GraphqlContext.res.mjs";
23
23
 
@@ -32,4 +32,4 @@ function Make(Bus) {
32
32
  export {
33
33
  Make,
34
34
  }
35
- /* EventLogStorage_Sqlite-ReventlessLocal Not a pure module */
35
+ /* BackendState-ReventlessLocal Not a pure module */
@@ -47,48 +47,9 @@ type queuedEvent = {
47
47
  done_: Effect.t<unit, unit, unit>,
48
48
  }
49
49
 
50
- // ── State-change descriptor helper ───────────────────────────────────────────
51
- // Mirrors the JSON shape produced by the AWS StateTopic Lambda (Phase 2):
52
- // `{ changeKind, id, sortKeyValue? }`. Storage adapters call this and pass
53
- // the result to `publishStateChange(~descriptor)` so dev and prod subscribers
54
- // see the same payload.
55
-
56
- let pickSortKeyValue = (state: JSON.t): option<string> =>
57
- switch state->JSON.Decode.object {
58
- | Some(obj) =>
59
- switch obj->Dict.get("updatedAt") {
60
- | Some(JSON.String(v)) => Some(v)
61
- | _ =>
62
- switch obj->Dict.get("createdAt") {
63
- | Some(JSON.String(v)) => Some(v)
64
- | _ => None
65
- }
66
- }
67
- | None => None
68
- }
69
-
70
- /** Build a state-change descriptor.
71
- - `changeKind`: one of "Added" | "Updated" | "Removed". save() emits "Added"
72
- when no visible row held the key and "Updated" otherwise; delete() emits
73
- "Removed".
74
- - `id`: entity key. Single-key projections: the partition-key value.
75
- Composite projections: `partition ++ "-" ++ subKey`.
76
- - `state`: present for save() (for `sortKeyValue` extraction);
77
- `None` for delete() — descriptor will omit `sortKeyValue`. */
78
- let makeStateChangeDescriptor = (
79
- ~changeKind: string,
80
- ~id: string,
81
- ~state: option<JSON.t>,
82
- ): JSON.t => {
83
- let descriptor = Dict.make()
84
- descriptor->Dict.set("changeKind", JSON.Encode.string(changeKind))
85
- descriptor->Dict.set("id", JSON.Encode.string(id))
86
- switch state->Option.flatMap(pickSortKeyValue) {
87
- | Some(v) => descriptor->Dict.set("sortKeyValue", JSON.Encode.string(v))
88
- | None => ()
89
- }
90
- descriptor->JSON.Encode.object
91
- }
50
+ // The state-change descriptor itself lives in `LocalStateChangeDescriptor` —
51
+ // it is one of three implementations of a shared wire format and is covered by
52
+ // its own parity test, so it stays out of the bus.
92
53
 
93
54
  // Opt-in NDJSON domain-event tap for the VS Code local platform runner (features
94
55
  // plan Phase 9). Enabled when REVENTLESS_EVENT_TAP is set; off by default so normal
@@ -205,16 +166,15 @@ module type T = {
205
166
  //
206
167
  // Source B (state changes): QueryDbStorage_InMemory calls publishStateChange after
207
168
  // every save/delete so subscription listeners receive a change descriptor matching
208
- // the AWS StateTopic Lambda output (Phase 2): {changeKind, id, sortKeyValue?}.
169
+ // the AWS StateTopic Lambda output: {changeKind, id, sortKeyValue?, seq, state?}.
209
170
  // ~name is the QueryDb/ReadModel Spec.name; ~descriptor is built via
210
- // `makeStateChangeDescriptor` below.
171
+ // `LocalStateChangeDescriptor.make`.
211
172
  //
212
173
  // `changeKind` matches AWS, which reads it off the DynamoDB stream eventName:
213
174
  // save() checks whether a visible row already held the key and emits "Added"
214
175
  // or "Updated" accordingly, `delete()` emits "Removed". Without the "Added"
215
176
  // arm a list view drops every row it doesn't already hold, so seeding into an
216
- // empty read model looked like live updates were broken. `position` is
217
- // omitted (Phase 3 deferred).
177
+ // empty read model looked like live updates were broken.
218
178
  let publishStateChange: (~name: string, ~descriptor: JSON.t) => unit
219
179
  let subscribeToStateChanges: (string, JSON.t => unit) => unit
220
180
  // All-changes variant: receives every publishStateChange with its read-model
@@ -2,7 +2,6 @@
2
2
 
3
3
  import * as Effect from "@reventlessdev/rescript-effect/src/Effect.res.mjs";
4
4
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
- import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
5
  import * as Queue from "effect/Queue";
7
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
7
  import * as Effect$1 from "effect/Effect";
@@ -12,32 +11,6 @@ import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
12
11
  import * as Deferred from "effect/Deferred";
13
12
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
14
13
 
15
- function pickSortKeyValue(state) {
16
- let obj = Stdlib_JSON.Decode.object(state);
17
- if (obj === undefined) {
18
- return;
19
- }
20
- let match = obj["updatedAt"];
21
- if (typeof match === "string") {
22
- return match;
23
- }
24
- let match$1 = obj["createdAt"];
25
- if (typeof match$1 === "string") {
26
- return match$1;
27
- }
28
- }
29
-
30
- function makeStateChangeDescriptor(changeKind, id, state) {
31
- let descriptor = {};
32
- descriptor["changeKind"] = changeKind;
33
- descriptor["id"] = id;
34
- let v = Stdlib_Option.flatMap(state, pickSortKeyValue);
35
- if (v !== undefined) {
36
- descriptor["sortKeyValue"] = v;
37
- }
38
- return descriptor;
39
- }
40
-
41
14
  function eventTapEnabled() {
42
15
  return Stdlib_Option.isSome(process.env["REVENTLESS_EVENT_TAP"]);
43
16
  }
@@ -1187,8 +1160,6 @@ function MakeBounded(C) {
1187
1160
  }
1188
1161
 
1189
1162
  export {
1190
- pickSortKeyValue,
1191
- makeStateChangeDescriptor,
1192
1163
  eventTapEnabled,
1193
1164
  eventTapSeq,
1194
1165
  seedEventTapSeq,