@reventlessdev/reventless-local 3.0.0-alpha.319 → 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 +11 -0
- package/package.json +11 -11
- package/src/Platform.res +13 -0
- package/src/Platform.res.mjs +14 -0
- 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/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/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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
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
|
+
|
|
6
17
|
# 3.0.0-alpha.319 (2026-11-01)
|
|
7
18
|
|
|
8
19
|
### 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-
|
|
38
|
-
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.30",
|
|
37
|
+
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.31",
|
|
39
38
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
40
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.25",
|
|
41
39
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
|
|
42
|
-
"@reventlessdev/
|
|
43
|
-
"@reventlessdev/
|
|
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",
|
|
44
45
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-seed": "1.0.0-alpha.40"
|
|
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",
|
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()
|
|
@@ -1426,6 +1437,8 @@ module MakeWithConfig = (
|
|
|
1426
1437
|
? `http://localhost:${platformPort->Int.toString}/graphql`
|
|
1427
1438
|
: `http://localhost:${domainPort->Int.toString}/graphql`,
|
|
1428
1439
|
)
|
|
1440
|
+
// A platform left running for an editor stops once nothing has watched it for a while.
|
|
1441
|
+
LocalIdleExit.start(~env=NodeProcess.env)
|
|
1429
1442
|
// After the register, and it fills the tap port in once the socket is bound:
|
|
1430
1443
|
// the port is ephemeral by default, so it is not known until `listen` calls
|
|
1431
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") {
|
|
@@ -1177,6 +1186,7 @@ function MakeWithConfig(Config) {
|
|
|
1177
1186
|
kind: match[0],
|
|
1178
1187
|
path: match[1]
|
|
1179
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();
|
|
@@ -2974,6 +2987,7 @@ function Make($star) {
|
|
|
2974
2987
|
kind: match[0],
|
|
2975
2988
|
path: match[1]
|
|
2976
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
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
7
|
+
import * as SqliteDriver$ReventlessLocal from "../SqliteDriver.res.mjs";
|
|
8
|
+
|
|
9
|
+
let table = "reventless_command_outcomes";
|
|
10
|
+
|
|
11
|
+
function ensureSchema(db) {
|
|
12
|
+
SqliteDriver$ReventlessLocal.exec(db, `CREATE TABLE IF NOT EXISTS ` + table + ` (seq INTEGER PRIMARY KEY AUTOINCREMENT, flow TEXT, msg_id TEXT NOT NULL, item TEXT NOT NULL)`);
|
|
13
|
+
SqliteDriver$ReventlessLocal.exec(db, `CREATE INDEX IF NOT EXISTS ` + table + `_flow ON ` + table + ` (flow, seq)`);
|
|
14
|
+
SqliteDriver$ReventlessLocal.exec(db, `CREATE INDEX IF NOT EXISTS ` + table + `_msg ON ` + table + ` (msg_id)`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function str(prim) {
|
|
18
|
+
return prim;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function append(db, e) {
|
|
22
|
+
SqliteDriver$ReventlessLocal.run(SqliteDriver$ReventlessLocal.prepare(db, `INSERT INTO ` + table + ` (flow, msg_id, item) VALUES (?, ?, ?)`), [
|
|
23
|
+
Stdlib_Option.mapOr(e.flow, null, str),
|
|
24
|
+
e.msgId,
|
|
25
|
+
JSON.stringify(e.item)
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function entryOfRow(row) {
|
|
30
|
+
let s = k => Stdlib_Option.flatMap(row[k], Stdlib_JSON.Decode.string);
|
|
31
|
+
let match = Stdlib_Option.flatMap(row["seq"], Stdlib_JSON.Decode.float);
|
|
32
|
+
let match$1 = s("msg_id");
|
|
33
|
+
let match$2 = Stdlib_Option.flatMap(s("item"), i => {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(i);
|
|
36
|
+
} catch (exn) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
if (match !== undefined && match$1 !== undefined && match$2 !== undefined) {
|
|
41
|
+
return {
|
|
42
|
+
seq: match | 0,
|
|
43
|
+
flow: s("flow"),
|
|
44
|
+
msgId: match$1,
|
|
45
|
+
item: match$2
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function read(db, filter, before, first) {
|
|
51
|
+
let where = [];
|
|
52
|
+
let params = [];
|
|
53
|
+
let add = (clause, value) => {
|
|
54
|
+
where.push(clause);
|
|
55
|
+
params.push(value);
|
|
56
|
+
};
|
|
57
|
+
Stdlib_Option.forEach(filter.correlationId, c => add("flow = ?", c));
|
|
58
|
+
Stdlib_Option.forEach(filter.msgId, m => add("msg_id = ?", m));
|
|
59
|
+
Stdlib_Option.forEach(before, b => add("seq < ?", b));
|
|
60
|
+
let sql = `SELECT seq, flow, msg_id, item FROM ` + table + (
|
|
61
|
+
Primitive_object.equal(where, []) ? "" : " WHERE " + where.join(" AND ")
|
|
62
|
+
) + ` ORDER BY seq DESC LIMIT ?`;
|
|
63
|
+
params.push(first);
|
|
64
|
+
return Stdlib_Array.filterMap(SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, sql), params), entryOfRow);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
table,
|
|
69
|
+
ensureSchema,
|
|
70
|
+
str,
|
|
71
|
+
append,
|
|
72
|
+
entryOfRow,
|
|
73
|
+
read,
|
|
74
|
+
}
|
|
75
|
+
/* SqliteDriver-ReventlessLocal Not a pure module */
|