@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.
- package/CHANGELOG.md +10 -0
- package/package.json +10 -9
- package/rescript.json +2 -1
- package/src/Platform.res +70 -37
- package/src/Platform.res.mjs +48 -34
- package/src/adapter/BackendState.res +13 -0
- package/src/adapter/BackendState.res.mjs +17 -1
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +1 -1
- package/src/adapter/DomainGraphQL_Server.res +5 -5
- package/src/adapter/DomainGraphQL_Server.res.mjs +1 -1
- package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +1 -1
- package/src/adapter/LocalBus.res +6 -46
- package/src/adapter/LocalBus.res.mjs +0 -29
- package/src/adapter/LocalStateChangeDescriptor.res +93 -0
- package/src/adapter/LocalStateChangeDescriptor.res.mjs +61 -0
- package/src/adapter/LocalUploadResolvers.res +16 -3
- package/src/adapter/LocalUploadResolvers.res.mjs +6 -4
- package/src/adapter/ObjectStore/LocalObjectStore.res +157 -0
- package/src/adapter/ObjectStore/LocalObjectStore.res.mjs +129 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +215 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res.mjs +286 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_InMemory.res +33 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_InMemory.res.mjs +47 -0
- package/src/adapter/QueryDb/LocalQueryDbStorage.res.mjs +1 -1
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +4 -2
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +3 -3
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +4 -2
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +3 -3
- package/src/reset/LocalSeedReset.res +552 -0
- package/src/reset/LocalSeedReset.res.mjs +533 -0
- package/tests/adapter/BackendParityTest.res +51 -0
- package/tests/adapter/BackendParityTest.res.mjs +44 -0
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +6 -4
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res.mjs +4 -3
- package/tests/adapter/ObjectStorePersistenceTest.res +243 -0
- package/tests/adapter/ObjectStorePersistenceTest.res.mjs +159 -0
- package/tests/reset/LocalSeedResetTest.res +252 -0
- package/tests/reset/LocalSeedResetTest.res.mjs +280 -0
- package/src/adapter/LocalObjectStore.res +0 -70
- 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.
|
|
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/
|
|
44
|
-
"@reventlessdev/
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-
|
|
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) =>
|
|
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 =>
|
|
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 =>
|
|
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
|
}
|
package/src/Platform.res.mjs
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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
|
-
/*
|
|
70
|
+
/* node:path 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,
|
|
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") _,
|
|
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:
|
|
51
|
-
let chunks: array<
|
|
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(
|
|
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
|
|
package/src/adapter/LocalBus.res
CHANGED
|
@@ -47,48 +47,9 @@ type queuedEvent = {
|
|
|
47
47
|
done_: Effect.t<unit, unit, unit>,
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
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
|
|
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
|
-
// `
|
|
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.
|
|
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,
|