@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.
- package/CHANGELOG.md +15 -0
- package/package.json +8 -7
- package/rescript.json +2 -1
- package/src/Platform.res +44 -6
- package/src/Platform.res.mjs +53 -16
- package/src/adapter/Backend.res +23 -0
- package/src/adapter/Backend.res.mjs +23 -0
- package/src/adapter/BackendState.res +12 -7
- package/src/adapter/BackendState.res.mjs +18 -6
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +3 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +1 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +13 -10
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +22 -8
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +52 -0
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +35 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +3 -15
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +0 -21
- package/src/adapter/EventLog/LocalEventLogStorage.res +33 -0
- package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +35 -0
- package/src/adapter/LocalBus.res +1 -1
- package/src/adapter/PgProjectionCatchup.res +146 -0
- package/src/adapter/PgProjectionCatchup.res.mjs +114 -0
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +43 -0
- package/src/adapter/QueryDb/LocalQueryDbStorage.res.mjs +42 -0
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +3 -24
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +1 -28
- package/src/adapter/Task/LocalTaskBucket.res +1 -1
- package/src/adapter/Task/LocalTaskBucket.res.mjs +1 -1
- package/src/components/Aggregate_Builder.res +1 -1
- package/src/components/Aggregate_Builder.res.mjs +3 -3
- package/src/components/AutomationSlice_Builder.res +1 -1
- package/src/components/AutomationSlice_Builder.res.mjs +3 -3
- package/src/components/Counter_Builder.res +1 -1
- package/src/components/Counter_Builder.res.mjs +2 -2
- package/src/components/DcbEventLog_Builder.res +1 -1
- package/src/components/DcbEventLog_Builder.res.mjs +2 -2
- package/src/components/InboundTranslationSlice_Builder.res +1 -1
- package/src/components/InboundTranslationSlice_Builder.res.mjs +2 -2
- package/src/components/OutboundTranslationSlice_Builder.res +1 -1
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +2 -2
- package/src/components/Plugin_Builder.res +1 -1
- package/src/components/Plugin_Builder.res.mjs +2 -2
- package/src/components/ReadModel_Builder.res +1 -1
- package/src/components/ReadModel_Builder.res.mjs +2 -2
- package/src/components/StateViewSlice_Builder.res +1 -1
- package/src/components/StateViewSlice_Builder.res.mjs +2 -2
- package/src/test/Mocks/MockDcbEventLogStorage.res +2 -2
- package/src/test/Mocks/MockDcbEventLogStorage.res.mjs +5 -2
- package/tests/adapter/BackendParityTest.res +43 -8
- package/tests/adapter/BackendParityTest.res.mjs +39 -9
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +17 -9
- package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +19 -10
- package/tests/adapter/PgProjectionCatchupTest.res +149 -0
- package/tests/adapter/PgProjectionCatchupTest.res.mjs +181 -0
- package/tests/adapter/QueryDbGsiTtlTest.res +2 -2
- package/tests/adapter/QueryDbGsiTtlTest.res.mjs +3 -2
- package/tests/adapter/QueryDbListResolverTest.res +1 -1
- package/tests/adapter/QueryDbListResolverTest.res.mjs +2 -2
- package/tests/adapter/QueryDbStorageTest.res +10 -10
- package/tests/adapter/QueryDbStorageTest.res.mjs +11 -11
- package/tests/adapter/QueryEngineTest.res +8 -8
- package/tests/adapter/QueryEngineTest.res.mjs +9 -9
- package/tests/components/querydb/QueryDbFixtures.res +1 -1
- package/tests/components/querydb/QueryDbFixtures.res.mjs +2 -2
- package/tests/components/querydb/QueryDbSubIdFixtures.res +1 -1
- 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.
|
|
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/
|
|
39
|
-
"@reventlessdev/reventless-
|
|
40
|
-
"@reventlessdev/reventless-
|
|
41
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
42
|
-
"@reventlessdev/reventless-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
|
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
|
package/src/Platform.res.mjs
CHANGED
|
@@ -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))(
|
|
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 =
|
|
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" :
|
|
1098
|
-
match.
|
|
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.
|
|
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))(
|
|
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 =
|
|
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" :
|
|
2701
|
-
backend.
|
|
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.
|
|
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
|
}
|
package/src/adapter/Backend.res
CHANGED
|
@@ -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
|
|
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
|
-
|
|
29
|
-
// their implementation entirely, not just their data access.
|
|
30
|
-
let isSqlite = () =>
|
|
35
|
+
let getPostgresPool = () =>
|
|
31
36
|
switch current.contents {
|
|
32
|
-
|
|
|
33
|
-
| Memory =>
|
|
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
|
|
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
|
|
37
|
+
function getPostgresPool() {
|
|
31
38
|
let match = current.contents;
|
|
32
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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(
|
|
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: "
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
420
|
+
// LocalDcbEventLogStorage.Make so callers only ever wire one module.
|