@reventlessdev/reventless-local 3.0.0-alpha.317 → 3.0.0-alpha.319
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 +16 -0
- package/package.json +10 -10
- package/src/LocalPlatformRegistry.res +26 -1
- package/src/LocalPlatformRegistry.res.mjs +25 -5
- package/src/Platform.res +27 -2
- package/src/Platform.res.mjs +24 -4
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +38 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +29 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +87 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +76 -0
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +8 -1
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +17 -4
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +49 -2
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +57 -21
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +85 -0
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +50 -0
- package/src/adapter/EventLog/LocalEventLogStorage.res +3 -0
- package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +6 -2
- package/src/adapter/LocalBus.res +32 -5
- package/src/adapter/LocalBus.res.mjs +27 -7
- package/src/adapter/LocalEventLogRead.res +477 -0
- package/src/adapter/LocalEventLogRead.res.mjs +507 -0
- package/src/adapter/LocalEventTap.res +18 -80
- package/src/adapter/LocalEventTap.res.mjs +19 -108
- package/src/adapter/LocalFeed.res +433 -0
- package/src/adapter/LocalFeed.res.mjs +419 -0
- package/src/adapter/LocalLogFeed.res +217 -0
- package/src/adapter/LocalLogFeed.res.mjs +295 -0
- package/src/adapter/LocalPgEventLogRead.res +85 -0
- package/src/adapter/LocalPgEventLogRead.res.mjs +91 -0
- package/src/adapter/LocalPlatformObserve.res +456 -0
- package/src/adapter/LocalPlatformObserve.res.mjs +505 -0
- package/src/adapter/LocalTapLines.res +149 -0
- package/src/adapter/LocalTapLines.res.mjs +232 -0
- package/src/adapter/PlatformGraphQL_Server.res +36 -3
- package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
- package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
- 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/src/reexport/CommandTopic.res.mjs +5 -2
- package/tests/LocalPlatformRegistryTest.res +17 -0
- package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
- package/tests/LocalPlatformStartTest.res.mjs +1 -1
- package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
- package/tests/adapter/LocalEventLogReadTest.res +539 -0
- package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
- package/tests/adapter/LocalFeedTest.res +314 -0
- package/tests/adapter/LocalFeedTest.res.mjs +329 -0
- package/tests/adapter/LocalPlatformObserveTest.res +165 -0
- package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
- package/tests/adapter/LocalTapLinesTest.res +207 -0
- package/tests/adapter/LocalTapLinesTest.res.mjs +274 -0
- package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
- package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePiiTest.res.mjs +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.319 (2026-11-01)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core:** a component's log line carries the ids of the message it is about ([9c26267](https://github.com/ReventlessDev/reventless-core/commit/9c26267ca6d03ff66b8406caa32243e205e29b5a))
|
|
11
|
+
* **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))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.318 (2026-11-01)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **core:** a command's outcome, the flow through automations, and each view change are told ([dcaeed2](https://github.com/ReventlessDev/reventless-core/commit/dcaeed2fabe2047a21797b459f2931618474dec3))
|
|
19
|
+
* **local:** the local platform serves its logs and events to whoever watches it ([0c29b0d](https://github.com/ReventlessDev/reventless-core/commit/0c29b0d08c668ad5b948a8017178dad8692647d0))
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# 3.0.0-alpha.317 (2026-10-31)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @reventlessdev/reventless-local
|
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.319",
|
|
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": {
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
"sury": "11.0.0",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
38
|
-
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.
|
|
38
|
+
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.30",
|
|
39
39
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
40
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.25",
|
|
40
41
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
|
|
41
|
-
"@reventlessdev/
|
|
42
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.327",
|
|
43
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.175",
|
|
42
44
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-seed": "1.0.0-alpha.39",
|
|
49
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.191"
|
|
45
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.276",
|
|
46
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.221",
|
|
47
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.191",
|
|
48
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.192",
|
|
49
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.40"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.1",
|
|
@@ -27,6 +27,12 @@ type entry = {
|
|
|
27
27
|
// DELETES an entry it cannot decode, so requiring it would silently hide every
|
|
28
28
|
// platform from an older build. Absent ⇒ stdout only.
|
|
29
29
|
tapPort?: int,
|
|
30
|
+
// Where the log feed is served, when it is: the platform's recent and live log
|
|
31
|
+
// records. Optional for the same reason as `tapPort`.
|
|
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,
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
// Threaded rather than read at each use, so tests need not move the process.
|
|
@@ -112,6 +118,7 @@ let write = (
|
|
|
112
118
|
~loginEndpoint: string,
|
|
113
119
|
~store: store,
|
|
114
120
|
~tapPort: option<int>=?,
|
|
121
|
+
~platformEndpoint: option<string>=?,
|
|
115
122
|
~pid: int=NodeProcess.pid,
|
|
116
123
|
~cwd: string=NodeProcess.cwd(),
|
|
117
124
|
): string => {
|
|
@@ -125,6 +132,7 @@ let write = (
|
|
|
125
132
|
store,
|
|
126
133
|
startedAt: Date.make()->Date.toISOString,
|
|
127
134
|
?tapPort,
|
|
135
|
+
?platformEndpoint,
|
|
128
136
|
}
|
|
129
137
|
try {
|
|
130
138
|
NodeFs.mkdirSync(runningDir(~cwd, ()), {recursive: true})
|
|
@@ -159,6 +167,22 @@ let publishTapPort = (~port: int, ~tapPort: int, ~cwd=NodeProcess.cwd()): unit =
|
|
|
159
167
|
}
|
|
160
168
|
}
|
|
161
169
|
|
|
170
|
+
/** Adds the log feed's port to an entry already written, once its socket is
|
|
171
|
+
bound; see {!publishTapPort}. */
|
|
172
|
+
let publishLogPort = (~port: int, ~logPort: int, ~cwd=NodeProcess.cwd()): unit => {
|
|
173
|
+
let path = entryPath(~port, ~cwd)
|
|
174
|
+
switch readEntry(path) {
|
|
175
|
+
| None => ()
|
|
176
|
+
| Some(entry) =>
|
|
177
|
+
try NodeFs.writeFileSync(
|
|
178
|
+
path,
|
|
179
|
+
{...entry, logPort}->Reventless.Util_Sury.toJson(entrySchema)->JSON.stringify(~space=2),
|
|
180
|
+
) catch {
|
|
181
|
+
| _ => ()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
162
186
|
/** Publishes this process, and unpublishes it on the way out. Both paths are
|
|
163
187
|
needed: `exit` is sync-only, and registering a signal handler suppresses
|
|
164
188
|
Node's default exit, so each signal must leave explicitly. */
|
|
@@ -168,8 +192,9 @@ let register = (
|
|
|
168
192
|
~loginEndpoint: string,
|
|
169
193
|
~store: store,
|
|
170
194
|
~tapPort: option<int>=?,
|
|
195
|
+
~platformEndpoint: option<string>=?,
|
|
171
196
|
): unit => {
|
|
172
|
-
let path = write(~port, ~endpoint, ~loginEndpoint, ~store, ~tapPort?)
|
|
197
|
+
let path = write(~port, ~endpoint, ~loginEndpoint, ~store, ~tapPort?, ~platformEndpoint?)
|
|
173
198
|
NodeProcess.onExit(_ => removeQuietly(path))
|
|
174
199
|
NodeProcess.onSignal(SIGINT, () => {
|
|
175
200
|
removeQuietly(path)
|
|
@@ -22,7 +22,9 @@ let entrySchema = Sury.$schema(s => ({
|
|
|
22
22
|
loginEndpoint: s.m(Sury.string),
|
|
23
23
|
store: s.m(storeSchema),
|
|
24
24
|
startedAt: s.m(Sury.string),
|
|
25
|
-
tapPort: s.m(Sury.$option(Sury.int32))
|
|
25
|
+
tapPort: s.m(Sury.$option(Sury.int32)),
|
|
26
|
+
logPort: s.m(Sury.$option(Sury.int32)),
|
|
27
|
+
platformEndpoint: s.m(Sury.$option(Sury.string))
|
|
26
28
|
}));
|
|
27
29
|
|
|
28
30
|
function runningDir(cwdOpt, param) {
|
|
@@ -109,7 +111,7 @@ function list(cwdOpt, param) {
|
|
|
109
111
|
}).toSorted((a, b) => Primitive_int.compare(a.port, b.port));
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
function write(port, endpoint, loginEndpoint, store, tapPort, pidOpt, cwdOpt) {
|
|
114
|
+
function write(port, endpoint, loginEndpoint, store, tapPort, platformEndpoint, pidOpt, cwdOpt) {
|
|
113
115
|
let pid = pidOpt !== undefined ? pidOpt : process.pid;
|
|
114
116
|
let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
|
|
115
117
|
let path = entryPath(port, cwd);
|
|
@@ -123,7 +125,8 @@ function write(port, endpoint, loginEndpoint, store, tapPort, pidOpt, cwdOpt) {
|
|
|
123
125
|
loginEndpoint: loginEndpoint,
|
|
124
126
|
store: store,
|
|
125
127
|
startedAt: entry_startedAt,
|
|
126
|
-
tapPort: tapPort
|
|
128
|
+
tapPort: tapPort,
|
|
129
|
+
platformEndpoint: platformEndpoint
|
|
127
130
|
};
|
|
128
131
|
try {
|
|
129
132
|
Nodefs.mkdirSync(runningDir(cwd, undefined), {
|
|
@@ -152,8 +155,24 @@ function publishTapPort(port, tapPort, cwdOpt) {
|
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
function
|
|
156
|
-
let
|
|
158
|
+
function publishLogPort(port, logPort, cwdOpt) {
|
|
159
|
+
let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
|
|
160
|
+
let path = entryPath(port, cwd);
|
|
161
|
+
let entry = readEntry(path);
|
|
162
|
+
if (entry === undefined) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
let newrecord = {...entry};
|
|
167
|
+
Nodefs.writeFileSync(path, JSON.stringify(Util_Sury$Reventless.toJson((newrecord.logPort = logPort, newrecord), entrySchema), undefined, 2), "utf8");
|
|
168
|
+
return;
|
|
169
|
+
} catch (exn) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function register(port, endpoint, loginEndpoint, store, tapPort, platformEndpoint) {
|
|
175
|
+
let path = write(port, endpoint, loginEndpoint, store, tapPort, platformEndpoint, undefined, undefined);
|
|
157
176
|
process.on("exit", param => removeQuietly(path));
|
|
158
177
|
process.on("SIGINT", () => {
|
|
159
178
|
removeQuietly(path);
|
|
@@ -177,6 +196,7 @@ export {
|
|
|
177
196
|
list,
|
|
178
197
|
write,
|
|
179
198
|
publishTapPort,
|
|
199
|
+
publishLogPort,
|
|
180
200
|
register,
|
|
181
201
|
}
|
|
182
202
|
/* storeSchema Not a pure module */
|
package/src/Platform.res
CHANGED
|
@@ -106,6 +106,13 @@ module MakeWithConfig = (
|
|
|
106
106
|
// local platform has no per-component process boundary.
|
|
107
107
|
let _ = Config.commandHandlerConfig
|
|
108
108
|
|
|
109
|
+
// Keep the recent log records from here on, for the log feed and the MCP
|
|
110
|
+
// resource that read them.
|
|
111
|
+
LocalLogFeed.install()
|
|
112
|
+
// What became of each command, and each row a projection changed, on the
|
|
113
|
+
// event tap; a rejected command also in the problem ring.
|
|
114
|
+
LocalTapLines.install()
|
|
115
|
+
|
|
109
116
|
// What this platform provides for the roles its plugins need: every account it
|
|
110
117
|
// can sign in as, built-in and loaded alike, or the file the store will load
|
|
111
118
|
// when the servers start — after the plugins are built and checked.
|
|
@@ -327,8 +334,13 @@ module MakeWithConfig = (
|
|
|
327
334
|
// Event-history queries — the historical read counterpart of the Source A
|
|
328
335
|
// subscription bridged further down. Same `eventLogEntries`, so the two
|
|
329
336
|
// always describe the same set of streams.
|
|
330
|
-
eventQueryResolverHook: params =>
|
|
331
|
-
|
|
337
|
+
eventQueryResolverHook: params => {
|
|
338
|
+
// The read across every log names each event's plugin.
|
|
339
|
+
params.eventLogEntries->Array.forEach(entry =>
|
|
340
|
+
LocalEventLogRead.notePlugin(~log=entry.busKey, ~plugin=params.pluginName)
|
|
341
|
+
)
|
|
342
|
+
EventHistoryResolvers.register(~server=resolveTargetGraphQL(), params)
|
|
343
|
+
},
|
|
332
344
|
// MCP tools and resources — registered during plugin construction.
|
|
333
345
|
// See the large lambda below; it references Bus for QueryDb lookups.
|
|
334
346
|
mcpSchemaRegistrationHook: ({
|
|
@@ -1410,6 +1422,9 @@ module MakeWithConfig = (
|
|
|
1410
1422
|
~endpoint=`http://localhost:${domainPort->Int.toString}/graphql`,
|
|
1411
1423
|
~loginEndpoint=`http://localhost:${domainPort->Int.toString}/__inmemory/login`,
|
|
1412
1424
|
~store={kind, ?path},
|
|
1425
|
+
~platformEndpoint=Config.splitApi
|
|
1426
|
+
? `http://localhost:${platformPort->Int.toString}/graphql`
|
|
1427
|
+
: `http://localhost:${domainPort->Int.toString}/graphql`,
|
|
1413
1428
|
)
|
|
1414
1429
|
// After the register, and it fills the tap port in once the socket is bound:
|
|
1415
1430
|
// the port is ephemeral by default, so it is not known until `listen` calls
|
|
@@ -1418,6 +1433,11 @@ module MakeWithConfig = (
|
|
|
1418
1433
|
~onBound=tapPort => LocalPlatformRegistry.publishTapPort(~port=domainPort, ~tapPort),
|
|
1419
1434
|
(),
|
|
1420
1435
|
)
|
|
1436
|
+
// The log feed, the same way: its port is filled in once it is bound.
|
|
1437
|
+
LocalLogFeed.start(
|
|
1438
|
+
~onBound=logPort => LocalPlatformRegistry.publishLogPort(~port=domainPort, ~logPort),
|
|
1439
|
+
(),
|
|
1440
|
+
)
|
|
1421
1441
|
// Fire onPlatformDeployed after all servers are started so late-deployed
|
|
1422
1442
|
// plugins (e.g. PlatformInspector) have their handler refs populated.
|
|
1423
1443
|
ReventlessCore.Plugin_Helpers.firePlatformDeployedHook({
|
|
@@ -2113,6 +2133,11 @@ module MakeWithConfig = (
|
|
|
2113
2133
|
|
|
2114
2134
|
platformGraphQL.registerQueries(~sdlFields=baseParts.queries, ~resolvers=queryResolvers)
|
|
2115
2135
|
|
|
2136
|
+
// What a tool watching this platform reads: every event of every log, and
|
|
2137
|
+
// the log level while running.
|
|
2138
|
+
LocalPlatformObserve.registerGraphQL(platformGraphQL)
|
|
2139
|
+
LocalPlatformObserve.registerMcp(platformMCP)
|
|
2140
|
+
|
|
2116
2141
|
// Admin Plugin lifecycle mutations (Activate / Deactivate / Retire) now flow
|
|
2117
2142
|
// through the LocalPluginAggregate, mirroring AWS. The resolver looks up the
|
|
2118
2143
|
// plugin's current version from the "Plugins" read model (the aggregate is
|
package/src/Platform.res.mjs
CHANGED
|
@@ -38,11 +38,13 @@ import * as ShellConfig$ReventlessLocal from "./ShellConfig.res.mjs";
|
|
|
38
38
|
import * as AccountStore$ReventlessLocal from "./adapter/Auth/AccountStore.res.mjs";
|
|
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
|
+
import * as LocalLogFeed$ReventlessLocal from "./adapter/LocalLogFeed.res.mjs";
|
|
41
42
|
import * as SqliteDriver$ReventlessLocal from "./adapter/SqliteDriver.res.mjs";
|
|
42
43
|
import * as Task_Builder$ReventlessLocal from "./components/Task_Builder.res.mjs";
|
|
43
44
|
import * as BakedManifest$ReventlessLocal from "./BakedManifest.res.mjs";
|
|
44
45
|
import * as HostShellDist$ReventlessLocal from "./HostShellDist.res.mjs";
|
|
45
46
|
import * as LocalEventTap$ReventlessLocal from "./adapter/LocalEventTap.res.mjs";
|
|
47
|
+
import * as LocalTapLines$ReventlessLocal from "./adapter/LocalTapLines.res.mjs";
|
|
46
48
|
import * as Platform_Admin$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin.res.mjs";
|
|
47
49
|
import * as PluginDecision$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginDecision.res.mjs";
|
|
48
50
|
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
@@ -63,6 +65,7 @@ import * as Scheduler_Builder$ReventlessCore from "@reventlessdev/reventless-cor
|
|
|
63
65
|
import * as Aggregate_Builder$ReventlessLocal from "./components/Aggregate_Builder.res.mjs";
|
|
64
66
|
import * as LocalCapabilities$ReventlessLocal from "./adapter/LocalCapabilities.res.mjs";
|
|
65
67
|
import * as LocalClonerRunner$ReventlessLocal from "./adapter/Cloner/LocalClonerRunner.res.mjs";
|
|
68
|
+
import * as LocalEventLogRead$ReventlessLocal from "./adapter/LocalEventLogRead.res.mjs";
|
|
66
69
|
import * as PluginBaseFragment$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/api/PluginBaseFragment.res.mjs";
|
|
67
70
|
import * as ProjectionPending$ReventlessLocal from "./adapter/ProjectionPending.res.mjs";
|
|
68
71
|
import * as ReadModel_Builder$ReventlessLocal from "./components/ReadModel_Builder.res.mjs";
|
|
@@ -77,6 +80,7 @@ import * as PluginsReadModelSpec$ReventlessCore from "@reventlessdev/reventless-
|
|
|
77
80
|
import * as StorePrefixCollision$ReventlessCore from "@reventlessdev/reventless-core/src/util/StorePrefixCollision.res.mjs";
|
|
78
81
|
import * as DomainGraphQL_Server$ReventlessLocal from "./adapter/DomainGraphQL_Server.res.mjs";
|
|
79
82
|
import * as EventPublish_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventLog/EventPublish_Callback.res.mjs";
|
|
83
|
+
import * as LocalPlatformObserve$ReventlessLocal from "./adapter/LocalPlatformObserve.res.mjs";
|
|
80
84
|
import * as LocalUploadResolvers$ReventlessLocal from "./adapter/LocalUploadResolvers.res.mjs";
|
|
81
85
|
import * as ProjectionCheckpoint$ReventlessLocal from "./adapter/ProjectionCheckpoint.res.mjs";
|
|
82
86
|
import * as ExtensionPointMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
|
|
@@ -146,6 +150,8 @@ function decodeUiFragmentRegistryEventEnvelope(eventJson) {
|
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
function MakeWithConfig(Config) {
|
|
153
|
+
LocalLogFeed$ReventlessLocal.install(undefined);
|
|
154
|
+
LocalTapLines$ReventlessLocal.install();
|
|
149
155
|
Role$Reventless.provideGroupsFrom(AccountStore$ReventlessLocal.providedGroups);
|
|
150
156
|
TestRunner$ReventlessLocal.setup();
|
|
151
157
|
let match = Config.backend;
|
|
@@ -444,7 +450,10 @@ function MakeWithConfig(Config) {
|
|
|
444
450
|
});
|
|
445
451
|
LocalGraphQL_SubscriptionResolvers$ReventlessLocal.registerAll(server, subscriptionFields, sourceAEntries, []);
|
|
446
452
|
};
|
|
447
|
-
let hooks_eventQueryResolverHook = params =>
|
|
453
|
+
let hooks_eventQueryResolverHook = params => {
|
|
454
|
+
params.eventLogEntries.forEach(entry => LocalEventLogRead$ReventlessLocal.notePlugin(entry.busKey, params.pluginName));
|
|
455
|
+
EventHistoryResolvers.register(resolveTargetGraphQL(), params);
|
|
456
|
+
};
|
|
448
457
|
let hooks_adminExtensionPoints = {
|
|
449
458
|
contents: Pulumi.output({})
|
|
450
459
|
};
|
|
@@ -1167,8 +1176,9 @@ function MakeWithConfig(Config) {
|
|
|
1167
1176
|
LocalPlatformRegistry$ReventlessLocal.register(domainPort, `http://localhost:` + domainPort.toString() + `/graphql`, `http://localhost:` + domainPort.toString() + `/__inmemory/login`, {
|
|
1168
1177
|
kind: match[0],
|
|
1169
1178
|
path: match[1]
|
|
1170
|
-
}, undefined);
|
|
1179
|
+
}, undefined, Config.splitApi ? `http://localhost:` + platformPort.toString() + `/graphql` : `http://localhost:` + domainPort.toString() + `/graphql`);
|
|
1171
1180
|
LocalEventTap$ReventlessLocal.start(undefined, tapPort => LocalPlatformRegistry$ReventlessLocal.publishTapPort(domainPort, tapPort, undefined), undefined);
|
|
1181
|
+
LocalLogFeed$ReventlessLocal.start(undefined, logPort => LocalPlatformRegistry$ReventlessLocal.publishLogPort(domainPort, logPort, undefined), undefined);
|
|
1172
1182
|
Plugin_Helpers$ReventlessCore.firePlatformDeployedHook({
|
|
1173
1183
|
name: "local",
|
|
1174
1184
|
environment: Pulumi.getStack(),
|
|
@@ -1545,6 +1555,8 @@ function MakeWithConfig(Config) {
|
|
|
1545
1555
|
};
|
|
1546
1556
|
registerAdminItemsAndIndexResolvers(queryResolvers, true);
|
|
1547
1557
|
platformGraphQL.registerQueries(baseParts.queries, queryResolvers);
|
|
1558
|
+
LocalPlatformObserve$ReventlessLocal.registerGraphQL(platformGraphQL);
|
|
1559
|
+
LocalPlatformObserve$ReventlessLocal.registerMcp(platformMCP);
|
|
1548
1560
|
let argId = args => Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d["id"]), Stdlib_JSON.Decode.string), "");
|
|
1549
1561
|
let currentVersionOf = pluginName => {
|
|
1550
1562
|
let scanAll = Bus.getQueryDbScan(PluginsReadModelSpec$ReventlessCore.name);
|
|
@@ -1946,6 +1958,8 @@ function MakeWithConfig(Config) {
|
|
|
1946
1958
|
|
|
1947
1959
|
function Make($star) {
|
|
1948
1960
|
let backend = Backend$ReventlessLocal.fromEnv();
|
|
1961
|
+
LocalLogFeed$ReventlessLocal.install(undefined);
|
|
1962
|
+
LocalTapLines$ReventlessLocal.install();
|
|
1949
1963
|
Role$Reventless.provideGroupsFrom(AccountStore$ReventlessLocal.providedGroups);
|
|
1950
1964
|
TestRunner$ReventlessLocal.setup();
|
|
1951
1965
|
if (typeof backend !== "object") {
|
|
@@ -2237,7 +2251,10 @@ function Make($star) {
|
|
|
2237
2251
|
});
|
|
2238
2252
|
LocalGraphQL_SubscriptionResolvers$ReventlessLocal.registerAll(server, subscriptionFields, sourceAEntries, []);
|
|
2239
2253
|
};
|
|
2240
|
-
let hooks_eventQueryResolverHook = params =>
|
|
2254
|
+
let hooks_eventQueryResolverHook = params => {
|
|
2255
|
+
params.eventLogEntries.forEach(entry => LocalEventLogRead$ReventlessLocal.notePlugin(entry.busKey, params.pluginName));
|
|
2256
|
+
EventHistoryResolvers.register(resolveTargetGraphQL(), params);
|
|
2257
|
+
};
|
|
2241
2258
|
let hooks_adminExtensionPoints = {
|
|
2242
2259
|
contents: Pulumi.output({})
|
|
2243
2260
|
};
|
|
@@ -2956,8 +2973,9 @@ function Make($star) {
|
|
|
2956
2973
|
LocalPlatformRegistry$ReventlessLocal.register(domainPort, `http://localhost:` + domainPort.toString() + `/graphql`, `http://localhost:` + domainPort.toString() + `/__inmemory/login`, {
|
|
2957
2974
|
kind: match[0],
|
|
2958
2975
|
path: match[1]
|
|
2959
|
-
}, undefined);
|
|
2976
|
+
}, undefined, `http://localhost:` + platformPort.toString() + `/graphql`);
|
|
2960
2977
|
LocalEventTap$ReventlessLocal.start(undefined, tapPort => LocalPlatformRegistry$ReventlessLocal.publishTapPort(domainPort, tapPort, undefined), undefined);
|
|
2978
|
+
LocalLogFeed$ReventlessLocal.start(undefined, logPort => LocalPlatformRegistry$ReventlessLocal.publishLogPort(domainPort, logPort, undefined), undefined);
|
|
2961
2979
|
Plugin_Helpers$ReventlessCore.firePlatformDeployedHook({
|
|
2962
2980
|
name: "local",
|
|
2963
2981
|
environment: Pulumi.getStack(),
|
|
@@ -3333,6 +3351,8 @@ function Make($star) {
|
|
|
3333
3351
|
};
|
|
3334
3352
|
registerAdminItemsAndIndexResolvers(queryResolvers, true);
|
|
3335
3353
|
platformGraphQL.registerQueries(baseParts.queries, queryResolvers);
|
|
3354
|
+
LocalPlatformObserve$ReventlessLocal.registerGraphQL(platformGraphQL);
|
|
3355
|
+
LocalPlatformObserve$ReventlessLocal.registerMcp(platformMCP);
|
|
3336
3356
|
let argId = args => Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d["id"]), Stdlib_JSON.Decode.string), "");
|
|
3337
3357
|
let currentVersionOf = pluginName => {
|
|
3338
3358
|
let scanAll = Bus.getQueryDbScan(PluginsReadModelSpec$ReventlessCore.name);
|
|
@@ -128,6 +128,39 @@ let makeStorage = (~name as _name, ~indexes as _, ~partitionTag as _, ~opts as _
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// Newest first, for the read across every log.
|
|
132
|
+
LocalEventLogRead.register(~log=_name, ~kind=Dcb, async (~bound, ~limit, ~correlationId) => {
|
|
133
|
+
let out = []
|
|
134
|
+
let i = ref(events.contents->Array.length - 1)
|
|
135
|
+
while i.contents >= 0 && out->Array.length < limit {
|
|
136
|
+
let ev = events.contents->Array.getUnsafe(i.contents)
|
|
137
|
+
let position = ev.position->posToInt->Int.toFloat
|
|
138
|
+
let inBound = switch bound {
|
|
139
|
+
| Some(LocalEventLogRead.Below(p)) => position < p
|
|
140
|
+
| Some(AtMost(p)) => position <= p
|
|
141
|
+
| None => true
|
|
142
|
+
}
|
|
143
|
+
let inFlow = correlationId->Option.mapOr(true, id => ev.meta.correlationId == id)
|
|
144
|
+
if inBound && inFlow {
|
|
145
|
+
out->Array.push(
|
|
146
|
+
(
|
|
147
|
+
{
|
|
148
|
+
log: _name,
|
|
149
|
+
position,
|
|
150
|
+
eventType: ev.eventType,
|
|
151
|
+
tags: ev.tags,
|
|
152
|
+
payload: ev.data,
|
|
153
|
+
meta: ev.meta->Reventless.Util_Sury.toJson(Reventless.Message.metaSchema),
|
|
154
|
+
recordedAt: ev.recordedAt,
|
|
155
|
+
}: LocalEventLogRead.stored
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
i := i.contents - 1
|
|
160
|
+
}
|
|
161
|
+
out
|
|
162
|
+
})
|
|
163
|
+
|
|
131
164
|
let read = async (~query, ~after=?) => {
|
|
132
165
|
let filtered = matchingEvents(~query, ~after)
|
|
133
166
|
let headPosition =
|
|
@@ -165,6 +198,11 @@ let makeStorage = (~name as _name, ~indexes as _, ~partitionTag as _, ~opts as _
|
|
|
165
198
|
recordedAt: Message.nowAsISOString(),
|
|
166
199
|
}
|
|
167
200
|
indexEvent(base + j, stored)
|
|
201
|
+
LocalEventLogRead.note(
|
|
202
|
+
~log=_name,
|
|
203
|
+
~msgId=event.meta.msgId,
|
|
204
|
+
~position=position.contents->Int.toFloat,
|
|
205
|
+
)
|
|
168
206
|
stored
|
|
169
207
|
})
|
|
170
208
|
events := events.contents->Array.concat(storedEvents)
|
|
@@ -6,7 +6,10 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import * as Stream from "effect/Stream";
|
|
8
8
|
import * as Pulumi from "@pulumi/pulumi";
|
|
9
|
+
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
10
|
+
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
9
11
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
12
|
+
import * as LocalEventLogRead$ReventlessLocal from "../LocalEventLogRead.res.mjs";
|
|
10
13
|
|
|
11
14
|
function posToInt(pos) {
|
|
12
15
|
return Stdlib_Option.getOr(Stdlib_Int.fromString(pos, undefined), 0);
|
|
@@ -130,6 +133,31 @@ function makeStorage(_name, param, param$1, param$2) {
|
|
|
130
133
|
});
|
|
131
134
|
}
|
|
132
135
|
};
|
|
136
|
+
LocalEventLogRead$ReventlessLocal.register(_name, "Dcb", async (bound, limit, correlationId) => {
|
|
137
|
+
let out = [];
|
|
138
|
+
let i = events.contents.length - 1 | 0;
|
|
139
|
+
while (i >= 0 && out.length < limit) {
|
|
140
|
+
let ev = events.contents[i];
|
|
141
|
+
let position = posToInt(ev.position);
|
|
142
|
+
let inBound = bound !== undefined ? (
|
|
143
|
+
bound.TAG === "Below" ? position < bound._0 : position <= bound._0
|
|
144
|
+
) : true;
|
|
145
|
+
let inFlow = Stdlib_Option.mapOr(correlationId, true, id => ev.meta.correlationId === id);
|
|
146
|
+
if (inBound && inFlow) {
|
|
147
|
+
out.push({
|
|
148
|
+
log: _name,
|
|
149
|
+
position: position,
|
|
150
|
+
eventType: ev.eventType,
|
|
151
|
+
tags: ev.tags,
|
|
152
|
+
payload: ev.data,
|
|
153
|
+
meta: Util_Sury$Reventless.toJson(ev.meta, Message$Reventless.metaSchema),
|
|
154
|
+
recordedAt: ev.recordedAt
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
i = i - 1 | 0;
|
|
158
|
+
};
|
|
159
|
+
return out;
|
|
160
|
+
});
|
|
133
161
|
let read = async (query, after) => {
|
|
134
162
|
let filtered = matchingEvents(query, after);
|
|
135
163
|
let headPosition = Stdlib_Option.map(events.contents[events.contents.length - 1 | 0], e => e.position);
|
|
@@ -164,6 +192,7 @@ function makeStorage(_name, param, param$1, param$2) {
|
|
|
164
192
|
recordedAt: stored_recordedAt
|
|
165
193
|
};
|
|
166
194
|
indexEvent(base + j | 0, stored);
|
|
195
|
+
LocalEventLogRead$ReventlessLocal.note(_name, event.meta.msgId, position.contents);
|
|
167
196
|
return stored;
|
|
168
197
|
});
|
|
169
198
|
events.contents = events.contents.concat(storedEvents);
|
|
@@ -38,6 +38,11 @@ let ensureSchema = (db: SqliteDriver.t) => {
|
|
|
38
38
|
// IN (...)` batch). The `_by_kv` index leads with tag_key/tag_value, so it
|
|
39
39
|
// cannot answer a position lookup — without this the batch scanned dcb_tag.
|
|
40
40
|
db->SqliteDriver.exec("CREATE INDEX IF NOT EXISTS dcb_tag_by_pos ON dcb_tag(log_name, position)")
|
|
41
|
+
// Finding every event of one flow in one log. Created on open, so an existing store gains
|
|
42
|
+
// it; a query must use this exact expression for the index to apply.
|
|
43
|
+
db->SqliteDriver.exec(
|
|
44
|
+
"CREATE INDEX IF NOT EXISTS dcb_event_by_correlation ON dcb_event(log_name, json_extract(meta, '$.correlationId'), position)",
|
|
45
|
+
)
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
let posToInt = (pos: string) => pos->Int.fromString->Option.getOr(0)
|
|
@@ -157,6 +162,81 @@ let buildQuerySql = (~logName: string, ~query: DcbTag.query, ~after: option<int>
|
|
|
157
162
|
)
|
|
158
163
|
}
|
|
159
164
|
|
|
165
|
+
/** The SQL of one backwards read, and its parameters. */
|
|
166
|
+
let readBackSql = (
|
|
167
|
+
~name: string,
|
|
168
|
+
~bound: option<LocalEventLogRead.bound>,
|
|
169
|
+
~limit: int,
|
|
170
|
+
~correlationId: option<string>,
|
|
171
|
+
): (string, array<JSON.t>) => {
|
|
172
|
+
let parts = ["log_name = ?"]
|
|
173
|
+
let params = [JSON.Encode.string(name)]
|
|
174
|
+
switch bound {
|
|
175
|
+
| Some(LocalEventLogRead.Below(p)) =>
|
|
176
|
+
parts->Array.push("position < ?")
|
|
177
|
+
params->Array.push(JSON.Encode.float(p))
|
|
178
|
+
| Some(AtMost(p)) =>
|
|
179
|
+
parts->Array.push("position <= ?")
|
|
180
|
+
params->Array.push(JSON.Encode.float(p))
|
|
181
|
+
| None => ()
|
|
182
|
+
}
|
|
183
|
+
switch correlationId {
|
|
184
|
+
| Some(id) =>
|
|
185
|
+
parts->Array.push("json_extract(meta, '$.correlationId') = ?")
|
|
186
|
+
params->Array.push(JSON.Encode.string(id))
|
|
187
|
+
| None => ()
|
|
188
|
+
}
|
|
189
|
+
params->Array.push(JSON.Encode.int(limit))
|
|
190
|
+
(
|
|
191
|
+
`SELECT position, event_type, data, meta, recorded_at FROM dcb_event WHERE ${parts->Array.join(
|
|
192
|
+
" AND ",
|
|
193
|
+
)} ORDER BY position DESC LIMIT ?`,
|
|
194
|
+
params,
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Reads one log newest first by position, with each event's tags. */
|
|
199
|
+
let readBack = (~db: SqliteDriver.t, ~name: string): LocalEventLogRead.readBack =>
|
|
200
|
+
async (~bound, ~limit, ~correlationId) => {
|
|
201
|
+
let (sql, params) = readBackSql(~name, ~bound, ~limit, ~correlationId)
|
|
202
|
+
let rows = db->SqliteDriver.prepare(sql)->SqliteDriver.all(params)
|
|
203
|
+
let positions =
|
|
204
|
+
rows->Array.map(row => row->Dict.get("position")->Option.mapOr(0, coercePosition))
|
|
205
|
+
let tagsByPos: Dict.t<array<DcbTag.tag>> = Dict.make()
|
|
206
|
+
if positions->Array.length > 0 {
|
|
207
|
+
let placeholders = positions->Array.map(_ => "?")->Array.join(",")
|
|
208
|
+
db
|
|
209
|
+
->SqliteDriver.prepare(
|
|
210
|
+
`SELECT position, tag_key, tag_value FROM dcb_tag WHERE log_name = ? AND position IN (${placeholders}) ORDER BY position ASC, rowid ASC`,
|
|
211
|
+
)
|
|
212
|
+
->SqliteDriver.all(
|
|
213
|
+
[JSON.Encode.string(name)]->Array.concat(positions->Array.map(JSON.Encode.int)),
|
|
214
|
+
)
|
|
215
|
+
->Array.forEach(row => {
|
|
216
|
+
let k = row->Dict.get("position")->Option.mapOr(0, coercePosition)->Int.toString
|
|
217
|
+
let str = key => row->Dict.get(key)->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
218
|
+
let tag: DcbTag.tag = {key: str("tag_key"), value: str("tag_value")}
|
|
219
|
+
switch tagsByPos->Dict.get(k) {
|
|
220
|
+
| Some(list) => list->Array.push(tag)
|
|
221
|
+
| None => tagsByPos->Dict.set(k, [tag])
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
rows->Array.map((row): LocalEventLogRead.stored => {
|
|
226
|
+
let position = row->Dict.get("position")->Option.mapOr(0, coercePosition)
|
|
227
|
+
let str = key => row->Dict.get(key)->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
228
|
+
{
|
|
229
|
+
log: name,
|
|
230
|
+
position: position->Int.toFloat,
|
|
231
|
+
eventType: str("event_type"),
|
|
232
|
+
tags: tagsByPos->Dict.get(position->Int.toString)->Option.getOr([]),
|
|
233
|
+
payload: parseDataPayload(str("data")),
|
|
234
|
+
meta: parseDataPayload(str("meta")),
|
|
235
|
+
recordedAt: str("recorded_at"),
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
160
240
|
let makeStorage = (
|
|
161
241
|
~db: SqliteDriver.t,
|
|
162
242
|
~name: string,
|
|
@@ -165,6 +245,7 @@ let makeStorage = (
|
|
|
165
245
|
~opts as _,
|
|
166
246
|
) => {
|
|
167
247
|
ensureSchema(db)
|
|
248
|
+
LocalEventLogRead.register(~log=name, ~kind=Dcb, readBack(~db, ~name))
|
|
168
249
|
|
|
169
250
|
let insertEventStmt =
|
|
170
251
|
db->SqliteDriver.prepare(
|
|
@@ -327,6 +408,8 @@ let makeStorage = (
|
|
|
327
408
|
// each event insert — before the tag inserts move last_insert_rowid() to
|
|
328
409
|
// the dcb_tag table. Registered only after the transaction commits.
|
|
329
410
|
let appended: array<(string, int)> = []
|
|
411
|
+
// (msgId, position) per inserted event, for the tap line that publishes it.
|
|
412
|
+
let positionsOf: array<(string, int)> = []
|
|
330
413
|
try {
|
|
331
414
|
db->SqliteDriver.transaction(() => {
|
|
332
415
|
switch condition {
|
|
@@ -371,12 +454,16 @@ let makeStorage = (
|
|
|
371
454
|
])
|
|
372
455
|
},
|
|
373
456
|
)
|
|
457
|
+
positionsOf->Array.push((event.meta.msgId, pos))
|
|
374
458
|
lastPos := pos
|
|
375
459
|
})
|
|
376
460
|
|
|
377
461
|
result := Ok(Int.toString(lastPos.contents))
|
|
378
462
|
})
|
|
379
463
|
ProjectionPending.trackAppended(~axis=ProjectionPending.Dcb, appended)
|
|
464
|
+
positionsOf->Array.forEach(((msgId, pos)) =>
|
|
465
|
+
LocalEventLogRead.note(~log=name, ~msgId, ~position=pos->Int.toFloat)
|
|
466
|
+
)
|
|
380
467
|
result.contents
|
|
381
468
|
} catch {
|
|
382
469
|
// The deliberate append-condition failure throws Failure("conflict: …").
|