@reventlessdev/reventless-local 3.0.0-alpha.183 → 3.0.0-alpha.184
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 +8 -0
- package/package.json +13 -12
- package/rescript.json +1 -0
- package/src/EmitCapabilities.res +16 -31
- package/src/Platform.res +2 -3
- package/src/adapter/Auth/LocalAuth.res +5 -12
- package/src/adapter/Auth/UserStore.res +3 -7
- package/src/adapter/Auth/UserStore.res.mjs +1 -1
- package/src/adapter/Backend.res +2 -4
- package/src/adapter/DomainGraphQL_Server.res +2 -4
- package/src/adapter/DomainGraphQL_Server.res.mjs +2 -2
- package/src/adapter/DomainMCP_Server.res +1 -2
- package/src/adapter/LocalBus.res +1 -2
- package/src/adapter/MCP_ServerInstance.res +1 -2
- package/src/adapter/SqliteDriver.res +1 -5
- package/tests/adapter/BackendParityTest.res +1 -2
- package/tests/adapter/EventTapTest.res +3 -4
- package/tests/adapter/LocalAuthUserStoreTest.res +5 -10
- package/tests/adapter/PgProjectionCatchupTest.res +1 -2
- package/tests/adapter/SqliteDriverTest.res +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.184 (2026-07-31)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
# 3.0.0-alpha.183 (2026-07-30)
|
|
7
15
|
|
|
8
16
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.184",
|
|
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": {
|
|
@@ -34,17 +34,18 @@
|
|
|
34
34
|
"graphql-yoga": "^5.21.0",
|
|
35
35
|
"sury": "11.0.0-alpha.4",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
|
-
"@reventlessdev/rescript-
|
|
38
|
-
"@reventlessdev/rescript-
|
|
39
|
-
"@reventlessdev/rescript-
|
|
40
|
-
"@reventlessdev/rescript-
|
|
41
|
-
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.
|
|
42
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
43
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.
|
|
44
|
-
"@reventlessdev/
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
37
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
38
|
+
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.26",
|
|
39
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
40
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.0",
|
|
41
|
+
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
|
|
42
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.196",
|
|
43
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.143",
|
|
44
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
45
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.44",
|
|
46
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.60",
|
|
47
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.114",
|
|
48
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.89"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"rescript": "12.3.0",
|
package/rescript.json
CHANGED
package/src/EmitCapabilities.res
CHANGED
|
@@ -20,21 +20,6 @@
|
|
|
20
20
|
// Usage: emit-capabilities <srcDir> [<compositionModule>]
|
|
21
21
|
// (run from the plugin package, after `rescript build`)
|
|
22
22
|
|
|
23
|
-
// ── Node ─────────────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
@module("node:fs") external existsSync: string => bool = "existsSync"
|
|
26
|
-
@module("node:fs") external writeFileSync: (string, string, @as("utf8") _) => unit = "writeFileSync"
|
|
27
|
-
|
|
28
|
-
@module("node:path") @variadic external join: array<string> => string = "join"
|
|
29
|
-
@module("node:path") @variadic external resolve: array<string> => string = "resolve"
|
|
30
|
-
@module("node:path") @val external sep: string = "sep"
|
|
31
|
-
|
|
32
|
-
@module("node:url") external pathToFileURL: string => {"href": string} = "pathToFileURL"
|
|
33
|
-
|
|
34
|
-
@val @scope("process") external argv: array<string> = "argv"
|
|
35
|
-
@val @scope("process") external env: Dict.t<string> = "env"
|
|
36
|
-
@val external processExit: int => unit = "process.exit"
|
|
37
|
-
|
|
38
23
|
// Emitted by ReScript as a literal `import(...)` expression — the composition
|
|
39
24
|
// root's path is only known at run time, so it cannot be a static binding.
|
|
40
25
|
@val external dynImport: string => promise<'a> = "import"
|
|
@@ -56,11 +41,11 @@ type compositionExports = {"Make": platform => builtPlugin}
|
|
|
56
41
|
let compositionModulePath = (~srcDir: string, ~moduleArg: option<string>): string => {
|
|
57
42
|
let withExtension = name => name->String.endsWith(".mjs") ? name : name ++ ".res.mjs"
|
|
58
43
|
switch moduleArg {
|
|
59
|
-
| None | Some("") => join([srcDir, "Plugin.res.mjs"])
|
|
44
|
+
| None | Some("") => NodePath.join([srcDir, "Plugin.res.mjs"])
|
|
60
45
|
| Some(name) =>
|
|
61
|
-
name->String.includes("/") || name->String.includes(sep)
|
|
62
|
-
? resolve([withExtension(name)])
|
|
63
|
-
: join([srcDir, withExtension(name)])
|
|
46
|
+
name->String.includes("/") || name->String.includes(NodePath.sep)
|
|
47
|
+
? NodePath.resolve([withExtension(name)])
|
|
48
|
+
: NodePath.join([srcDir, withExtension(name)])
|
|
64
49
|
}
|
|
65
50
|
}
|
|
66
51
|
|
|
@@ -68,25 +53,25 @@ let compositionModulePath = (~srcDir: string, ~moduleArg: option<string>): strin
|
|
|
68
53
|
|
|
69
54
|
let fail = (message: string) => {
|
|
70
55
|
Console.error("emit-capabilities: " ++ message)
|
|
71
|
-
|
|
56
|
+
NodeProcess.exit(1)
|
|
72
57
|
}
|
|
73
58
|
|
|
74
59
|
let main = async () => {
|
|
75
60
|
// The local platform defaults to Debug-level logging; a build step should not.
|
|
76
|
-
switch env->Dict.get("LOG_LEVEL") {
|
|
61
|
+
switch NodeProcess.env->Dict.get("LOG_LEVEL") {
|
|
77
62
|
| Some(_) => ()
|
|
78
|
-
| None => env->Dict.set("LOG_LEVEL", "warn")
|
|
63
|
+
| None => NodeProcess.env->Dict.set("LOG_LEVEL", "warn")
|
|
79
64
|
}
|
|
80
65
|
|
|
81
|
-
switch argv->Array.get(2) {
|
|
66
|
+
switch NodeProcess.argv->Array.get(2) {
|
|
82
67
|
| None | Some("") => {
|
|
83
68
|
Console.error("Usage: emit-capabilities <srcDir> [<compositionModule>]")
|
|
84
|
-
|
|
69
|
+
NodeProcess.exit(1)
|
|
85
70
|
}
|
|
86
71
|
| Some(srcDirArg) => {
|
|
87
|
-
let srcDir = resolve([srcDirArg])
|
|
88
|
-
let modulePath = compositionModulePath(~srcDir, ~moduleArg=argv->Array.get(3))
|
|
89
|
-
if !existsSync(modulePath) {
|
|
72
|
+
let srcDir = NodePath.resolve([srcDirArg])
|
|
73
|
+
let modulePath = compositionModulePath(~srcDir, ~moduleArg=NodeProcess.argv->Array.get(3))
|
|
74
|
+
if !NodeFs.existsSync(modulePath) {
|
|
90
75
|
fail(`${modulePath} not found — run \`rescript build\` first.`)
|
|
91
76
|
}
|
|
92
77
|
|
|
@@ -95,11 +80,11 @@ let main = async () => {
|
|
|
95
80
|
let platformModule: localPlatformExports = await dynImport("./Platform.res.mjs")
|
|
96
81
|
let platform = platformModule["Make"]()
|
|
97
82
|
|
|
98
|
-
let composition: compositionExports = await dynImport(pathToFileURL(modulePath)["href"])
|
|
83
|
+
let composition: compositionExports = await dynImport(NodeUrl.pathToFileURL(modulePath)["href"])
|
|
99
84
|
let built = composition["Make"](platform)
|
|
100
85
|
|
|
101
|
-
let manifestPath = join([srcDir, "capabilities.json"])
|
|
102
|
-
writeFileSync(
|
|
86
|
+
let manifestPath = NodePath.join([srcDir, "capabilities.json"])
|
|
87
|
+
NodeFs.writeFileSync(
|
|
103
88
|
manifestPath,
|
|
104
89
|
Reventless.CapabilityManifest.renderForStructure(built["pluginStructure"]),
|
|
105
90
|
)
|
|
@@ -107,7 +92,7 @@ let main = async () => {
|
|
|
107
92
|
|
|
108
93
|
// Applying the platform functor wires in-process infrastructure; exit
|
|
109
94
|
// explicitly so no lingering handle keeps the build step alive.
|
|
110
|
-
|
|
95
|
+
NodeProcess.exit(0)
|
|
111
96
|
}
|
|
112
97
|
}
|
|
113
98
|
}
|
package/src/Platform.res
CHANGED
|
@@ -821,15 +821,14 @@ module MakeWithConfig = (
|
|
|
821
821
|
component->ReventlessCore.Component.operations
|
|
822
822
|
}
|
|
823
823
|
|
|
824
|
-
|
|
825
|
-
let graphqlDebug = processEnv->Dict.get("GRAPHQL_DEBUG")->Option.isSome
|
|
824
|
+
let graphqlDebug = NodeProcess.env->Dict.get("GRAPHQL_DEBUG")->Option.isSome
|
|
826
825
|
|
|
827
826
|
// Per-session server ports for the VS Code local platform runner (features
|
|
828
827
|
// plan Phase 9). The runner spawns one platform child per app folder; default
|
|
829
828
|
// ports (4000/4001/3001/3002) collide when several run, so each is overridable
|
|
830
829
|
// via env. Defaults preserve today's behaviour for all non-runner callers.
|
|
831
830
|
let resolvePort = (key, fallback) =>
|
|
832
|
-
|
|
831
|
+
NodeProcess.env->Dict.get(key)->Option.flatMap(v => Int.fromString(v))->Option.getOr(fallback)
|
|
833
832
|
let domainPort = resolvePort("REVENTLESS_DOMAIN_PORT", 4000)
|
|
834
833
|
let domainMcpPort = resolvePort("REVENTLESS_DOMAIN_MCP_PORT", 3001)
|
|
835
834
|
let platformPort = resolvePort("REVENTLESS_PLATFORM_PORT", 4001)
|
|
@@ -95,24 +95,15 @@ module Login = {
|
|
|
95
95
|
// after a backend reload. When unset, falls back to a random per-process
|
|
96
96
|
// secret — secure for ephemeral test runs but invalidates every previously
|
|
97
97
|
// issued token on every restart.
|
|
98
|
-
type buf
|
|
99
|
-
@module("node:crypto") external _randomBytes: int => buf = "randomBytes"
|
|
100
|
-
@send external _bufToString: (buf, string) => string = "toString"
|
|
101
|
-
type hmac
|
|
102
|
-
@module("node:crypto") external _createHmac: (string, string) => hmac = "createHmac"
|
|
103
|
-
@send external _update: (hmac, string) => hmac = "update"
|
|
104
|
-
@send external _digest: (hmac, string) => string = "digest"
|
|
105
|
-
|
|
106
|
-
@val external _processEnv: Dict.t<string> = "process.env"
|
|
107
98
|
|
|
108
99
|
let _secret: ref<option<string>> = ref(None)
|
|
109
100
|
let _getSecret = (): string =>
|
|
110
101
|
switch _secret.contents {
|
|
111
102
|
| Some(s) => s
|
|
112
103
|
| None =>
|
|
113
|
-
let s = switch
|
|
104
|
+
let s = switch NodeProcess.env->Dict.get("REVENTLESS_INMEMORY_TOKEN_SECRET") {
|
|
114
105
|
| Some(envSecret) if String.length(envSecret) >= 16 => envSecret
|
|
115
|
-
| _ =>
|
|
106
|
+
| _ => NodeCrypto.randomBytes(32)->NodeCrypto.bufferToString("hex")
|
|
116
107
|
}
|
|
117
108
|
_secret := Some(s)
|
|
118
109
|
s
|
|
@@ -121,7 +112,9 @@ module Login = {
|
|
|
121
112
|
let setTokenSecret = (s: string): unit => _secret := Some(s)
|
|
122
113
|
|
|
123
114
|
let _sign = (payload: string): string =>
|
|
124
|
-
|
|
115
|
+
NodeCrypto.createHmac("sha256", _getSecret())
|
|
116
|
+
->NodeCrypto.hmacUpdate(payload)
|
|
117
|
+
->NodeCrypto.hmacDigest("base64url")
|
|
125
118
|
|
|
126
119
|
// base64url over arbitrary JSON strings: btoa-encode + URL-safe alphabet,
|
|
127
120
|
// strip `=` padding. Avoids pulling in Buffer for a single-line conversion.
|
|
@@ -26,10 +26,6 @@ type entry = {
|
|
|
26
26
|
// ── YAML parsing (eemeli/yaml) ──────────────────────────────────────────────
|
|
27
27
|
|
|
28
28
|
@module("yaml") external _yamlParse: string => unknown = "parse"
|
|
29
|
-
@module("node:fs") external _readFileSync: (string, string) => string = "readFileSync"
|
|
30
|
-
@module("node:fs") external _existsSync: string => bool = "existsSync"
|
|
31
|
-
@module("node:path") external _join: (string, string) => string = "join"
|
|
32
|
-
@val external _processCwd: unit => string = "process.cwd"
|
|
33
29
|
|
|
34
30
|
let _entriesSchema = S.array(entrySchema)
|
|
35
31
|
|
|
@@ -46,7 +42,7 @@ let parseString = (yamlText: string): result<array<entry>, string> =>
|
|
|
46
42
|
/** Reads + parses a YAML file. */
|
|
47
43
|
let parseFile = (path: string): result<array<entry>, string> =>
|
|
48
44
|
try {
|
|
49
|
-
let text =
|
|
45
|
+
let text = NodeFs.readFileSync(path)
|
|
50
46
|
parseString(text)
|
|
51
47
|
} catch {
|
|
52
48
|
| JsExn(err) => Error(JsExn.message(err)->Option.getOr(`Cannot read ${path}`))
|
|
@@ -66,7 +62,7 @@ let _registerEntries = (entries: array<entry>): unit =>
|
|
|
66
62
|
LocalAuth.Login.setCredentials(~username, ~password, ~identity=id)
|
|
67
63
|
})
|
|
68
64
|
|
|
69
|
-
let _defaultPath = (): string =>
|
|
65
|
+
let _defaultPath = (): string => NodePath.join([NodeProcess.cwd(), ".reventless/users.yaml"])
|
|
70
66
|
|
|
71
67
|
/**
|
|
72
68
|
* Hydrates the Login store. Returns the resolution path actually used
|
|
@@ -106,7 +102,7 @@ let load = (
|
|
|
106
102
|
})
|
|
107
103
|
| None =>
|
|
108
104
|
let defaultPath = _defaultPath()
|
|
109
|
-
if
|
|
105
|
+
if NodeFs.existsSync(defaultPath) {
|
|
110
106
|
parseFile(defaultPath)->Result.map(entries => {
|
|
111
107
|
_registerEntries(entries)
|
|
112
108
|
DefaultFile(defaultPath)
|
|
@@ -106,7 +106,7 @@ function load(users, usersFile, param) {
|
|
|
106
106
|
};
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
|
-
let defaultPath =
|
|
109
|
+
let defaultPath = _defaultPath();
|
|
110
110
|
if (Nodefs.existsSync(defaultPath)) {
|
|
111
111
|
return Stdlib_Result.map(parseFile(defaultPath), entries => {
|
|
112
112
|
_registerEntries(entries);
|
package/src/adapter/Backend.res
CHANGED
|
@@ -34,13 +34,11 @@ let postgres = async (~connection, ~resetOnStart=false) => {
|
|
|
34
34
|
Postgres({pool, initialCount, connection})
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
@val external processEnv: dict<string> = "process.env"
|
|
38
37
|
|
|
39
38
|
// Deletes a file if it exists. No-op if the path is missing.
|
|
40
|
-
@module("node:fs") external _unlinkSync: string => unit = "unlinkSync"
|
|
41
39
|
|
|
42
40
|
let removeFileIfExists = (path: string) =>
|
|
43
|
-
try
|
|
41
|
+
try NodeFs.unlinkSync(path) catch {
|
|
44
42
|
| _ => ()
|
|
45
43
|
}
|
|
46
44
|
|
|
@@ -50,7 +48,7 @@ let removeFileIfExists = (path: string) =>
|
|
|
50
48
|
// memory — explicit Memory selector
|
|
51
49
|
// Anything else (including unset) yields Memory.
|
|
52
50
|
let fromEnv = () =>
|
|
53
|
-
switch
|
|
51
|
+
switch NodeProcess.env->Dict.get("REVENTLESS_LOCAL_BACKEND") {
|
|
54
52
|
| None => Memory
|
|
55
53
|
| Some(raw) =>
|
|
56
54
|
let trimmed = raw->String.trim
|
|
@@ -37,7 +37,6 @@ type nodeResponse
|
|
|
37
37
|
// mangled by utf8 decoding (setEncoding is deliberately NOT called).
|
|
38
38
|
@send external _onDataBuf: (nodeRequest, @as("data") _, LocalObjectStore.buffer => unit) => unit = "on"
|
|
39
39
|
|
|
40
|
-
@module("crypto") external randomUUID: unit => string = "randomUUID"
|
|
41
40
|
|
|
42
41
|
let readBody = (req: nodeRequest, onBody: string => unit): unit => {
|
|
43
42
|
let buf = ref("")
|
|
@@ -155,7 +154,7 @@ let handleUploadPresign = (req: nodeRequest, res: nodeResponse): unit =>
|
|
|
155
154
|
}
|
|
156
155
|
let fileName =
|
|
157
156
|
parsed->Dict.get("fileName")->Option.flatMap(JSON.Decode.string)->Option.getOr("upload")
|
|
158
|
-
let ref = `/${LocalObjectStore.defaultUploadPrefix}/${randomUUID()}/${fileName}`
|
|
157
|
+
let ref = `/${LocalObjectStore.defaultUploadPrefix}/${NodeCrypto.randomUUID()}/${fileName}`
|
|
159
158
|
_writeJson(
|
|
160
159
|
res,
|
|
161
160
|
~status=200,
|
|
@@ -496,8 +495,7 @@ let buildNodeResolver = (): option<(string, resolverFn)> =>
|
|
|
496
495
|
|
|
497
496
|
// -- Server lifecycle ------------------------------------------------------
|
|
498
497
|
|
|
499
|
-
|
|
500
|
-
let debug = processEnv->Dict.get("GRAPHQL_DEBUG")->Option.isSome
|
|
498
|
+
let debug = NodeProcess.env->Dict.get("GRAPHQL_DEBUG")->Option.isSome
|
|
501
499
|
|
|
502
500
|
let activeServer: ref<option<YG.httpServer>> = ref(None)
|
|
503
501
|
let activeSchema: ref<option<YG.schema>> = ref(None)
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
4
|
import * as Http from "http";
|
|
5
|
-
import * as Crypto from "crypto";
|
|
6
5
|
import * as Graphql from "graphql";
|
|
7
6
|
import * as GraphqlYoga from "@reventlessdev/rescript-graphql-yoga/src/GraphqlYoga.res.mjs";
|
|
8
7
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
9
8
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
9
|
+
import * as Nodecrypto from "node:crypto";
|
|
10
10
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
11
11
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
12
12
|
import * as GraphqlYoga$1 from "graphql-yoga";
|
|
@@ -117,7 +117,7 @@ function handleUploadPresign(req, res) {
|
|
|
117
117
|
parsed = {};
|
|
118
118
|
}
|
|
119
119
|
let fileName = Stdlib_Option.getOr(Stdlib_Option.flatMap(parsed["fileName"], Stdlib_JSON.Decode.string), "upload");
|
|
120
|
-
let ref = `/` + LocalObjectStore$ReventlessLocal.defaultUploadPrefix + `/` +
|
|
120
|
+
let ref = `/` + LocalObjectStore$ReventlessLocal.defaultUploadPrefix + `/` + Nodecrypto.randomUUID() + `/` + fileName;
|
|
121
121
|
_writeJson(res, 200, Object.fromEntries([
|
|
122
122
|
[
|
|
123
123
|
"uploadUrl",
|
|
@@ -132,8 +132,7 @@ let registerEventHistoryResourcesFromEntries = (
|
|
|
132
132
|
|
|
133
133
|
let activeServer: ref<option<McpSdk.httpServer>> = ref(None)
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
let debug = processEnv->Dict.get("MCP_DEBUG")->Option.isSome
|
|
135
|
+
let debug = NodeProcess.env->Dict.get("MCP_DEBUG")->Option.isSome
|
|
137
136
|
|
|
138
137
|
// Extract identity from an HTTP IncomingMessage via the X-Identity header.
|
|
139
138
|
// Falls back to Identity.anonymous when the header is absent or malformed.
|
package/src/adapter/LocalBus.res
CHANGED
|
@@ -98,10 +98,9 @@ let makeStateChangeDescriptor = (
|
|
|
98
98
|
// so the runner's line parser can pick it out of the platform's ANSI log noise on
|
|
99
99
|
// stdout. The emit is a plain Console.log, so it never perturbs publishEvent's
|
|
100
100
|
// subscriber-countdown delivery semantics.
|
|
101
|
-
@val external tapProcessEnv: dict<string> = "process.env"
|
|
102
101
|
// Read per publish (not once at import) so the runner can toggle it live and so
|
|
103
102
|
// hermetic tests can flip it between cases. The cost is one dict lookup per event.
|
|
104
|
-
let eventTapEnabled = () =>
|
|
103
|
+
let eventTapEnabled = () => NodeProcess.env->Dict.get("REVENTLESS_EVENT_TAP")->Option.isSome
|
|
105
104
|
let eventTapSeq = ref(0)
|
|
106
105
|
// Seed the in-memory tap counter from a persistent store's existing event count
|
|
107
106
|
// so the timeline's #N continues across process restarts / app switches instead
|
|
@@ -51,8 +51,7 @@ type t = {
|
|
|
51
51
|
printDiagnostics: unit => unit,
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
let debug = processEnv->Dict.get("MCP_DEBUG")->Option.isSome
|
|
54
|
+
let debug = NodeProcess.env->Dict.get("MCP_DEBUG")->Option.isSome
|
|
56
55
|
|
|
57
56
|
let make = (~label: string="MCP"): t => {
|
|
58
57
|
let tools: ref<dict<registeredTool>> = ref(Dict.make())
|
|
@@ -7,10 +7,6 @@ type statement
|
|
|
7
7
|
@module("node:sqlite") @new
|
|
8
8
|
external openDatabaseSync: string => t = "DatabaseSync"
|
|
9
9
|
|
|
10
|
-
@module("node:path") external dirname: string => string = "dirname"
|
|
11
|
-
type mkdirOpts = {recursive: bool}
|
|
12
|
-
@module("node:fs") external mkdirSync: (string, mkdirOpts) => unit = "mkdirSync"
|
|
13
|
-
|
|
14
10
|
@send external _close: t => unit = "close"
|
|
15
11
|
@send external _exec: (t, string) => unit = "exec"
|
|
16
12
|
@send external _prepare: (t, string) => statement = "prepare"
|
|
@@ -25,7 +21,7 @@ let _iterateApply: (statement, array<JSON.t>) => Iterator.t<dict<JSON.t>> = %raw
|
|
|
25
21
|
|
|
26
22
|
let openDb = (~path) => {
|
|
27
23
|
if path !== ":memory:" {
|
|
28
|
-
mkdirSync(dirname(path), {recursive: true})
|
|
24
|
+
NodeFs.mkdirSync(NodePath.dirname(path), {recursive: true})
|
|
29
25
|
}
|
|
30
26
|
let db = openDatabaseSync(path)
|
|
31
27
|
// Connection pragmas. WAL lets readers proceed concurrently with a writer and
|
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
open JestGlobals
|
|
12
12
|
|
|
13
|
-
@val external processEnv: dict<string> = "process.env"
|
|
14
13
|
|
|
15
14
|
let _ = TestRunner.setup()
|
|
16
15
|
let opts: Pulumi.CustomResourceOptions.t = {}
|
|
17
16
|
|
|
18
17
|
let pgPool =
|
|
19
|
-
|
|
18
|
+
NodeProcess.env
|
|
20
19
|
->Dict.get("PG_URL")
|
|
21
20
|
->Option.map(url => ReventlessPostgres.PgDriver.makePool({connectionString: url}))
|
|
22
21
|
|
|
@@ -17,7 +17,6 @@ let defaultMeta: Reventless.Message.meta = {
|
|
|
17
17
|
correlationId: "",
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
@val external processEnv: Dict.t<string> = "process.env"
|
|
21
20
|
|
|
22
21
|
type consoleObj = {mutable log: string => unit}
|
|
23
22
|
@val external console: consoleObj = "console"
|
|
@@ -48,7 +47,7 @@ let tapLines = (logs: array<string>): array<string> =>
|
|
|
48
47
|
|
|
49
48
|
describe("LocalBus event tap (Phase 9)", () => {
|
|
50
49
|
// Each test owns the env flag; always clear it afterwards so cases don't leak.
|
|
51
|
-
let clearEnv = () =>
|
|
50
|
+
let clearEnv = () => NodeProcess.env->Dict.delete("REVENTLESS_EVENT_TAP")
|
|
52
51
|
|
|
53
52
|
testPromise("tap off → no sentinel line is emitted", async () => {
|
|
54
53
|
clearEnv()
|
|
@@ -60,7 +59,7 @@ describe("LocalBus event tap (Phase 9)", () => {
|
|
|
60
59
|
})
|
|
61
60
|
|
|
62
61
|
testPromise("tap on → one well-formed line per published event", async () => {
|
|
63
|
-
|
|
62
|
+
NodeProcess.env->Dict.set("REVENTLESS_EVENT_TAP", "ndjson")
|
|
64
63
|
module TestBus = LocalBus.MakeSilent()
|
|
65
64
|
let logs = await captureLogs(async () => {
|
|
66
65
|
await TestBus.publishEvent(
|
|
@@ -90,7 +89,7 @@ describe("LocalBus event tap (Phase 9)", () => {
|
|
|
90
89
|
})
|
|
91
90
|
|
|
92
91
|
testPromise("tap emits even when the topic has no subscribers", async () => {
|
|
93
|
-
|
|
92
|
+
NodeProcess.env->Dict.set("REVENTLESS_EVENT_TAP", "ndjson")
|
|
94
93
|
module TestBus = LocalBus.MakeSilent()
|
|
95
94
|
let logs = await captureLogs(async () => {
|
|
96
95
|
// No subscribeToEvents call — the tap fires before the subscriber-count gate.
|
|
@@ -4,11 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
open JestGlobals
|
|
6
6
|
|
|
7
|
-
@module("node:fs") external _writeFileSync: (string, string, string) => unit = "writeFileSync"
|
|
8
|
-
@module("node:fs") external _mkdtempSync: string => string = "mkdtempSync"
|
|
9
|
-
@module("node:fs") external _rmSync: (string, {..}) => unit = "rmSync"
|
|
10
|
-
@module("node:path") external _join: (string, string) => string = "join"
|
|
11
|
-
@module("node:os") external _tmpdir: unit => string = "tmpdir"
|
|
12
7
|
|
|
13
8
|
let resetAll = () => {
|
|
14
9
|
LocalAuth.resetUsers()
|
|
@@ -17,9 +12,9 @@ let resetAll = () => {
|
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
let _writeYamlFile = (contents: string): string => {
|
|
20
|
-
let dir =
|
|
21
|
-
let path =
|
|
22
|
-
|
|
15
|
+
let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-userstore-"]))
|
|
16
|
+
let path = NodePath.join([dir, "users.yaml"])
|
|
17
|
+
NodeFs.writeFileSync(path, contents)
|
|
23
18
|
path
|
|
24
19
|
}
|
|
25
20
|
|
|
@@ -102,7 +97,7 @@ testPromise("load(~usersFile) reads a YAML file from disk", async () => {
|
|
|
102
97
|
| Ok(_) => ()
|
|
103
98
|
| Error(msg) => JsError.throwWithMessage("issue rejected file-loaded credentials: " ++ msg)
|
|
104
99
|
}
|
|
105
|
-
|
|
100
|
+
NodeFs.rmSync(path, {force: true})
|
|
106
101
|
})
|
|
107
102
|
|
|
108
103
|
testPromise("load(~usersFile) returns Error when the file is missing", async () => {
|
|
@@ -152,7 +147,7 @@ testPromise("inline users win over a file path", async () => {
|
|
|
152
147
|
| Some(_) => ()
|
|
153
148
|
| None => JsError.throwWithMessage("inline-user missing from registry")
|
|
154
149
|
}
|
|
155
|
-
|
|
150
|
+
NodeFs.rmSync(path, {force: true})
|
|
156
151
|
})
|
|
157
152
|
|
|
158
153
|
testPromise("autoLoadOnce is a no-op after an explicit load", async () => {
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
open JestGlobals
|
|
12
12
|
|
|
13
|
-
@val external processEnv: dict<string> = "process.env"
|
|
14
13
|
let opts: Pulumi.CustomResourceOptions.t = {}
|
|
15
14
|
|
|
16
15
|
// Flat stored aggregate event — the exact shape EventLog_Operations.encodeEvent'
|
|
@@ -58,7 +57,7 @@ let recorder = () => {
|
|
|
58
57
|
(received, handler)
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
switch
|
|
60
|
+
switch NodeProcess.env->Dict.get("PG_URL") {
|
|
62
61
|
| None => testSync("Postgres projection catch-up (skipped — set PG_URL)", () => expect(true)->toBe(true))
|
|
63
62
|
| Some(url) =>
|
|
64
63
|
let pool = ReventlessPostgres.PgDriver.makePool({connectionString: url})
|
|
@@ -64,23 +64,23 @@ describe("SqliteDriver", () => {
|
|
|
64
64
|
|
|
65
65
|
testPromise("Backend.fromEnv parses sqlite path and reset flag", async () => {
|
|
66
66
|
let originalEnv =
|
|
67
|
-
|
|
67
|
+
NodeProcess.env->Dict.get("REVENTLESS_LOCAL_BACKEND")
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
NodeProcess.env->Dict.set("REVENTLESS_LOCAL_BACKEND", "sqlite:./local.db")
|
|
70
70
|
let parsed = Backend.fromEnv()
|
|
71
71
|
expect(parsed)->toEqual(Backend.Sqlite({path: "./local.db", resetOnStart: false}))
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
NodeProcess.env->Dict.set("REVENTLESS_LOCAL_BACKEND", "sqlite:./local.db?reset")
|
|
74
74
|
let parsedReset = Backend.fromEnv()
|
|
75
75
|
expect(parsedReset)->toEqual(Backend.Sqlite({path: "./local.db", resetOnStart: true}))
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
NodeProcess.env->Dict.set("REVENTLESS_LOCAL_BACKEND", "memory")
|
|
78
78
|
let parsedMem = Backend.fromEnv()
|
|
79
79
|
expect(parsedMem)->toEqual(Backend.Memory)
|
|
80
80
|
|
|
81
81
|
switch originalEnv {
|
|
82
|
-
| Some(v) =>
|
|
83
|
-
| None =>
|
|
82
|
+
| Some(v) => NodeProcess.env->Dict.set("REVENTLESS_LOCAL_BACKEND", v)
|
|
83
|
+
| None => NodeProcess.env->Dict.delete("REVENTLESS_LOCAL_BACKEND")
|
|
84
84
|
}
|
|
85
85
|
})
|
|
86
86
|
})
|