@reventlessdev/reventless-local 3.0.0-alpha.317 → 3.0.0-alpha.318

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 (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +10 -10
  3. package/src/LocalPlatformRegistry.res +19 -0
  4. package/src/LocalPlatformRegistry.res.mjs +19 -1
  5. package/src/Platform.res +24 -2
  6. package/src/Platform.res.mjs +22 -2
  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 +147 -0
  34. package/src/adapter/LocalTapLines.res.mjs +229 -0
  35. package/src/adapter/PlatformGraphQL_Server.res +36 -3
  36. package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
  37. package/src/reexport/CommandTopic.res.mjs +5 -2
  38. package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
  39. package/tests/adapter/LocalEventLogReadTest.res +539 -0
  40. package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
  41. package/tests/adapter/LocalFeedTest.res +314 -0
  42. package/tests/adapter/LocalFeedTest.res.mjs +329 -0
  43. package/tests/adapter/LocalPlatformObserveTest.res +165 -0
  44. package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
  45. package/tests/adapter/LocalTapLinesTest.res +182 -0
  46. package/tests/adapter/LocalTapLinesTest.res.mjs +242 -0
  47. package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
  48. package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
  49. package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
  50. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
  51. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
  52. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
  53. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
  54. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
  55. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
  56. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
  57. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePiiTest.res.mjs +7 -7
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.318 (2026-11-01)
7
+
8
+ ### Features
9
+
10
+ * **core:** a command's outcome, the flow through automations, and each view change are told ([dcaeed2](https://github.com/ReventlessDev/reventless-core/commit/dcaeed2fabe2047a21797b459f2931618474dec3))
11
+ * **local:** the local platform serves its logs and events to whoever watches it ([0c29b0d](https://github.com/ReventlessDev/reventless-core/commit/0c29b0d08c668ad5b948a8017178dad8692647d0))
12
+
13
+
6
14
  # 3.0.0-alpha.317 (2026-10-31)
7
15
 
8
16
  **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.318",
4
4
  "description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -34,19 +34,19 @@
34
34
  "graphql-yoga": "^5.21.0",
35
35
  "sury": "11.0.0",
36
36
  "ws": "^8.18.0",
37
+ "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.30",
37
38
  "@reventlessdev/rescript-effect": "1.0.0-alpha.1",
38
- "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
39
39
  "@reventlessdev/rescript-jest": "1.0.0-alpha.13",
40
40
  "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
41
- "@reventlessdev/rescript-node": "2.0.0-alpha.24",
41
+ "@reventlessdev/rescript-node": "2.0.0-alpha.25",
42
42
  "@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"
43
+ "@reventlessdev/reventless-core": "3.0.0-alpha.326",
44
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.275",
45
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.221",
46
+ "@reventlessdev/reventless-seed": "1.0.0-alpha.40",
47
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.174",
48
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.192",
49
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.190"
50
50
  },
51
51
  "devDependencies": {
52
52
  "rescript": "12.3.1",
@@ -27,6 +27,9 @@ 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,
30
33
  }
31
34
 
32
35
  // Threaded rather than read at each use, so tests need not move the process.
@@ -159,6 +162,22 @@ let publishTapPort = (~port: int, ~tapPort: int, ~cwd=NodeProcess.cwd()): unit =
159
162
  }
160
163
  }
161
164
 
165
+ /** Adds the log feed's port to an entry already written, once its socket is
166
+ bound; see {!publishTapPort}. */
167
+ let publishLogPort = (~port: int, ~logPort: int, ~cwd=NodeProcess.cwd()): unit => {
168
+ let path = entryPath(~port, ~cwd)
169
+ switch readEntry(path) {
170
+ | None => ()
171
+ | Some(entry) =>
172
+ try NodeFs.writeFileSync(
173
+ path,
174
+ {...entry, logPort}->Reventless.Util_Sury.toJson(entrySchema)->JSON.stringify(~space=2),
175
+ ) catch {
176
+ | _ => ()
177
+ }
178
+ }
179
+ }
180
+
162
181
  /** Publishes this process, and unpublishes it on the way out. Both paths are
163
182
  needed: `exit` is sync-only, and registering a signal handler suppresses
164
183
  Node's default exit, so each signal must leave explicitly. */
@@ -22,7 +22,8 @@ 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))
26
27
  }));
27
28
 
28
29
  function runningDir(cwdOpt, param) {
@@ -152,6 +153,22 @@ function publishTapPort(port, tapPort, cwdOpt) {
152
153
  }
153
154
  }
154
155
 
156
+ function publishLogPort(port, logPort, cwdOpt) {
157
+ let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
158
+ let path = entryPath(port, cwd);
159
+ let entry = readEntry(path);
160
+ if (entry === undefined) {
161
+ return;
162
+ }
163
+ try {
164
+ let newrecord = {...entry};
165
+ Nodefs.writeFileSync(path, JSON.stringify(Util_Sury$Reventless.toJson((newrecord.logPort = logPort, newrecord), entrySchema), undefined, 2), "utf8");
166
+ return;
167
+ } catch (exn) {
168
+ return;
169
+ }
170
+ }
171
+
155
172
  function register(port, endpoint, loginEndpoint, store, tapPort) {
156
173
  let path = write(port, endpoint, loginEndpoint, store, tapPort, undefined, undefined);
157
174
  process.on("exit", param => removeQuietly(path));
@@ -177,6 +194,7 @@ export {
177
194
  list,
178
195
  write,
179
196
  publishTapPort,
197
+ publishLogPort,
180
198
  register,
181
199
  }
182
200
  /* 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: ({
@@ -1418,6 +1430,11 @@ module MakeWithConfig = (
1418
1430
  ~onBound=tapPort => LocalPlatformRegistry.publishTapPort(~port=domainPort, ~tapPort),
1419
1431
  (),
1420
1432
  )
1433
+ // The log feed, the same way: its port is filled in once it is bound.
1434
+ LocalLogFeed.start(
1435
+ ~onBound=logPort => LocalPlatformRegistry.publishLogPort(~port=domainPort, ~logPort),
1436
+ (),
1437
+ )
1421
1438
  // Fire onPlatformDeployed after all servers are started so late-deployed
1422
1439
  // plugins (e.g. PlatformInspector) have their handler refs populated.
1423
1440
  ReventlessCore.Plugin_Helpers.firePlatformDeployedHook({
@@ -2113,6 +2130,11 @@ module MakeWithConfig = (
2113
2130
 
2114
2131
  platformGraphQL.registerQueries(~sdlFields=baseParts.queries, ~resolvers=queryResolvers)
2115
2132
 
2133
+ // What a tool watching this platform reads: every event of every log, and
2134
+ // the log level while running.
2135
+ LocalPlatformObserve.registerGraphQL(platformGraphQL)
2136
+ LocalPlatformObserve.registerMcp(platformMCP)
2137
+
2116
2138
  // Admin Plugin lifecycle mutations (Activate / Deactivate / Retire) now flow
2117
2139
  // through the LocalPluginAggregate, mirroring AWS. The resolver looks up the
2118
2140
  // 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
  };
@@ -1169,6 +1178,7 @@ function MakeWithConfig(Config) {
1169
1178
  path: match[1]
1170
1179
  }, undefined);
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
  };
@@ -2958,6 +2975,7 @@ function Make($star) {
2958
2975
  path: match[1]
2959
2976
  }, undefined);
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: …").
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
5
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
7
  import * as Effect from "effect/Effect";
@@ -13,6 +14,7 @@ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_excep
13
14
  import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
14
15
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
15
16
  import * as SqliteDriver$ReventlessLocal from "../SqliteDriver.res.mjs";
17
+ import * as LocalEventLogRead$ReventlessLocal from "../LocalEventLogRead.res.mjs";
16
18
  import * as ProjectionPending$ReventlessLocal from "../ProjectionPending.res.mjs";
17
19
 
18
20
  function ensureSchema(db) {
@@ -20,6 +22,7 @@ function ensureSchema(db) {
20
22
  SqliteDriver$ReventlessLocal.exec(db, "CREATE TABLE IF NOT EXISTS dcb_tag (log_name TEXT NOT NULL, position INTEGER NOT NULL, tag_key TEXT NOT NULL, tag_value TEXT NOT NULL)");
21
23
  SqliteDriver$ReventlessLocal.exec(db, "CREATE INDEX IF NOT EXISTS dcb_tag_by_kv ON dcb_tag(log_name, tag_key, tag_value, position)");
22
24
  SqliteDriver$ReventlessLocal.exec(db, "CREATE INDEX IF NOT EXISTS dcb_tag_by_pos ON dcb_tag(log_name, position)");
25
+ SqliteDriver$ReventlessLocal.exec(db, "CREATE INDEX IF NOT EXISTS dcb_event_by_correlation ON dcb_event(log_name, json_extract(meta, '$.correlationId'), position)");
23
26
  }
24
27
 
25
28
  function posToInt(pos) {
@@ -128,8 +131,73 @@ function buildQuerySql(logName, query, after) {
128
131
  ];
129
132
  }
130
133
 
134
+ function readBackSql(name, bound, limit, correlationId) {
135
+ let parts = ["log_name = ?"];
136
+ let params = [name];
137
+ if (bound !== undefined) {
138
+ if (bound.TAG === "Below") {
139
+ parts.push("position < ?");
140
+ params.push(bound._0);
141
+ } else {
142
+ parts.push("position <= ?");
143
+ params.push(bound._0);
144
+ }
145
+ }
146
+ if (correlationId !== undefined) {
147
+ parts.push("json_extract(meta, '$.correlationId') = ?");
148
+ params.push(correlationId);
149
+ }
150
+ params.push(limit);
151
+ return [
152
+ `SELECT position, event_type, data, meta, recorded_at FROM dcb_event WHERE ` + parts.join(" AND ") + ` ORDER BY position DESC LIMIT ?`,
153
+ params
154
+ ];
155
+ }
156
+
157
+ function readBack(db, name) {
158
+ return async (bound, limit, correlationId) => {
159
+ let match = readBackSql(name, bound, limit, correlationId);
160
+ let rows = SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, match[0]), match[1]);
161
+ let positions = rows.map(row => Stdlib_Option.mapOr(row["position"], 0, coercePosition));
162
+ let tagsByPos = {};
163
+ if (positions.length !== 0) {
164
+ let placeholders = positions.map(param => "?").join(",");
165
+ SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, `SELECT position, tag_key, tag_value FROM dcb_tag WHERE log_name = ? AND position IN (` + placeholders + `) ORDER BY position ASC, rowid ASC`), [name].concat(positions.map(prim => prim))).forEach(row => {
166
+ let k = Stdlib_Option.mapOr(row["position"], 0, coercePosition).toString();
167
+ let str = key => Stdlib_Option.getOr(Stdlib_Option.flatMap(row[key], Stdlib_JSON.Decode.string), "");
168
+ let tag_key = str("tag_key");
169
+ let tag_value = str("tag_value");
170
+ let tag = {
171
+ key: tag_key,
172
+ value: tag_value
173
+ };
174
+ let list = tagsByPos[k];
175
+ if (list !== undefined) {
176
+ list.push(tag);
177
+ } else {
178
+ tagsByPos[k] = [tag];
179
+ }
180
+ });
181
+ }
182
+ return rows.map(row => {
183
+ let position = Stdlib_Option.mapOr(row["position"], 0, coercePosition);
184
+ let str = key => Stdlib_Option.getOr(Stdlib_Option.flatMap(row[key], Stdlib_JSON.Decode.string), "");
185
+ return {
186
+ log: name,
187
+ position: position,
188
+ eventType: str("event_type"),
189
+ tags: Stdlib_Option.getOr(tagsByPos[position.toString()], []),
190
+ payload: parseDataPayload(str("data")),
191
+ meta: parseDataPayload(str("meta")),
192
+ recordedAt: str("recorded_at")
193
+ };
194
+ });
195
+ };
196
+ }
197
+
131
198
  function makeStorage(db, name, param, param$1, param$2) {
132
199
  ensureSchema(db);
200
+ LocalEventLogRead$ReventlessLocal.register(name, "Dcb", readBack(db, name));
133
201
  let insertEventStmt = SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO dcb_event(log_name, position, event_type, data, meta, recorded_at) VALUES(?,?,?,?,?,?)");
134
202
  let lastRowidStmt = SqliteDriver$ReventlessLocal.prepare(db, "SELECT last_insert_rowid() AS r");
135
203
  let insertTagStmt = SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO dcb_tag(log_name, position, tag_key, tag_value) VALUES(?,?,?,?)");
@@ -263,6 +331,7 @@ function makeStorage(db, name, param, param$1, param$2) {
263
331
  }
264
332
  };
265
333
  let appended = [];
334
+ let positionsOf = [];
266
335
  try {
267
336
  SqliteDriver$ReventlessLocal.transaction(db, () => {
268
337
  if (condition !== undefined) {
@@ -310,6 +379,10 @@ function makeStorage(db, name, param, param$1, param$2) {
310
379
  tag.key,
311
380
  tag.value
312
381
  ]));
382
+ positionsOf.push([
383
+ event.meta.msgId,
384
+ pos
385
+ ]);
313
386
  lastPos.contents = pos;
314
387
  });
315
388
  result.contents = {
@@ -318,6 +391,7 @@ function makeStorage(db, name, param, param$1, param$2) {
318
391
  };
319
392
  });
320
393
  ProjectionPending$ReventlessLocal.trackAppended("Dcb", appended);
394
+ positionsOf.forEach(param => LocalEventLogRead$ReventlessLocal.note(name, param[0], param[1]));
321
395
  return result.contents;
322
396
  } catch (raw_msg) {
323
397
  let msg = Primitive_exceptions.internalToException(raw_msg);
@@ -380,6 +454,8 @@ export {
380
454
  buildClauseSql,
381
455
  buildQueryWhere,
382
456
  buildQuerySql,
457
+ readBackSql,
458
+ readBack,
383
459
  makeStorage,
384
460
  }
385
461
  /* Stdlib_JsExn Not a pure module */
@@ -37,7 +37,14 @@ module Make = (Bus: LocalBus.T) => {
37
37
  ~opts,
38
38
  )
39
39
  Bus.registerDcbEventLogRead(storageName, ops.read)
40
- storage
40
+ LocalPgEventLogRead.registerDcb(~pool, ~name=storageName)
41
+ {
42
+ ReventlessCore.DcbEventLog_Adapter.resources: storage.resources,
43
+ operations: Pulumi.Output.make({
44
+ ...ops,
45
+ append: LocalPgEventLogRead.notingDcbAppend(~pool, ~name=storageName, ops.append),
46
+ }),
47
+ }
41
48
  | (None, None) =>
42
49
  let (storageName, read, storage) = DcbEventLogStorage_InMemory.makeStorage(
43
50
  ~name,