@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +10 -10
  3. package/src/LocalPlatformRegistry.res +26 -1
  4. package/src/LocalPlatformRegistry.res.mjs +25 -5
  5. package/src/Platform.res +27 -2
  6. package/src/Platform.res.mjs +24 -4
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +38 -0
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +29 -0
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +87 -0
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +76 -0
  11. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +8 -1
  12. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +17 -4
  13. package/src/adapter/EventLog/EventLogStorage_InMemory.res +49 -2
  14. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +57 -21
  15. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +85 -0
  16. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +50 -0
  17. package/src/adapter/EventLog/LocalEventLogStorage.res +3 -0
  18. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +6 -2
  19. package/src/adapter/LocalBus.res +32 -5
  20. package/src/adapter/LocalBus.res.mjs +27 -7
  21. package/src/adapter/LocalEventLogRead.res +477 -0
  22. package/src/adapter/LocalEventLogRead.res.mjs +507 -0
  23. package/src/adapter/LocalEventTap.res +18 -80
  24. package/src/adapter/LocalEventTap.res.mjs +19 -108
  25. package/src/adapter/LocalFeed.res +433 -0
  26. package/src/adapter/LocalFeed.res.mjs +419 -0
  27. package/src/adapter/LocalLogFeed.res +217 -0
  28. package/src/adapter/LocalLogFeed.res.mjs +295 -0
  29. package/src/adapter/LocalPgEventLogRead.res +85 -0
  30. package/src/adapter/LocalPgEventLogRead.res.mjs +91 -0
  31. package/src/adapter/LocalPlatformObserve.res +456 -0
  32. package/src/adapter/LocalPlatformObserve.res.mjs +505 -0
  33. package/src/adapter/LocalTapLines.res +149 -0
  34. package/src/adapter/LocalTapLines.res.mjs +232 -0
  35. package/src/adapter/PlatformGraphQL_Server.res +36 -3
  36. package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
  37. package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
  38. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  39. package/src/components/LocalAutomationSchedules.res.mjs +1 -1
  40. package/src/components/LocalAutomationSweep.res.mjs +1 -1
  41. package/src/reexport/CommandTopic.res.mjs +5 -2
  42. package/tests/LocalPlatformRegistryTest.res +17 -0
  43. package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
  44. package/tests/LocalPlatformStartTest.res.mjs +1 -1
  45. package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
  46. package/tests/adapter/LocalEventLogReadTest.res +539 -0
  47. package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
  48. package/tests/adapter/LocalFeedTest.res +314 -0
  49. package/tests/adapter/LocalFeedTest.res.mjs +329 -0
  50. package/tests/adapter/LocalPlatformObserveTest.res +165 -0
  51. package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
  52. package/tests/adapter/LocalTapLinesTest.res +207 -0
  53. package/tests/adapter/LocalTapLinesTest.res.mjs +274 -0
  54. package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
  55. package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
  56. package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
  57. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
  58. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
  59. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
  60. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
  61. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
  62. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
  63. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
  64. 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.317",
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.29",
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/rescript-node": "2.0.0-alpha.24",
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-core": "3.0.0-alpha.325",
44
- "@reventlessdev/reventless-infra": "3.0.0-alpha.220",
45
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.274",
46
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.173",
47
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.189",
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 register(port, endpoint, loginEndpoint, store, tapPort) {
156
- let path = write(port, endpoint, loginEndpoint, store, tapPort, undefined, undefined);
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
- EventHistoryResolvers.register(~server=resolveTargetGraphQL(), params),
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
@@ -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 => EventHistoryResolvers.register(resolveTargetGraphQL(), 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 => EventHistoryResolvers.register(resolveTargetGraphQL(), 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: …").