@reventlessdev/reventless-local 3.0.0-alpha.318 → 3.0.0-alpha.320
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 +19 -0
- package/package.json +11 -11
- package/src/LocalPlatformRegistry.res +7 -1
- package/src/LocalPlatformRegistry.res.mjs +7 -5
- package/src/Platform.res +16 -0
- package/src/Platform.res.mjs +16 -2
- package/src/adapter/Api/LocalEvents_Server.res +34 -3
- package/src/adapter/Api/LocalEvents_Server.res.mjs +31 -3
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res +24 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res.mjs +28 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_InMemory.res +26 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_InMemory.res.mjs +35 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res +61 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res.mjs +75 -0
- package/src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res +88 -0
- package/src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res.mjs +112 -0
- package/src/adapter/DomainGraphQL_Server.res +2 -1
- package/src/adapter/DomainGraphQL_Server.res.mjs +3 -1
- package/src/adapter/LocalFeed.res +16 -2
- package/src/adapter/LocalFeed.res.mjs +19 -2
- package/src/adapter/LocalIdleExit.res +71 -0
- package/src/adapter/LocalIdleExit.res.mjs +96 -0
- package/src/adapter/LocalMetrics.res +29 -0
- package/src/adapter/LocalMetrics.res.mjs +61 -0
- package/src/adapter/LocalPlatformObserve.res +129 -5
- package/src/adapter/LocalPlatformObserve.res.mjs +144 -2
- package/src/adapter/LocalRequestLog.res +62 -0
- package/src/adapter/LocalRequestLog.res.mjs +80 -0
- package/src/adapter/LocalTapLines.res +2 -0
- package/src/adapter/LocalTapLines.res.mjs +4 -1
- package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +11 -3
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +10 -3
- package/src/adapter/ViewChangeLog/LocalViewChangeLog.res +95 -0
- package/src/adapter/ViewChangeLog/LocalViewChangeLog.res.mjs +116 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogEntry.res +33 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogEntry.res.mjs +30 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_InMemory.res +28 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_InMemory.res.mjs +40 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_Sqlite.res +76 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_Sqlite.res.mjs +88 -0
- package/src/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/LocalAutomationSchedules.res.mjs +1 -1
- package/src/components/LocalAutomationSweep.res.mjs +1 -1
- package/tests/LocalPlatformRegistryTest.res +17 -0
- package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
- package/tests/LocalPlatformStartTest.res.mjs +1 -1
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +3 -2
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +1 -1
- package/tests/adapter/LocalEvents_ServerTest.res +11 -0
- package/tests/adapter/LocalEvents_ServerTest.res.mjs +7 -1
- package/tests/adapter/LocalIdleExitTest.res +57 -0
- package/tests/adapter/LocalIdleExitTest.res.mjs +62 -0
- package/tests/adapter/LocalPlatformObserveTest.res +183 -0
- package/tests/adapter/LocalPlatformObserveTest.res.mjs +181 -0
- package/tests/adapter/LocalRequestLogTest.res +46 -0
- package/tests/adapter/LocalRequestLogTest.res.mjs +61 -0
- package/tests/adapter/LocalTapLinesTest.res +25 -0
- package/tests/adapter/LocalTapLinesTest.res.mjs +35 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
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.320 (2026-11-02)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* a view row knows its last change, and the local platform keeps every change ([1a1fd71](https://github.com/ReventlessDev/reventless-core/commit/1a1fd7117d9837696a7c0080dcd6f0983e57780c))
|
|
11
|
+
* **local:** a platform left running for an editor stops once nothing watches it ([1ccff67](https://github.com/ReventlessDev/reventless-core/commit/1ccff678126053dbce88ee002c6800af1333cac6))
|
|
12
|
+
* **local:** the app's API logs each request and subscription it answers ([31a163c](https://github.com/ReventlessDev/reventless-core/commit/31a163c945d8929e9a604cc0dc8a74939320f85e))
|
|
13
|
+
* **local:** the local platform counts its metrics instead of logging them ([76c6094](https://github.com/ReventlessDev/reventless-core/commit/76c60946fa74520519dccb4ae39fae5880817832))
|
|
14
|
+
* **local:** the local platform keeps every command outcome ([252ed2e](https://github.com/ReventlessDev/reventless-core/commit/252ed2ea46f2634b86851aea402e94af7c371f10))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# 3.0.0-alpha.319 (2026-11-01)
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* **core:** a component's log line carries the ids of the message it is about ([9c26267](https://github.com/ReventlessDev/reventless-core/commit/9c26267ca6d03ff66b8406caa32243e205e29b5a))
|
|
22
|
+
* **local:** the registry entry names the platform API, and a command's outcome names its cause ([51717c4](https://github.com/ReventlessDev/reventless-core/commit/51717c441820c19dc59e595559b037b808ca716a))
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
# 3.0.0-alpha.318 (2026-11-01)
|
|
7
26
|
|
|
8
27
|
### 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.320",
|
|
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,19 +34,19 @@
|
|
|
34
34
|
"graphql-yoga": "^5.21.0",
|
|
35
35
|
"sury": "11.0.0",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
|
-
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.
|
|
38
|
-
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
37
|
+
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.31",
|
|
39
38
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
40
39
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
|
|
41
|
-
"@reventlessdev/rescript-
|
|
40
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
41
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.26",
|
|
42
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.328",
|
|
43
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.176",
|
|
44
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.277",
|
|
42
45
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.174",
|
|
48
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.192",
|
|
49
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.190"
|
|
46
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.222",
|
|
47
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.41",
|
|
48
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.193",
|
|
49
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.192"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.1",
|
|
@@ -30,6 +30,9 @@ type entry = {
|
|
|
30
30
|
// Where the log feed is served, when it is: the platform's recent and live log
|
|
31
31
|
// records. Optional for the same reason as `tapPort`.
|
|
32
32
|
logPort?: int,
|
|
33
|
+
// Where the platform API is served: its own port when the platform serves two
|
|
34
|
+
// APIs, else the same endpoint as `endpoint`. Optional for the same reason.
|
|
35
|
+
platformEndpoint?: string,
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
// Threaded rather than read at each use, so tests need not move the process.
|
|
@@ -115,6 +118,7 @@ let write = (
|
|
|
115
118
|
~loginEndpoint: string,
|
|
116
119
|
~store: store,
|
|
117
120
|
~tapPort: option<int>=?,
|
|
121
|
+
~platformEndpoint: option<string>=?,
|
|
118
122
|
~pid: int=NodeProcess.pid,
|
|
119
123
|
~cwd: string=NodeProcess.cwd(),
|
|
120
124
|
): string => {
|
|
@@ -128,6 +132,7 @@ let write = (
|
|
|
128
132
|
store,
|
|
129
133
|
startedAt: Date.make()->Date.toISOString,
|
|
130
134
|
?tapPort,
|
|
135
|
+
?platformEndpoint,
|
|
131
136
|
}
|
|
132
137
|
try {
|
|
133
138
|
NodeFs.mkdirSync(runningDir(~cwd, ()), {recursive: true})
|
|
@@ -187,8 +192,9 @@ let register = (
|
|
|
187
192
|
~loginEndpoint: string,
|
|
188
193
|
~store: store,
|
|
189
194
|
~tapPort: option<int>=?,
|
|
195
|
+
~platformEndpoint: option<string>=?,
|
|
190
196
|
): unit => {
|
|
191
|
-
let path = write(~port, ~endpoint, ~loginEndpoint, ~store, ~tapPort?)
|
|
197
|
+
let path = write(~port, ~endpoint, ~loginEndpoint, ~store, ~tapPort?, ~platformEndpoint?)
|
|
192
198
|
NodeProcess.onExit(_ => removeQuietly(path))
|
|
193
199
|
NodeProcess.onSignal(SIGINT, () => {
|
|
194
200
|
removeQuietly(path)
|
|
@@ -23,7 +23,8 @@ let entrySchema = Sury.$schema(s => ({
|
|
|
23
23
|
store: s.m(storeSchema),
|
|
24
24
|
startedAt: s.m(Sury.string),
|
|
25
25
|
tapPort: s.m(Sury.$option(Sury.int32)),
|
|
26
|
-
logPort: s.m(Sury.$option(Sury.int32))
|
|
26
|
+
logPort: s.m(Sury.$option(Sury.int32)),
|
|
27
|
+
platformEndpoint: s.m(Sury.$option(Sury.string))
|
|
27
28
|
}));
|
|
28
29
|
|
|
29
30
|
function runningDir(cwdOpt, param) {
|
|
@@ -110,7 +111,7 @@ function list(cwdOpt, param) {
|
|
|
110
111
|
}).toSorted((a, b) => Primitive_int.compare(a.port, b.port));
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
function write(port, endpoint, loginEndpoint, store, tapPort, pidOpt, cwdOpt) {
|
|
114
|
+
function write(port, endpoint, loginEndpoint, store, tapPort, platformEndpoint, pidOpt, cwdOpt) {
|
|
114
115
|
let pid = pidOpt !== undefined ? pidOpt : process.pid;
|
|
115
116
|
let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
|
|
116
117
|
let path = entryPath(port, cwd);
|
|
@@ -124,7 +125,8 @@ function write(port, endpoint, loginEndpoint, store, tapPort, pidOpt, cwdOpt) {
|
|
|
124
125
|
loginEndpoint: loginEndpoint,
|
|
125
126
|
store: store,
|
|
126
127
|
startedAt: entry_startedAt,
|
|
127
|
-
tapPort: tapPort
|
|
128
|
+
tapPort: tapPort,
|
|
129
|
+
platformEndpoint: platformEndpoint
|
|
128
130
|
};
|
|
129
131
|
try {
|
|
130
132
|
Nodefs.mkdirSync(runningDir(cwd, undefined), {
|
|
@@ -169,8 +171,8 @@ function publishLogPort(port, logPort, cwdOpt) {
|
|
|
169
171
|
}
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
function register(port, endpoint, loginEndpoint, store, tapPort) {
|
|
173
|
-
let path = write(port, endpoint, loginEndpoint, store, tapPort, undefined, undefined);
|
|
174
|
+
function register(port, endpoint, loginEndpoint, store, tapPort, platformEndpoint) {
|
|
175
|
+
let path = write(port, endpoint, loginEndpoint, store, tapPort, platformEndpoint, undefined, undefined);
|
|
174
176
|
process.on("exit", param => removeQuietly(path));
|
|
175
177
|
process.on("SIGINT", () => {
|
|
176
178
|
removeQuietly(path);
|
package/src/Platform.res
CHANGED
|
@@ -23,6 +23,10 @@ ReventlessCore.EffectLogger.setDefaultMinLevel(ReventlessCore.Logger.Debug)
|
|
|
23
23
|
// REVENTLESS_LOG_FORMAT still wins.
|
|
24
24
|
Reventless.AnsiStyle.setDefaultFormat(#text)
|
|
25
25
|
|
|
26
|
+
// Locally nothing collects metric lines, so metrics are counted and served on the
|
|
27
|
+
// platform API instead of written among the log lines.
|
|
28
|
+
LocalMetrics.install()
|
|
29
|
+
|
|
26
30
|
let log = ReventlessCore.Logger.fromEnv()
|
|
27
31
|
|
|
28
32
|
// Module-level ref to hold the platform GraphQL server instance in split mode.
|
|
@@ -112,11 +116,18 @@ module MakeWithConfig = (
|
|
|
112
116
|
// What became of each command, and each row a projection changed, on the
|
|
113
117
|
// event tap; a rejected command also in the problem ring.
|
|
114
118
|
LocalTapLines.install()
|
|
119
|
+
// Every row change, kept so a tool that missed one live can read it back.
|
|
120
|
+
LocalViewChangeLog.install()
|
|
121
|
+
// And what became of every command, for the same reason.
|
|
122
|
+
LocalCommandOutcomeLog.install()
|
|
115
123
|
|
|
116
124
|
// What this platform provides for the roles its plugins need: every account it
|
|
117
125
|
// can sign in as, built-in and loaded alike, or the file the store will load
|
|
118
126
|
// when the servers start — after the plugins are built and checked.
|
|
119
127
|
Reventless.Role.provideGroupsFrom(AccountStore.providedGroups)
|
|
128
|
+
// The administrator reads across every owner, as a deployment's does: the same default the
|
|
129
|
+
// AWS platform sets, and like it never a forcing over an explicit `setElevatedGroups`.
|
|
130
|
+
Reventless.OwnerScope.defaultElevatedRoles([Reventless.Role.admin])
|
|
120
131
|
// Activate Pulumi mock mode — must happen before any component creation.
|
|
121
132
|
// Idempotent, so safe to call even if TestRunner.setup() was already called.
|
|
122
133
|
let _ = TestRunner.setup()
|
|
@@ -1422,7 +1433,12 @@ module MakeWithConfig = (
|
|
|
1422
1433
|
~endpoint=`http://localhost:${domainPort->Int.toString}/graphql`,
|
|
1423
1434
|
~loginEndpoint=`http://localhost:${domainPort->Int.toString}/__inmemory/login`,
|
|
1424
1435
|
~store={kind, ?path},
|
|
1436
|
+
~platformEndpoint=Config.splitApi
|
|
1437
|
+
? `http://localhost:${platformPort->Int.toString}/graphql`
|
|
1438
|
+
: `http://localhost:${domainPort->Int.toString}/graphql`,
|
|
1425
1439
|
)
|
|
1440
|
+
// A platform left running for an editor stops once nothing has watched it for a while.
|
|
1441
|
+
LocalIdleExit.start(~env=NodeProcess.env)
|
|
1426
1442
|
// After the register, and it fills the tap port in once the socket is bound:
|
|
1427
1443
|
// the port is ephemeral by default, so it is not known until `listen` calls
|
|
1428
1444
|
// back. The entry therefore never names a port that is not listening.
|
package/src/Platform.res.mjs
CHANGED
|
@@ -39,11 +39,13 @@ import * as AccountStore$ReventlessLocal from "./adapter/Auth/AccountStore.res.m
|
|
|
39
39
|
import * as BackendState$ReventlessLocal from "./adapter/BackendState.res.mjs";
|
|
40
40
|
import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/src/ComponentType.res.mjs";
|
|
41
41
|
import * as LocalLogFeed$ReventlessLocal from "./adapter/LocalLogFeed.res.mjs";
|
|
42
|
+
import * as LocalMetrics$ReventlessLocal from "./adapter/LocalMetrics.res.mjs";
|
|
42
43
|
import * as SqliteDriver$ReventlessLocal from "./adapter/SqliteDriver.res.mjs";
|
|
43
44
|
import * as Task_Builder$ReventlessLocal from "./components/Task_Builder.res.mjs";
|
|
44
45
|
import * as BakedManifest$ReventlessLocal from "./BakedManifest.res.mjs";
|
|
45
46
|
import * as HostShellDist$ReventlessLocal from "./HostShellDist.res.mjs";
|
|
46
47
|
import * as LocalEventTap$ReventlessLocal from "./adapter/LocalEventTap.res.mjs";
|
|
48
|
+
import * as LocalIdleExit$ReventlessLocal from "./adapter/LocalIdleExit.res.mjs";
|
|
47
49
|
import * as LocalTapLines$ReventlessLocal from "./adapter/LocalTapLines.res.mjs";
|
|
48
50
|
import * as Platform_Admin$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin.res.mjs";
|
|
49
51
|
import * as PluginDecision$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginDecision.res.mjs";
|
|
@@ -72,6 +74,7 @@ import * as ReadModel_Builder$ReventlessLocal from "./components/ReadModel_Build
|
|
|
72
74
|
import * as UiFragmentRegistry$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res.mjs";
|
|
73
75
|
import * as LocalEvents_Server$ReventlessLocal from "./adapter/Api/LocalEvents_Server.res.mjs";
|
|
74
76
|
import * as LocalPlatformStart$ReventlessLocal from "./LocalPlatformStart.res.mjs";
|
|
77
|
+
import * as LocalViewChangeLog$ReventlessLocal from "./adapter/ViewChangeLog/LocalViewChangeLog.res.mjs";
|
|
75
78
|
import * as PlatformMCP_Server$ReventlessLocal from "./adapter/PlatformMCP_Server.res.mjs";
|
|
76
79
|
import * as ResourceAttribution$ReventlessCore from "@reventlessdev/reventless-core/src/ResourceAttribution.res.mjs";
|
|
77
80
|
import * as Auth_GraphqlContext$ReventlessLocal from "./adapter/Auth/Auth_GraphqlContext.res.mjs";
|
|
@@ -89,6 +92,7 @@ import * as LocalPlatformRegistry$ReventlessLocal from "./LocalPlatformRegistry.
|
|
|
89
92
|
import * as UiFragments_Projection$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments_Projection.res.mjs";
|
|
90
93
|
import * as EventLogStorage_Sqlite$ReventlessLocal from "./adapter/EventLog/EventLogStorage_Sqlite.res.mjs";
|
|
91
94
|
import * as ExtensionPoint_Builder$ReventlessLocal from "./components/ExtensionPoint_Builder.res.mjs";
|
|
95
|
+
import * as LocalCommandOutcomeLog$ReventlessLocal from "./adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res.mjs";
|
|
92
96
|
import * as PlatformGraphQL_Server$ReventlessLocal from "./adapter/PlatformGraphQL_Server.res.mjs";
|
|
93
97
|
import * as Platform_UIFragmentsApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UIFragmentsApi.res.mjs";
|
|
94
98
|
import * as StateViewSlice_Builder$ReventlessLocal from "./components/StateViewSlice_Builder.res.mjs";
|
|
@@ -119,6 +123,8 @@ EffectLogger$ReventlessCore.setDefaultMinLevel("Debug");
|
|
|
119
123
|
|
|
120
124
|
AnsiStyle$Reventless.setDefaultFormat("text");
|
|
121
125
|
|
|
126
|
+
LocalMetrics$ReventlessLocal.install();
|
|
127
|
+
|
|
122
128
|
let log = Logger$ReventlessCore.fromEnv();
|
|
123
129
|
|
|
124
130
|
let platformGraphQLRef = {
|
|
@@ -152,7 +158,10 @@ function decodeUiFragmentRegistryEventEnvelope(eventJson) {
|
|
|
152
158
|
function MakeWithConfig(Config) {
|
|
153
159
|
LocalLogFeed$ReventlessLocal.install(undefined);
|
|
154
160
|
LocalTapLines$ReventlessLocal.install();
|
|
161
|
+
LocalViewChangeLog$ReventlessLocal.install();
|
|
162
|
+
LocalCommandOutcomeLog$ReventlessLocal.install();
|
|
155
163
|
Role$Reventless.provideGroupsFrom(AccountStore$ReventlessLocal.providedGroups);
|
|
164
|
+
OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
|
|
156
165
|
TestRunner$ReventlessLocal.setup();
|
|
157
166
|
let match = Config.backend;
|
|
158
167
|
if (typeof match !== "object") {
|
|
@@ -1176,7 +1185,8 @@ function MakeWithConfig(Config) {
|
|
|
1176
1185
|
LocalPlatformRegistry$ReventlessLocal.register(domainPort, `http://localhost:` + domainPort.toString() + `/graphql`, `http://localhost:` + domainPort.toString() + `/__inmemory/login`, {
|
|
1177
1186
|
kind: match[0],
|
|
1178
1187
|
path: match[1]
|
|
1179
|
-
}, undefined);
|
|
1188
|
+
}, undefined, Config.splitApi ? `http://localhost:` + platformPort.toString() + `/graphql` : `http://localhost:` + domainPort.toString() + `/graphql`);
|
|
1189
|
+
LocalIdleExit$ReventlessLocal.start(process.env);
|
|
1180
1190
|
LocalEventTap$ReventlessLocal.start(undefined, tapPort => LocalPlatformRegistry$ReventlessLocal.publishTapPort(domainPort, tapPort, undefined), undefined);
|
|
1181
1191
|
LocalLogFeed$ReventlessLocal.start(undefined, logPort => LocalPlatformRegistry$ReventlessLocal.publishLogPort(domainPort, logPort, undefined), undefined);
|
|
1182
1192
|
Plugin_Helpers$ReventlessCore.firePlatformDeployedHook({
|
|
@@ -1960,7 +1970,10 @@ function Make($star) {
|
|
|
1960
1970
|
let backend = Backend$ReventlessLocal.fromEnv();
|
|
1961
1971
|
LocalLogFeed$ReventlessLocal.install(undefined);
|
|
1962
1972
|
LocalTapLines$ReventlessLocal.install();
|
|
1973
|
+
LocalViewChangeLog$ReventlessLocal.install();
|
|
1974
|
+
LocalCommandOutcomeLog$ReventlessLocal.install();
|
|
1963
1975
|
Role$Reventless.provideGroupsFrom(AccountStore$ReventlessLocal.providedGroups);
|
|
1976
|
+
OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
|
|
1964
1977
|
TestRunner$ReventlessLocal.setup();
|
|
1965
1978
|
if (typeof backend !== "object") {
|
|
1966
1979
|
BackendState$ReventlessLocal.setMemory();
|
|
@@ -2973,7 +2986,8 @@ function Make($star) {
|
|
|
2973
2986
|
LocalPlatformRegistry$ReventlessLocal.register(domainPort, `http://localhost:` + domainPort.toString() + `/graphql`, `http://localhost:` + domainPort.toString() + `/__inmemory/login`, {
|
|
2974
2987
|
kind: match[0],
|
|
2975
2988
|
path: match[1]
|
|
2976
|
-
}, undefined);
|
|
2989
|
+
}, undefined, `http://localhost:` + platformPort.toString() + `/graphql`);
|
|
2990
|
+
LocalIdleExit$ReventlessLocal.start(process.env);
|
|
2977
2991
|
LocalEventTap$ReventlessLocal.start(undefined, tapPort => LocalPlatformRegistry$ReventlessLocal.publishTapPort(domainPort, tapPort, undefined), undefined);
|
|
2978
2992
|
LocalLogFeed$ReventlessLocal.start(undefined, logPort => LocalPlatformRegistry$ReventlessLocal.publishLogPort(domainPort, logPort, undefined), undefined);
|
|
2979
2993
|
Plugin_Helpers$ReventlessCore.firePlatformDeployedHook({
|
|
@@ -48,14 +48,16 @@ type connection = {
|
|
|
48
48
|
send: string => unit,
|
|
49
49
|
/** subscription id → channel (exact, or ending in a `*` wildcard). */
|
|
50
50
|
subscriptions: dict<string>,
|
|
51
|
+
/** Who connected, when a token said. */
|
|
52
|
+
user?: string,
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
let connections: ref<array<connection>> = ref([])
|
|
54
56
|
let nextConnectionId = ref(0)
|
|
55
57
|
|
|
56
|
-
let addConnection = (~send: string => unit): connection => {
|
|
58
|
+
let addConnection = (~send: string => unit, ~user: option<string>=?): connection => {
|
|
57
59
|
nextConnectionId.contents = nextConnectionId.contents + 1
|
|
58
|
-
let conn = {id: nextConnectionId.contents, send, subscriptions: Dict.make()}
|
|
60
|
+
let conn = {id: nextConnectionId.contents, send, subscriptions: Dict.make(), ?user}
|
|
59
61
|
connections.contents->Array.push(conn)
|
|
60
62
|
conn
|
|
61
63
|
}
|
|
@@ -176,6 +178,27 @@ let decodeStringField = (json: JSON.t, field: string): option<string> =>
|
|
|
176
178
|
->Option.flatMap(o => o->Dict.get(field))
|
|
177
179
|
->Option.flatMap(JSON.Decode.string)
|
|
178
180
|
|
|
181
|
+
/** The line a client's subscribing or unsubscribing is told by. */
|
|
182
|
+
let subscriptionLine = (conn: connection, ~verb: string, ~channel: string) =>
|
|
183
|
+
`${verb} ${channel}${conn.user->Option.mapOr("", u => ` by ${u}`)}`
|
|
184
|
+
|
|
185
|
+
let logSubscription = (conn: connection, ~verb: string, ~channel: string) =>
|
|
186
|
+
if ReventlessCore.Logger.shouldLog(Debug, ~comp="Events:Local", ()) {
|
|
187
|
+
ReventlessCore.Logger.emit(
|
|
188
|
+
~level=Debug,
|
|
189
|
+
~comp="Events:Local",
|
|
190
|
+
~detail=JSON.Encode.object(
|
|
191
|
+
Dict.fromArray(
|
|
192
|
+
[
|
|
193
|
+
("channel", JSON.Encode.string(channel)),
|
|
194
|
+
("connection", JSON.Encode.int(conn.id)),
|
|
195
|
+
]->Array.concat(conn.user->Option.mapOr([], u => [("user", JSON.Encode.string(u))])),
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
subscriptionLine(conn, ~verb, ~channel),
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
179
202
|
/** Handle one client→server text frame on an established connection. */
|
|
180
203
|
let handleFrame = (conn: connection, text: string): unit => {
|
|
181
204
|
let parsed = try Some(text->JSON.parseOrThrow) catch {
|
|
@@ -190,6 +213,7 @@ let handleFrame = (conn: connection, text: string): unit => {
|
|
|
190
213
|
switch (decodeStringField(json, "id"), decodeStringField(json, "channel")) {
|
|
191
214
|
| (Some(id), Some(channel)) =>
|
|
192
215
|
conn.subscriptions->Dict.set(id, channel)
|
|
216
|
+
logSubscription(conn, ~verb="subscribe", ~channel)
|
|
193
217
|
conn.send(
|
|
194
218
|
frame([
|
|
195
219
|
("type", JSON.Encode.string("subscribe_success")),
|
|
@@ -201,6 +225,9 @@ let handleFrame = (conn: connection, text: string): unit => {
|
|
|
201
225
|
| Some("unsubscribe") =>
|
|
202
226
|
switch decodeStringField(json, "id") {
|
|
203
227
|
| Some(id) =>
|
|
228
|
+
conn.subscriptions
|
|
229
|
+
->Dict.get(id)
|
|
230
|
+
->Option.forEach(channel => logSubscription(conn, ~verb="unsubscribe", ~channel))
|
|
204
231
|
conn.subscriptions->Dict.delete(id)
|
|
205
232
|
conn.send(
|
|
206
233
|
frame([
|
|
@@ -367,7 +394,11 @@ let attach = (~server: YG.httpServer, ~port: int): unit => {
|
|
|
367
394
|
if tokenIsInvalid(auth) {
|
|
368
395
|
ws->wsClose(4401, "Invalid token")
|
|
369
396
|
} else {
|
|
370
|
-
let
|
|
397
|
+
let user =
|
|
398
|
+
auth
|
|
399
|
+
->Option.flatMap(t => LocalAuth.Login.verifyAndDecode(stripBearer(t)))
|
|
400
|
+
->Option.map(i => i.username)
|
|
401
|
+
let conn = addConnection(~send=f => ws->wsSend(f), ~user?)
|
|
371
402
|
ws->onMessage(data => handleFrame(conn, data->wsMessageToString("utf8")))
|
|
372
403
|
ws->onSocketClose(() => removeConnection(conn))
|
|
373
404
|
}
|
|
@@ -32,14 +32,15 @@ let nextConnectionId = {
|
|
|
32
32
|
contents: 0
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
function addConnection(send) {
|
|
35
|
+
function addConnection(send, user) {
|
|
36
36
|
nextConnectionId.contents = nextConnectionId.contents + 1 | 0;
|
|
37
37
|
let conn_id = nextConnectionId.contents;
|
|
38
38
|
let conn_subscriptions = {};
|
|
39
39
|
let conn = {
|
|
40
40
|
id: conn_id,
|
|
41
41
|
send: send,
|
|
42
|
-
subscriptions: conn_subscriptions
|
|
42
|
+
subscriptions: conn_subscriptions,
|
|
43
|
+
user: user
|
|
43
44
|
};
|
|
44
45
|
connections.contents.push(conn);
|
|
45
46
|
return conn;
|
|
@@ -127,6 +128,28 @@ function decodeStringField(json, field) {
|
|
|
127
128
|
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o[field]), Stdlib_JSON.Decode.string);
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
function subscriptionLine(conn, verb, channel) {
|
|
132
|
+
return verb + ` ` + channel + Stdlib_Option.mapOr(conn.user, "", u => ` by ` + u);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function logSubscription(conn, verb, channel) {
|
|
136
|
+
if (Logger$ReventlessCore.shouldLog("Debug", undefined, "Events:Local", undefined)) {
|
|
137
|
+
return Logger$ReventlessCore.emit("Debug", "Events:Local", undefined, Object.fromEntries([
|
|
138
|
+
[
|
|
139
|
+
"channel",
|
|
140
|
+
channel
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
"connection",
|
|
144
|
+
conn.id
|
|
145
|
+
]
|
|
146
|
+
].concat(Stdlib_Option.mapOr(conn.user, [], u => [[
|
|
147
|
+
"user",
|
|
148
|
+
u
|
|
149
|
+
]]))), undefined, subscriptionLine(conn, verb, channel));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
130
153
|
function handleFrame(conn, text) {
|
|
131
154
|
let parsed;
|
|
132
155
|
try {
|
|
@@ -149,6 +172,7 @@ function handleFrame(conn, text) {
|
|
|
149
172
|
let match$2 = decodeStringField(parsed, "channel");
|
|
150
173
|
if (match$1 !== undefined && match$2 !== undefined) {
|
|
151
174
|
conn.subscriptions[match$1] = match$2;
|
|
175
|
+
logSubscription(conn, "subscribe", match$2);
|
|
152
176
|
return conn.send(frame([
|
|
153
177
|
[
|
|
154
178
|
"type",
|
|
@@ -165,6 +189,7 @@ function handleFrame(conn, text) {
|
|
|
165
189
|
case "unsubscribe" :
|
|
166
190
|
let id = decodeStringField(parsed, "id");
|
|
167
191
|
if (id !== undefined) {
|
|
192
|
+
Stdlib_Option.forEach(conn.subscriptions[id], channel => logSubscription(conn, "unsubscribe", channel));
|
|
168
193
|
Stdlib_Dict.$$delete(conn.subscriptions, id);
|
|
169
194
|
return conn.send(frame([
|
|
170
195
|
[
|
|
@@ -320,9 +345,10 @@ function attach(server, port) {
|
|
|
320
345
|
ws.close(4401, "Invalid token");
|
|
321
346
|
return;
|
|
322
347
|
}
|
|
348
|
+
let user = Stdlib_Option.map(Stdlib_Option.flatMap(auth, t => LocalAuth$ReventlessLocal.Login.verifyAndDecode(stripBearer(t))), i => i.username);
|
|
323
349
|
let conn = addConnection(f => {
|
|
324
350
|
ws.send(f);
|
|
325
|
-
});
|
|
351
|
+
}, user);
|
|
326
352
|
ws.on("message", data => handleFrame(conn, data.toString("utf8")));
|
|
327
353
|
ws.on("close", () => removeConnection(conn));
|
|
328
354
|
});
|
|
@@ -352,6 +378,8 @@ export {
|
|
|
352
378
|
broadcastUiSlotsChanged,
|
|
353
379
|
broadcastStateChange,
|
|
354
380
|
decodeStringField,
|
|
381
|
+
subscriptionLine,
|
|
382
|
+
logSubscription,
|
|
355
383
|
handleFrame,
|
|
356
384
|
stripBearer,
|
|
357
385
|
tokenIsInvalid,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// One stored command outcome: the line the tap sends for it (component, plugin, message id,
|
|
2
|
+
// flow, cause, accepted with its entity and event count or rejected with its code), with the
|
|
3
|
+
// columns a read filters by.
|
|
4
|
+
|
|
5
|
+
type t = {
|
|
6
|
+
seq: int,
|
|
7
|
+
flow?: string,
|
|
8
|
+
msgId: string,
|
|
9
|
+
/** The tap's `commandOutcome` line, as JSON. */
|
|
10
|
+
item: JSON.t,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** What a read asks for: every field narrows, none reads every outcome. */
|
|
14
|
+
type filter = {correlationId?: string, msgId?: string}
|
|
15
|
+
|
|
16
|
+
let matches = (f: filter, e: t) =>
|
|
17
|
+
f.correlationId->Option.mapOr(true, c => e.flow == Some(c)) &&
|
|
18
|
+
f.msgId->Option.mapOr(true, m => e.msgId == m)
|
|
19
|
+
|
|
20
|
+
let filterOfJson = (j: option<JSON.t>): filter => {
|
|
21
|
+
let d = j->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
|
|
22
|
+
let s = k => d->Dict.get(k)->Option.flatMap(JSON.Decode.string)
|
|
23
|
+
{correlationId: ?s("correlationId"), msgId: ?s("msgId")}
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
|
+
|
|
7
|
+
function matches(f, e) {
|
|
8
|
+
if (Stdlib_Option.mapOr(f.correlationId, true, c => Primitive_object.equal(e.flow, c))) {
|
|
9
|
+
return Stdlib_Option.mapOr(f.msgId, true, m => e.msgId === m);
|
|
10
|
+
} else {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function filterOfJson(j) {
|
|
16
|
+
let d = Stdlib_Option.getOr(Stdlib_Option.flatMap(j, Stdlib_JSON.Decode.object), {});
|
|
17
|
+
let s = k => Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.string);
|
|
18
|
+
return {
|
|
19
|
+
correlationId: s("correlationId"),
|
|
20
|
+
msgId: s("msgId")
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
matches,
|
|
26
|
+
filterOfJson,
|
|
27
|
+
}
|
|
28
|
+
/* No side effect */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The command outcomes of an in-memory platform, oldest first, for as long as it runs.
|
|
2
|
+
|
|
3
|
+
type entry = CommandOutcomeLogEntry.t
|
|
4
|
+
|
|
5
|
+
let entries: array<entry> = []
|
|
6
|
+
|
|
7
|
+
let append = (e: entry) => entries->Array.push(e)
|
|
8
|
+
|
|
9
|
+
/** Up to `first` entries matching the filter, newest first, below the `before` sequence number. */
|
|
10
|
+
let read = (~filter: CommandOutcomeLogEntry.filter, ~before: option<int>, ~first: int): array<
|
|
11
|
+
entry,
|
|
12
|
+
> => {
|
|
13
|
+
let out = []
|
|
14
|
+
let i = ref(entries->Array.length - 1)
|
|
15
|
+
while i.contents >= 0 && out->Array.length < first {
|
|
16
|
+
let e = entries->Array.getUnsafe(i.contents)
|
|
17
|
+
if before->Option.mapOr(true, b => e.seq < b) && CommandOutcomeLogEntry.matches(filter, e) {
|
|
18
|
+
out->Array.push(e)
|
|
19
|
+
}
|
|
20
|
+
i := i.contents - 1
|
|
21
|
+
}
|
|
22
|
+
out
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Test seam. */
|
|
26
|
+
let clear = () => entries->Array.splice(~start=0, ~remove=entries->Array.length, ~insert=[])
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as CommandOutcomeLogEntry$ReventlessLocal from "./CommandOutcomeLogEntry.res.mjs";
|
|
5
|
+
|
|
6
|
+
let entries = [];
|
|
7
|
+
|
|
8
|
+
function append(e) {
|
|
9
|
+
entries.push(e);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function read(filter, before, first) {
|
|
13
|
+
let out = [];
|
|
14
|
+
let i = entries.length - 1 | 0;
|
|
15
|
+
while (i >= 0 && out.length < first) {
|
|
16
|
+
let e = entries[i];
|
|
17
|
+
if (Stdlib_Option.mapOr(before, true, b => e.seq < b) && CommandOutcomeLogEntry$ReventlessLocal.matches(filter, e)) {
|
|
18
|
+
out.push(e);
|
|
19
|
+
}
|
|
20
|
+
i = i - 1 | 0;
|
|
21
|
+
};
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function clear() {
|
|
26
|
+
entries.splice(0, entries.length);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
entries,
|
|
31
|
+
append,
|
|
32
|
+
read,
|
|
33
|
+
clear,
|
|
34
|
+
}
|
|
35
|
+
/* No side effect */
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The command outcomes of a SQLite platform, kept in its store across restarts.
|
|
2
|
+
|
|
3
|
+
type entry = CommandOutcomeLogEntry.t
|
|
4
|
+
|
|
5
|
+
let table = "reventless_command_outcomes"
|
|
6
|
+
|
|
7
|
+
let ensureSchema = db => {
|
|
8
|
+
db->SqliteDriver.exec(
|
|
9
|
+
`CREATE TABLE IF NOT EXISTS ${table} (seq INTEGER PRIMARY KEY AUTOINCREMENT, flow TEXT, msg_id TEXT NOT NULL, item TEXT NOT NULL)`,
|
|
10
|
+
)
|
|
11
|
+
db->SqliteDriver.exec(`CREATE INDEX IF NOT EXISTS ${table}_flow ON ${table} (flow, seq)`)
|
|
12
|
+
db->SqliteDriver.exec(`CREATE INDEX IF NOT EXISTS ${table}_msg ON ${table} (msg_id)`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let str = JSON.Encode.string
|
|
16
|
+
|
|
17
|
+
let append = (db, e: entry) =>
|
|
18
|
+
db
|
|
19
|
+
->SqliteDriver.prepare(`INSERT INTO ${table} (flow, msg_id, item) VALUES (?, ?, ?)`)
|
|
20
|
+
->SqliteDriver.run([
|
|
21
|
+
e.flow->Option.mapOr(JSON.Encode.null, str),
|
|
22
|
+
str(e.msgId),
|
|
23
|
+
str(e.item->JSON.stringify),
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
let entryOfRow = (row: dict<JSON.t>): option<entry> => {
|
|
27
|
+
let s = k => row->Dict.get(k)->Option.flatMap(JSON.Decode.string)
|
|
28
|
+
switch (
|
|
29
|
+
row->Dict.get("seq")->Option.flatMap(JSON.Decode.float),
|
|
30
|
+
s("msg_id"),
|
|
31
|
+
s("item")->Option.flatMap(i =>
|
|
32
|
+
try Some(JSON.parseOrThrow(i)) catch {
|
|
33
|
+
| _ => None
|
|
34
|
+
}
|
|
35
|
+
),
|
|
36
|
+
) {
|
|
37
|
+
| (Some(seq), Some(msgId), Some(item)) =>
|
|
38
|
+
Some({seq: seq->Float.toInt, flow: ?s("flow"), msgId, item})
|
|
39
|
+
| _ => None
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Up to `first` entries matching the filter, newest first, below the `before` sequence number. */
|
|
44
|
+
let read = (db, ~filter: CommandOutcomeLogEntry.filter, ~before: option<int>, ~first: int): array<
|
|
45
|
+
entry,
|
|
46
|
+
> => {
|
|
47
|
+
let where = []
|
|
48
|
+
let params = []
|
|
49
|
+
let add = (clause, value) => {
|
|
50
|
+
where->Array.push(clause)
|
|
51
|
+
params->Array.push(value)
|
|
52
|
+
}
|
|
53
|
+
filter.correlationId->Option.forEach(c => add("flow = ?", str(c)))
|
|
54
|
+
filter.msgId->Option.forEach(m => add("msg_id = ?", str(m)))
|
|
55
|
+
before->Option.forEach(b => add("seq < ?", JSON.Encode.int(b)))
|
|
56
|
+
let sql = `SELECT seq, flow, msg_id, item FROM ${table}${where == []
|
|
57
|
+
? ""
|
|
58
|
+
: " WHERE " ++ where->Array.join(" AND ")} ORDER BY seq DESC LIMIT ?`
|
|
59
|
+
params->Array.push(JSON.Encode.int(first))
|
|
60
|
+
db->SqliteDriver.prepare(sql)->SqliteDriver.all(params)->Array.filterMap(entryOfRow)
|
|
61
|
+
}
|