@reventlessdev/reventless-local 3.0.0-alpha.117 → 3.0.0-alpha.119

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 (66) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +8 -7
  3. package/rescript.json +2 -1
  4. package/src/Platform.res +44 -6
  5. package/src/Platform.res.mjs +53 -16
  6. package/src/adapter/Backend.res +23 -0
  7. package/src/adapter/Backend.res.mjs +23 -0
  8. package/src/adapter/BackendState.res +12 -7
  9. package/src/adapter/BackendState.res.mjs +18 -6
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +3 -22
  11. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +1 -22
  12. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +13 -10
  13. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +22 -8
  14. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +52 -0
  15. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +35 -0
  16. package/src/adapter/EventLog/EventLogStorage_InMemory.res +3 -15
  17. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +0 -21
  18. package/src/adapter/EventLog/LocalEventLogStorage.res +33 -0
  19. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +35 -0
  20. package/src/adapter/LocalBus.res +1 -1
  21. package/src/adapter/PgProjectionCatchup.res +146 -0
  22. package/src/adapter/PgProjectionCatchup.res.mjs +114 -0
  23. package/src/adapter/QueryDb/LocalQueryDbStorage.res +43 -0
  24. package/src/adapter/QueryDb/LocalQueryDbStorage.res.mjs +42 -0
  25. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +3 -24
  26. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +1 -28
  27. package/src/adapter/Task/LocalTaskBucket.res +1 -1
  28. package/src/adapter/Task/LocalTaskBucket.res.mjs +1 -1
  29. package/src/components/Aggregate_Builder.res +1 -1
  30. package/src/components/Aggregate_Builder.res.mjs +3 -3
  31. package/src/components/AutomationSlice_Builder.res +1 -1
  32. package/src/components/AutomationSlice_Builder.res.mjs +3 -3
  33. package/src/components/Counter_Builder.res +1 -1
  34. package/src/components/Counter_Builder.res.mjs +2 -2
  35. package/src/components/DcbEventLog_Builder.res +1 -1
  36. package/src/components/DcbEventLog_Builder.res.mjs +2 -2
  37. package/src/components/InboundTranslationSlice_Builder.res +1 -1
  38. package/src/components/InboundTranslationSlice_Builder.res.mjs +2 -2
  39. package/src/components/OutboundTranslationSlice_Builder.res +1 -1
  40. package/src/components/OutboundTranslationSlice_Builder.res.mjs +2 -2
  41. package/src/components/Plugin_Builder.res +1 -1
  42. package/src/components/Plugin_Builder.res.mjs +2 -2
  43. package/src/components/ReadModel_Builder.res +1 -1
  44. package/src/components/ReadModel_Builder.res.mjs +2 -2
  45. package/src/components/StateViewSlice_Builder.res +1 -1
  46. package/src/components/StateViewSlice_Builder.res.mjs +2 -2
  47. package/src/test/Mocks/MockDcbEventLogStorage.res +2 -2
  48. package/src/test/Mocks/MockDcbEventLogStorage.res.mjs +5 -2
  49. package/tests/adapter/BackendParityTest.res +43 -8
  50. package/tests/adapter/BackendParityTest.res.mjs +39 -9
  51. package/tests/adapter/DcbEventLogStorageSqliteTest.res +17 -9
  52. package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +19 -10
  53. package/tests/adapter/PgProjectionCatchupTest.res +149 -0
  54. package/tests/adapter/PgProjectionCatchupTest.res.mjs +181 -0
  55. package/tests/adapter/QueryDbGsiTtlTest.res +2 -2
  56. package/tests/adapter/QueryDbGsiTtlTest.res.mjs +3 -2
  57. package/tests/adapter/QueryDbListResolverTest.res +1 -1
  58. package/tests/adapter/QueryDbListResolverTest.res.mjs +2 -2
  59. package/tests/adapter/QueryDbStorageTest.res +10 -10
  60. package/tests/adapter/QueryDbStorageTest.res.mjs +11 -11
  61. package/tests/adapter/QueryEngineTest.res +8 -8
  62. package/tests/adapter/QueryEngineTest.res.mjs +9 -9
  63. package/tests/components/querydb/QueryDbFixtures.res +1 -1
  64. package/tests/components/querydb/QueryDbFixtures.res.mjs +2 -2
  65. package/tests/components/querydb/QueryDbSubIdFixtures.res +1 -1
  66. package/tests/components/querydb/QueryDbSubIdFixtures.res.mjs +2 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
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.119 (2026-07-05)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-local
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.118 (2026-07-05)
15
+
16
+ ### Features
17
+
18
+ * **postgres:** add reventless-postgres backend + local-platform integration ([6913200](https://github.com/ReventlessDev/reventless-core/commit/69132001f9271e832a5af33416acd5b645feaf47))
19
+
20
+
6
21
  # 3.0.0-alpha.117 (2026-07-04)
7
22
 
8
23
  **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.117",
3
+ "version": "3.0.0-alpha.119",
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
  "jest": {
@@ -31,15 +31,16 @@
31
31
  "graphql-yoga": "^5.21.0",
32
32
  "sury": "11.0.0-alpha.4",
33
33
  "ws": "^8.18.0",
34
+ "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.17",
34
35
  "@reventlessdev/rescript-effect": "0.1.0-alpha.24",
36
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.16",
35
37
  "@reventlessdev/rescript-jest": "1.0.0-alpha.5",
36
- "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.17",
37
38
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
38
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.16",
39
- "@reventlessdev/reventless-core": "3.0.0-alpha.138",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.64",
41
- "@reventlessdev/reventless-infra": "3.0.0-alpha.84",
42
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.78"
39
+ "@reventlessdev/reventless-core": "3.0.0-alpha.140",
40
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.80",
41
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.2",
42
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.86",
43
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.64"
43
44
  },
44
45
  "devDependencies": {
45
46
  "rescript": "^12.3.0",
package/rescript.json CHANGED
@@ -32,7 +32,8 @@
32
32
  "@reventlessdev/reventless-infra",
33
33
  "@reventlessdev/reventless-spec",
34
34
  "@reventlessdev/reventless-core",
35
- "@reventlessdev/reventless-gwt"
35
+ "@reventlessdev/reventless-gwt",
36
+ "@reventlessdev/reventless-postgres"
36
37
  ],
37
38
  "compiler-flags": [],
38
39
  "suffix": ".res.mjs"
package/src/Platform.res CHANGED
@@ -79,6 +79,22 @@ module MakeWithConfig = (
79
79
  // SQLite handle. resetOnStart truncates the file at construction.
80
80
  let _ = switch Config.backend {
81
81
  | Backend.Memory => BackendState.setMemory()
82
+ | Backend.Postgres({pool, initialCount}) =>
83
+ // Async setup (connect + ensureSchema + count) already ran in the
84
+ // Backend.postgres smart constructor — the sync functor only stores the
85
+ // ready pool and seeds the event-tap counter from the persisted totals.
86
+ BackendState.setPostgres(~pool)
87
+ LocalBus.seedEventTapSeq(initialCount)
88
+ // SCOPE: Backend.Postgres persists the durable event logs (classic + DCB) via
89
+ // the reventless-postgres runtime. Read models use the in-memory live-query
90
+ // path — the LocalBus scan/stream registrations are synchronous, which async pg
91
+ // cannot satisfy without a broader bus refactor; the package's
92
+ // QueryDbStorage_Postgres remains available to deploy-time compute layers.
93
+ // Because read models are in-memory (empty on start), they are rebuilt on every
94
+ // startup by full-replaying the durable pg event log through the projection
95
+ // handlers (PgProjectionCatchup, wired below). The runtime ProjectionPending
96
+ // low-watermark (a SQLite-only persisted-checkpoint optimisation) stays off —
97
+ // full replay needs no checkpoint.
82
98
  | Backend.Sqlite({path, resetOnStart}) =>
83
99
  if resetOnStart {
84
100
  Backend.removeFileIfExists(path)
@@ -741,7 +757,7 @@ module MakeWithConfig = (
741
757
  LocalRuntimeEnvironment,
742
758
  EventCollectorChannel,
743
759
  ),
744
- DcbEventLogStorage_InMemory.Make(Bus),
760
+ LocalDcbEventLogStorage.Make(Bus),
745
761
  LocalEventTopicPublisher.Make(Bus),
746
762
  LocalCommandTopicChannel.Make(Bus),
747
763
  LocalCommandTopicChannel.Make(Bus),
@@ -799,7 +815,7 @@ module MakeWithConfig = (
799
815
  // Backend-aware storage for the UIFragment registry — the same functor every
800
816
  // other read model uses. Replaces a hand-rolled memory-only store that ignored
801
817
  // BackendState, so the registry now persists under SQLite too.
802
- module UIFragmentStorage = QueryDbStorage_InMemory.Make(Bus)
818
+ module UIFragmentStorage = LocalQueryDbStorage.Make(Bus)
803
819
 
804
820
  let ensureUIFragmentRegistryQueryDbStore = () => {
805
821
  switch uiFragmentQueryDbOpsRef.contents {
@@ -1274,6 +1290,7 @@ module MakeWithConfig = (
1274
1290
  ~comp="Platform",
1275
1291
  switch Config.backend {
1276
1292
  | Backend.Memory => "backend: memory"
1293
+ | Backend.Postgres({connection}) => `backend: postgres (${connection}, event-logs only)`
1277
1294
  | Backend.Sqlite({path, resetOnStart}) =>
1278
1295
  `backend: sqlite (${path}${resetOnStart ? ", resetOnStart" : ""})`
1279
1296
  },
@@ -1282,11 +1299,16 @@ module MakeWithConfig = (
1282
1299
  // BEFORE this session appends anything. Catch-up replays only (checkpoint,
1283
1300
  // bound] — this session's own events (Connect dispatches, user commands)
1284
1301
  // are live-delivered and must not be redelivered.
1285
- let projectionCatchup = BackendState.getDb()->Option.map(db => (
1302
+ let projectionCatchup = BackendState.getSqliteDb()->Option.map(db => (
1286
1303
  db,
1287
1304
  ProjectionCheckpoint.maxPosition(db, ProjectionPending.Aggregate),
1288
1305
  ProjectionCheckpoint.maxPosition(db, ProjectionPending.Dcb),
1289
1306
  ))
1307
+ // Postgres: read models are in-memory (rebuilt on every start), so capture the
1308
+ // pre-session head NOW — before plugins build or this session appends — and
1309
+ // full-replay (0, head] after the plugins register their projection handlers.
1310
+ let pgProjectionCatchup =
1311
+ BackendState.getPostgresPool()->Option.map(pool => (pool, PgProjectionCatchup.captureBounds(pool)))
1290
1312
 
1291
1313
  // Create scheduler and populate platform context refs.
1292
1314
  let scheduler = makeScheduler()
@@ -1389,8 +1411,8 @@ module MakeWithConfig = (
1389
1411
  seedUIFragmentRegistryQueryDb(~pluginComponents=plugins)
1390
1412
  seedPluginStructuresStore(~pluginComponents=plugins)
1391
1413
  }
1392
- switch projectionCatchup {
1393
- | Some((db, upperBound, dcbUpperBound)) =>
1414
+ switch (projectionCatchup, pgProjectionCatchup) {
1415
+ | (Some((db, upperBound, dcbUpperBound)), _) =>
1394
1416
  let _ =
1395
1417
  ProjectionCheckpoint.startupCatchup(~db, ~upperBound, ~dcbUpperBound, ~handlers=() =>
1396
1418
  Bus.projectionCatchupHandlers()
@@ -1403,7 +1425,23 @@ module MakeWithConfig = (
1403
1425
  seedAdminStores()
1404
1426
  Promise.resolve()
1405
1427
  })
1406
- | None => seedAdminStores()
1428
+ | (_, Some((pool, boundsPromise))) =>
1429
+ let _ =
1430
+ boundsPromise
1431
+ ->Promise.then(bounds =>
1432
+ PgProjectionCatchup.startupCatchup(~pool, ~bounds, ~handlers=() =>
1433
+ Bus.projectionCatchupHandlers()
1434
+ )
1435
+ )
1436
+ ->Promise.catch(e => {
1437
+ Console.error2("[Platform] postgres projection catch-up failed:", e)
1438
+ Promise.resolve()
1439
+ })
1440
+ ->Promise.then(() => {
1441
+ seedAdminStores()
1442
+ Promise.resolve()
1443
+ })
1444
+ | (None, None) => seedAdminStores()
1407
1445
  }
1408
1446
 
1409
1447
  // Seed the built-in admin plugin's structure so its Auto UI (Plugin list with
@@ -50,6 +50,8 @@ import * as ProjectionPending$ReventlessLocal from "./adapter/ProjectionPending.
50
50
  import * as ReadModel_Builder$ReventlessLocal from "./components/ReadModel_Builder.res.mjs";
51
51
  import * as PlatformMCP_Server$ReventlessLocal from "./adapter/PlatformMCP_Server.res.mjs";
52
52
  import * as Auth_GraphqlContext$ReventlessLocal from "./adapter/Auth/Auth_GraphqlContext.res.mjs";
53
+ import * as LocalQueryDbStorage$ReventlessLocal from "./adapter/QueryDb/LocalQueryDbStorage.res.mjs";
54
+ import * as PgProjectionCatchup$ReventlessLocal from "./adapter/PgProjectionCatchup.res.mjs";
53
55
  import * as PluginsReadModelSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs";
54
56
  import * as DomainGraphQL_Server$ReventlessLocal from "./adapter/DomainGraphQL_Server.res.mjs";
55
57
  import * as EventPublish_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventLog/EventPublish_Callback.res.mjs";
@@ -62,17 +64,16 @@ import * as PlatformGraphQL_Server$ReventlessLocal from "./adapter/PlatformGraph
62
64
  import * as Platform_UIFragmentsApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UIFragmentsApi.res.mjs";
63
65
  import * as StateViewSlice_Builder$ReventlessLocal from "./components/StateViewSlice_Builder.res.mjs";
64
66
  import * as AutomationSlice_Builder$ReventlessLocal from "./components/AutomationSlice_Builder.res.mjs";
67
+ import * as LocalDcbEventLogStorage$ReventlessLocal from "./adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs";
65
68
  import * as LocalRuntimeEnvironment$ReventlessLocal from "./adapter/Runtime/LocalRuntimeEnvironment.res.mjs";
66
69
  import * as LocalScheduledPublisher$ReventlessLocal from "./adapter/Scheduler/LocalScheduledPublisher.res.mjs";
67
70
  import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
68
- import * as QueryDbStorage_InMemory$ReventlessLocal from "./adapter/QueryDb/QueryDbStorage_InMemory.res.mjs";
69
71
  import * as LocalCommandTopicChannel$ReventlessLocal from "./adapter/CommandTopic/LocalCommandTopicChannel.res.mjs";
70
72
  import * as LocalEventTopicPublisher$ReventlessLocal from "./adapter/EventTopic/LocalEventTopicPublisher.res.mjs";
71
73
  import * as StateChangeSlice_Builder$ReventlessLocal from "./components/StateChangeSlice_Builder.res.mjs";
72
74
  import * as DcbEventLogStorage_Sqlite$ReventlessLocal from "./adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs";
73
75
  import * as LocalEventCollectorChannel$ReventlessLocal from "./adapter/EventCollector/LocalEventCollectorChannel.res.mjs";
74
76
  import * as PluginRuntime_Builder_Micro$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Runtime/PluginRuntime_Builder_Micro.res.mjs";
75
- import * as DcbEventLogStorage_InMemory$ReventlessLocal from "./adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs";
76
77
  import * as UIFragmentRegistryReadModelSpec$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UIFragmentRegistryReadModelSpec.res.mjs";
77
78
  import * as InboundTranslationSlice_Builder$ReventlessLocal from "./components/InboundTranslationSlice_Builder.res.mjs";
78
79
  import * as Platform_ComponentDefinitionsApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_ComponentDefinitionsApi.res.mjs";
@@ -110,7 +111,7 @@ function MakeWithConfig(Config) {
110
111
  let match = Config.backend;
111
112
  if (typeof match !== "object") {
112
113
  BackendState$ReventlessLocal.setMemory();
113
- } else {
114
+ } else if (match.TAG === "Sqlite") {
114
115
  let path = match.path;
115
116
  if (match.resetOnStart) {
116
117
  Backend$ReventlessLocal.removeFileIfExists(path);
@@ -124,6 +125,9 @@ function MakeWithConfig(Config) {
124
125
  let msgIds = metas !== undefined ? metas.map(m => m.msgId) : Stdlib_Array.filterMap(publishedEvent.eventsJson, json => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["msgId"]), Stdlib_JSON.Decode.string));
125
126
  return ProjectionCheckpoint$ReventlessLocal.completePublished(db, msgIds);
126
127
  });
128
+ } else {
129
+ BackendState$ReventlessLocal.setPostgres(match.pool);
130
+ LocalBus$ReventlessLocal.seedEventTapSeq(match.initialCount);
127
131
  }
128
132
  let Bus = LocalBus$ReventlessLocal.Impl({
129
133
  capacity: undefined,
@@ -704,7 +708,7 @@ function MakeWithConfig(Config) {
704
708
  extractCorrelationId: LocalRuntimeEnvironment$ReventlessLocal.extractCorrelationId,
705
709
  asEventHandler: prim => prim,
706
710
  asEffectHandler: prim => prim
707
- })(EventCollectorChannel))(DcbEventLogStorage_InMemory$ReventlessLocal.Make(Bus))(LocalEventTopicPublisher$ReventlessLocal.Make(Bus))({
711
+ })(EventCollectorChannel))(LocalDcbEventLogStorage$ReventlessLocal.Make(Bus))(LocalEventTopicPublisher$ReventlessLocal.Make(Bus))({
708
712
  make: $$let$1.make
709
713
  })({
710
714
  make: $$let$2.make
@@ -730,7 +734,7 @@ function MakeWithConfig(Config) {
730
734
  let uiFragmentQueryDbOpsRef = {
731
735
  contents: undefined
732
736
  };
733
- let UIFragmentStorage = QueryDbStorage_InMemory$ReventlessLocal.Make(Bus);
737
+ let UIFragmentStorage = LocalQueryDbStorage$ReventlessLocal.Make(Bus);
734
738
  let ensureUIFragmentRegistryQueryDbStore = () => {
735
739
  let ops = uiFragmentQueryDbOpsRef.contents;
736
740
  if (ops !== undefined) {
@@ -1094,15 +1098,21 @@ function MakeWithConfig(Config) {
1094
1098
  log.info("Platform", undefined, `silent: ` + Stdlib_Bool.toString(Config.silent) + `, splitApi: ` + Stdlib_Bool.toString(Config.splitApi) + `, cloner: ` + Stdlib_Bool.toString(Config.cloner));
1095
1099
  let match = Config.backend;
1096
1100
  let tmp;
1097
- tmp = typeof match !== "object" ? "backend: memory" : `backend: sqlite (` + match.path + (
1098
- match.resetOnStart ? ", resetOnStart" : ""
1099
- ) + `)`;
1101
+ tmp = typeof match !== "object" ? "backend: memory" : (
1102
+ match.TAG === "Sqlite" ? `backend: sqlite (` + match.path + (
1103
+ match.resetOnStart ? ", resetOnStart" : ""
1104
+ ) + `)` : `backend: postgres (` + match.connection + `, event-logs only)`
1105
+ );
1100
1106
  log.info("Platform", undefined, tmp);
1101
- let projectionCatchup = Stdlib_Option.map(BackendState$ReventlessLocal.getDb(), db => [
1107
+ let projectionCatchup = Stdlib_Option.map(BackendState$ReventlessLocal.getSqliteDb(), db => [
1102
1108
  db,
1103
1109
  ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Aggregate"),
1104
1110
  ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Dcb")
1105
1111
  ]);
1112
+ let pgProjectionCatchup = Stdlib_Option.map(BackendState$ReventlessLocal.getPostgresPool(), pool => [
1113
+ pool,
1114
+ PgProjectionCatchup$ReventlessLocal.captureBounds(pool)
1115
+ ]);
1106
1116
  let scheduler = makeScheduler();
1107
1117
  hooks_scheduler.contents = scheduler;
1108
1118
  hooks_api.contents = {
@@ -1174,6 +1184,15 @@ function MakeWithConfig(Config) {
1174
1184
  seedAdminStores();
1175
1185
  return Promise.resolve();
1176
1186
  });
1187
+ } else if (pgProjectionCatchup !== undefined) {
1188
+ let pool = pgProjectionCatchup[0];
1189
+ Stdlib_Promise.$$catch(pgProjectionCatchup[1].then(bounds => PgProjectionCatchup$ReventlessLocal.startupCatchup(pool, bounds, () => Bus.projectionCatchupHandlers())), e => {
1190
+ console.error("[Platform] postgres projection catch-up failed:", e);
1191
+ return Promise.resolve();
1192
+ }).then(() => {
1193
+ seedAdminStores();
1194
+ return Promise.resolve();
1195
+ });
1177
1196
  } else {
1178
1197
  seedAdminStores();
1179
1198
  }
@@ -1725,7 +1744,7 @@ function Make($star) {
1725
1744
  TestRunner$ReventlessLocal.setup();
1726
1745
  if (typeof backend !== "object") {
1727
1746
  BackendState$ReventlessLocal.setMemory();
1728
- } else {
1747
+ } else if (backend.TAG === "Sqlite") {
1729
1748
  let path = backend.path;
1730
1749
  if (backend.resetOnStart) {
1731
1750
  Backend$ReventlessLocal.removeFileIfExists(path);
@@ -1739,6 +1758,9 @@ function Make($star) {
1739
1758
  let msgIds = metas !== undefined ? metas.map(m => m.msgId) : Stdlib_Array.filterMap(publishedEvent.eventsJson, json => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["msgId"]), Stdlib_JSON.Decode.string));
1740
1759
  return ProjectionCheckpoint$ReventlessLocal.completePublished(db, msgIds);
1741
1760
  });
1761
+ } else {
1762
+ BackendState$ReventlessLocal.setPostgres(backend.pool);
1763
+ LocalBus$ReventlessLocal.seedEventTapSeq(backend.initialCount);
1742
1764
  }
1743
1765
  let Bus = LocalBus$ReventlessLocal.Impl({
1744
1766
  capacity: undefined,
@@ -2312,7 +2334,7 @@ function Make($star) {
2312
2334
  extractCorrelationId: LocalRuntimeEnvironment$ReventlessLocal.extractCorrelationId,
2313
2335
  asEventHandler: prim => prim,
2314
2336
  asEffectHandler: prim => prim
2315
- })(EventCollectorChannel))(DcbEventLogStorage_InMemory$ReventlessLocal.Make(Bus))(LocalEventTopicPublisher$ReventlessLocal.Make(Bus))({
2337
+ })(EventCollectorChannel))(LocalDcbEventLogStorage$ReventlessLocal.Make(Bus))(LocalEventTopicPublisher$ReventlessLocal.Make(Bus))({
2316
2338
  make: $$let$1.make
2317
2339
  })({
2318
2340
  make: $$let$2.make
@@ -2338,7 +2360,7 @@ function Make($star) {
2338
2360
  let uiFragmentQueryDbOpsRef = {
2339
2361
  contents: undefined
2340
2362
  };
2341
- let UIFragmentStorage = QueryDbStorage_InMemory$ReventlessLocal.Make(Bus);
2363
+ let UIFragmentStorage = LocalQueryDbStorage$ReventlessLocal.Make(Bus);
2342
2364
  let ensureUIFragmentRegistryQueryDbStore = () => {
2343
2365
  let ops = uiFragmentQueryDbOpsRef.contents;
2344
2366
  if (ops !== undefined) {
@@ -2697,15 +2719,21 @@ function Make($star) {
2697
2719
  log.info("Platform", undefined, `v` + version);
2698
2720
  log.info("Platform", undefined, `silent: ` + Stdlib_Bool.toString(false) + `, splitApi: ` + Stdlib_Bool.toString(true) + `, cloner: ` + Stdlib_Bool.toString(false));
2699
2721
  let tmp;
2700
- tmp = typeof backend !== "object" ? "backend: memory" : `backend: sqlite (` + backend.path + (
2701
- backend.resetOnStart ? ", resetOnStart" : ""
2702
- ) + `)`;
2722
+ tmp = typeof backend !== "object" ? "backend: memory" : (
2723
+ backend.TAG === "Sqlite" ? `backend: sqlite (` + backend.path + (
2724
+ backend.resetOnStart ? ", resetOnStart" : ""
2725
+ ) + `)` : `backend: postgres (` + backend.connection + `, event-logs only)`
2726
+ );
2703
2727
  log.info("Platform", undefined, tmp);
2704
- let projectionCatchup = Stdlib_Option.map(BackendState$ReventlessLocal.getDb(), db => [
2728
+ let projectionCatchup = Stdlib_Option.map(BackendState$ReventlessLocal.getSqliteDb(), db => [
2705
2729
  db,
2706
2730
  ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Aggregate"),
2707
2731
  ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Dcb")
2708
2732
  ]);
2733
+ let pgProjectionCatchup = Stdlib_Option.map(BackendState$ReventlessLocal.getPostgresPool(), pool => [
2734
+ pool,
2735
+ PgProjectionCatchup$ReventlessLocal.captureBounds(pool)
2736
+ ]);
2709
2737
  let scheduler = makeScheduler();
2710
2738
  hooks_scheduler.contents = scheduler;
2711
2739
  hooks_api.contents = {
@@ -2777,6 +2805,15 @@ function Make($star) {
2777
2805
  seedAdminStores();
2778
2806
  return Promise.resolve();
2779
2807
  });
2808
+ } else if (pgProjectionCatchup !== undefined) {
2809
+ let pool = pgProjectionCatchup[0];
2810
+ Stdlib_Promise.$$catch(pgProjectionCatchup[1].then(bounds => PgProjectionCatchup$ReventlessLocal.startupCatchup(pool, bounds, () => Bus.projectionCatchupHandlers())), e => {
2811
+ console.error("[Platform] postgres projection catch-up failed:", e);
2812
+ return Promise.resolve();
2813
+ }).then(() => {
2814
+ seedAdminStores();
2815
+ return Promise.resolve();
2816
+ });
2780
2817
  } else {
2781
2818
  seedAdminStores();
2782
2819
  }
@@ -7,10 +7,33 @@
7
7
  type t =
8
8
  | Memory
9
9
  | Sqlite({path: string, resetOnStart: bool})
10
+ // Carries a live, schema-ensured pool: `pg` has no synchronous client, and
11
+ // Platform.MakeWithConfig is a synchronous functor, so the async setup
12
+ // (connect + ensureSchema + count) is done by the `postgres` smart constructor
13
+ // BEFORE Make. `initialCount` seeds the event-tap counter; `connection` is
14
+ // retained for diagnostics only.
15
+ | Postgres({pool: ReventlessPostgres.PgDriver.pool, initialCount: int, connection: string})
10
16
 
11
17
  let memory = Memory
12
18
  let sqlite = (~path, ~resetOnStart=false) => Sqlite({path, resetOnStart})
13
19
 
20
+ // Async constructor for the Postgres backend. Connects a pool, ensures the
21
+ // schema/functions exist, truncates when resetOnStart, and pre-counts persisted
22
+ // events. Await this before Platform.MakeWithConfig:
23
+ // let backend = await Backend.postgres(~connection="postgres://…")
24
+ // module Platform = Platform.MakeWithConfig({ …; let backend })
25
+ let postgres = async (~connection, ~resetOnStart=false) => {
26
+ let pool = ReventlessPostgres.PgDriver.makePool({connectionString: connection})
27
+ await ReventlessPostgres.PgSchema.ensureSchema(pool)
28
+ if resetOnStart {
29
+ await ReventlessPostgres.PgSchema.truncateAll(pool)
30
+ }
31
+ let initialCount =
32
+ (await ReventlessPostgres.EventLogStorage_Postgres.countAll(pool)) +
33
+ (await ReventlessPostgres.DcbEventLogStorage_Postgres.countAll(pool))
34
+ Postgres({pool, initialCount, connection})
35
+ }
36
+
14
37
  @val external processEnv: dict<string> = "process.env"
15
38
 
16
39
  // Deletes a file if it exists. No-op if the path is missing.
@@ -1,6 +1,10 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Nodefs from "node:fs";
4
+ import * as PgDriver$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgDriver.res.mjs";
5
+ import * as PgSchema$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgSchema.res.mjs";
6
+ import * as EventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/EventLogStorage_Postgres.res.mjs";
7
+ import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres.res.mjs";
4
8
 
5
9
  function sqlite(path, resetOnStartOpt) {
6
10
  let resetOnStart = resetOnStartOpt !== undefined ? resetOnStartOpt : false;
@@ -11,6 +15,24 @@ function sqlite(path, resetOnStartOpt) {
11
15
  };
12
16
  }
13
17
 
18
+ async function postgres(connection, resetOnStartOpt) {
19
+ let resetOnStart = resetOnStartOpt !== undefined ? resetOnStartOpt : false;
20
+ let pool = PgDriver$ReventlessPostgres.makePool({
21
+ connectionString: connection
22
+ });
23
+ await PgSchema$ReventlessPostgres.ensureSchema(pool);
24
+ if (resetOnStart) {
25
+ await PgSchema$ReventlessPostgres.truncateAll(pool);
26
+ }
27
+ let initialCount = await EventLogStorage_Postgres$ReventlessPostgres.countAll(pool) + await DcbEventLogStorage_Postgres$ReventlessPostgres.countAll(pool) | 0;
28
+ return {
29
+ TAG: "Postgres",
30
+ pool: pool,
31
+ initialCount: initialCount,
32
+ connection: connection
33
+ };
34
+ }
35
+
14
36
  function removeFileIfExists(path) {
15
37
  try {
16
38
  Nodefs.unlinkSync(path);
@@ -60,6 +82,7 @@ let memory = "Memory";
60
82
  export {
61
83
  memory,
62
84
  sqlite,
85
+ postgres,
63
86
  removeFileIfExists,
64
87
  fromEnv,
65
88
  }
@@ -12,6 +12,7 @@
12
12
  type active =
13
13
  | Memory
14
14
  | Sqlite({db: SqliteDriver.t, path: string})
15
+ | Postgres({pool: ReventlessPostgres.PgDriver.pool})
15
16
 
16
17
  let current: ref<active> = ref(Memory)
17
18
 
@@ -19,16 +20,20 @@ let setMemory = () => current := Memory
19
20
 
20
21
  let setSqlite = (~db, ~path) => current := Sqlite({db, path})
21
22
 
22
- let getDb = () =>
23
+ let setPostgres = (~pool) => current := Postgres({pool: pool})
24
+
25
+ // Per-backend handle accessors, symmetric across the SQL backends: each returns
26
+ // Some only when that backend is active. Dispatching adapters
27
+ // (LocalEventLogStorage, LocalDcbEventLogStorage, LocalQueryDbStorage,
28
+ // LocalTaskBucket) switch on these to select their concrete implementation.
29
+ let getSqliteDb = () =>
23
30
  switch current.contents {
24
31
  | Sqlite({db}) => Some(db)
25
- | Memory => None
32
+ | Memory | Postgres(_) => None
26
33
  }
27
34
 
28
- // True when the active backend is SQLite — used by adapters that want to swap
29
- // their implementation entirely, not just their data access.
30
- let isSqlite = () =>
35
+ let getPostgresPool = () =>
31
36
  switch current.contents {
32
- | Sqlite(_) => true
33
- | Memory => false
37
+ | Postgres({pool}) => Some(pool)
38
+ | Memory | Sqlite(_) => None
34
39
  }
@@ -18,25 +18,37 @@ function setSqlite(db, path) {
18
18
  };
19
19
  }
20
20
 
21
- function getDb() {
21
+ function setPostgres(pool) {
22
+ current.contents = {
23
+ TAG: "Postgres",
24
+ pool: pool
25
+ };
26
+ }
27
+
28
+ function getSqliteDb() {
22
29
  let match = current.contents;
23
- if (typeof match !== "object") {
30
+ if (typeof match !== "object" || match.TAG !== "Sqlite") {
24
31
  return;
25
32
  } else {
26
33
  return Primitive_option.some(match.db);
27
34
  }
28
35
  }
29
36
 
30
- function isSqlite() {
37
+ function getPostgresPool() {
31
38
  let match = current.contents;
32
- return typeof match === "object";
39
+ if (typeof match !== "object" || match.TAG === "Sqlite") {
40
+ return;
41
+ } else {
42
+ return Primitive_option.some(match.pool);
43
+ }
33
44
  }
34
45
 
35
46
  export {
36
47
  current,
37
48
  setMemory,
38
49
  setSqlite,
39
- getDb,
40
- isSqlite,
50
+ setPostgres,
51
+ getSqliteDb,
52
+ getPostgresPool,
41
53
  }
42
54
  /* No side effect */
@@ -144,7 +144,7 @@ let makeStorage = (~name as _name, ~indexes as _, ~partitionTag as _, ~opts as _
144
144
  | None => false
145
145
  }
146
146
  if conflictDetected {
147
- Error("conflict: condition check failed")
147
+ Error(ReventlessInfra.DcbEventLog.Conflict)
148
148
  } else {
149
149
  let base = events.contents->Array.length
150
150
  let storedEvents =
@@ -189,28 +189,9 @@ let makeStorage = (~name as _name, ~indexes as _, ~partitionTag as _, ~opts as _
189
189
  // (true DCB semantics), so a single-tag clause already matches every carrier of
190
190
  // the tag regardless of partition — cross-partition reads work without special
191
191
  // routing. The flag is accepted for interface parity and ignored.
192
+ // Pure in-memory storageMaker (no BackendState dispatch, no Bus). Backend
193
+ // selection lives in LocalDcbEventLogStorage.Make.
192
194
  let make: DcbEventLog_Adapter.storageMaker = (~name, ~indexes, ~partitionTag, ~crossPartitionTagKeys as _=?, ~opts) => {
193
195
  let (_, _, storage) = makeStorage(~name, ~indexes, ~partitionTag, ~opts)
194
196
  storage
195
197
  }
196
-
197
- module Make = (Bus: LocalBus.T) => {
198
- let make: DcbEventLog_Adapter.storageMaker = (~name, ~indexes, ~partitionTag, ~crossPartitionTagKeys as _=?, ~opts) => {
199
- switch BackendState.getDb() {
200
- | Some(db) =>
201
- let (storageName, read, storage) = DcbEventLogStorage_Sqlite.makeStorage(
202
- ~db,
203
- ~name,
204
- ~indexes,
205
- ~partitionTag,
206
- ~opts,
207
- )
208
- Bus.registerDcbEventLogRead(storageName, read)
209
- storage
210
- | None =>
211
- let (storageName, read, storage) = makeStorage(~name, ~indexes, ~partitionTag, ~opts)
212
- Bus.registerDcbEventLogRead(storageName, read)
213
- storage
214
- }
215
- }
216
- }
@@ -6,10 +6,7 @@ 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 Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
10
9
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
11
- import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
12
- import * as DcbEventLogStorage_Sqlite$ReventlessLocal from "./DcbEventLogStorage_Sqlite.res.mjs";
13
10
 
14
11
  function posToInt(pos) {
15
12
  return Stdlib_Option.getOr(Stdlib_Int.fromString(pos, undefined), 0);
@@ -146,7 +143,7 @@ function makeStorage(_name, param, param$1, param$2) {
146
143
  if (conflictDetected) {
147
144
  return {
148
145
  TAG: "Error",
149
- _0: "conflict: condition check failed"
146
+ _0: "Conflict"
150
147
  };
151
148
  }
152
149
  let base = events.contents.length;
@@ -194,23 +191,6 @@ function make(name, indexes, partitionTag, param, opts) {
194
191
  return makeStorage(name, indexes, partitionTag, opts)[2];
195
192
  }
196
193
 
197
- function Make(Bus) {
198
- let make = (name, indexes, partitionTag, param, opts) => {
199
- let db = BackendState$ReventlessLocal.getDb();
200
- if (db !== undefined) {
201
- let match = DcbEventLogStorage_Sqlite$ReventlessLocal.makeStorage(Primitive_option.valFromOption(db), name, indexes, partitionTag, opts);
202
- Bus.registerDcbEventLogRead(match[0], match[1]);
203
- return match[2];
204
- }
205
- let match$1 = makeStorage(name, indexes, partitionTag, opts);
206
- Bus.registerDcbEventLogRead(match$1[0], match$1[1]);
207
- return match$1[2];
208
- };
209
- return {
210
- make: make
211
- };
212
- }
213
-
214
194
  export {
215
195
  posToInt,
216
196
  matchesQuery,
@@ -218,6 +198,5 @@ export {
218
198
  intersectSorted,
219
199
  makeStorage,
220
200
  make,
221
- Make,
222
201
  }
223
202
  /* effect/Effect Not a pure module */
@@ -322,7 +322,7 @@ let makeStorage = (
322
322
  let append = async (
323
323
  newEvents: array<DcbEventLog_Adapter.rawStoredEvent>,
324
324
  ~condition=?,
325
- ): result<DcbTag.sequencePosition, string> => {
325
+ ): result<DcbTag.sequencePosition, ReventlessInfra.DcbEventLog.appendError> => {
326
326
  let result = ref(Ok(""))
327
327
  // (msgId, dcb_event rowid) per inserted event, for the projection
328
328
  // checkpoint's DCB-axis pending set. The rowid is read immediately after
@@ -379,17 +379,20 @@ let makeStorage = (
379
379
  ProjectionPending.trackAppended(~axis=ProjectionPending.Dcb, appended)
380
380
  result.contents
381
381
  } catch {
382
- | Failure(msg) => Error(msg)
382
+ // The deliberate append-condition failure throws Failure("conflict: …").
383
+ | Failure(msg) =>
384
+ msg->String.includes("conflict")
385
+ ? Error(ReventlessInfra.DcbEventLog.Conflict)
386
+ : Error(StorageFailure(msg))
383
387
  | exn =>
384
- // The deliberate append-condition failure above throws
385
- // Failure("conflict: …"); a lost race on the computed position shows up here
386
- // as a PRIMARY KEY / UNIQUE violation also a genuine conflict. Every other
387
- // failure (disk full, SQL error, locked db) must surface as a real error,
388
- // not the retryable "conflict" sentinel that would be retried forever.
388
+ // A lost race on the computed position shows up here as a PRIMARY KEY /
389
+ // UNIQUE violation also a genuine conflict. Every other failure (disk
390
+ // full, SQL error, locked db) is a real StorageFailure, not the retryable
391
+ // Conflict sentinel that would be retried forever.
389
392
  let msg = exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("")
390
393
  msg->String.includes("constraint failed")
391
- ? Error("conflict")
392
- : Error(msg == "" ? "storage error" : msg)
394
+ ? Error(ReventlessInfra.DcbEventLog.Conflict)
395
+ : Error(StorageFailure(msg == "" ? "storage error" : msg))
393
396
  }
394
397
  }
395
398
 
@@ -414,4 +417,4 @@ let makeStorage = (
414
417
  }
415
418
 
416
419
  // No `Make(Bus)` functor here on purpose — dispatch is centralised in
417
- // DcbEventLogStorage_InMemory.Make so callers only ever wire one module.
420
+ // LocalDcbEventLogStorage.Make so callers only ever wire one module.