@reventlessdev/reventless-local 3.0.0-alpha.114 → 3.0.0-alpha.115
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 +11 -0
- package/package.json +7 -7
- package/src/Platform.res +25 -50
- package/src/Platform.res.mjs +25 -160
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +10 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +9 -1
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +1 -1
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +1 -1
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +16 -3
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +35 -7
- package/src/adapter/LocalBus.res +23 -3
- package/src/adapter/LocalBus.res.mjs +100 -66
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +99 -7
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +97 -13
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +24 -1
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +19 -4
- package/src/adapter/SqliteDriver.res +6 -1
- package/src/adapter/SqliteDriver.res.mjs +5 -1
- package/src/components/DcbEventLog_Builder.res +7 -1
- package/src/components/DcbEventLog_Builder.res.mjs +3 -3
- package/src/test/Mocks/MockEventLogStorage.res +1 -1
- package/src/test/Mocks/MockEventLogStorage.res.mjs +4 -1
- package/tests/adapter/BackendParityTest.res +64 -0
- package/tests/adapter/BackendParityTest.res.mjs +45 -0
- package/tests/adapter/EventLogStorageSqliteTest.res +4 -2
- package/tests/adapter/EventLogStorageSqliteTest.res.mjs +6 -1
- package/tests/adapter/LocalBusPubSubTest.res +31 -0
- package/tests/adapter/LocalBusPubSubTest.res.mjs +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
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.115 (2026-07-02)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core,local:** projection/publisher/bus correctness (plan A5,A7) ([86e27a5](https://github.com/ReventlessDev/reventless-core/commit/86e27a5d68873d7dd9e746f9b4126f4d5bad63c9))
|
|
11
|
+
* **local:** conflict classification, rollback masking, MCP pagination (plan A6) ([b3e6d24](https://github.com/ReventlessDev/reventless-core/commit/b3e6d242ddf2df066ca94b0db2266275f1453fbf))
|
|
12
|
+
* **local:** InMemory QueryDb TTL parity + backend-aware UIFragment store (plan A6) ([c31727f](https://github.com/ReventlessDev/reventless-core/commit/c31727fa3fe98b4f15b55300fdf8b80221fabbee))
|
|
13
|
+
* **local:** QueryDb count returns a running total, not the increment (plan A6) ([4ac3822](https://github.com/ReventlessDev/reventless-core/commit/4ac3822ac310d62bb4b1c4c021a85f45883f4785))
|
|
14
|
+
* **local:** wire standalone DCB log through the backend-aware storage (plan A6) ([40734e9](https://github.com/ReventlessDev/reventless-core/commit/40734e902436dfce623061416e9edeafa822f1d3))
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# 3.0.0-alpha.114 (2026-06-29)
|
|
7
18
|
|
|
8
19
|
### Bug Fixes
|
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.115",
|
|
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,15 @@
|
|
|
31
31
|
"graphql-yoga": "^5.21.0",
|
|
32
32
|
"sury": "11.0.0-alpha.4",
|
|
33
33
|
"ws": "^8.18.0",
|
|
34
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.
|
|
34
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.24",
|
|
35
35
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.17",
|
|
36
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.5",
|
|
36
37
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.16",
|
|
37
38
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
|
|
38
|
-
"@reventlessdev/
|
|
39
|
-
"@reventlessdev/reventless-
|
|
40
|
-
"@reventlessdev/reventless-
|
|
41
|
-
"@reventlessdev/reventless-
|
|
42
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.62"
|
|
39
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.136",
|
|
40
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.76",
|
|
41
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.83",
|
|
42
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.63"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"rescript": "^12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -279,10 +279,18 @@ module MakeWithConfig = (
|
|
|
279
279
|
])->JSON.Encode.object
|
|
280
280
|
|
|
281
281
|
let paginate = (events: array<JSON.t>, getPosition: JSON.t => option<string>) => {
|
|
282
|
+
// Positions are numeric strings; compare them as ints, not lexically
|
|
283
|
+
// ("10" > "9" is false as strings) — otherwise cursor pages skip or
|
|
284
|
+
// duplicate events once positions cross a digit boundary.
|
|
285
|
+
let positionGt = (p, afterPos) =>
|
|
286
|
+
switch (Int.fromString(p), Int.fromString(afterPos)) {
|
|
287
|
+
| (Some(pi), Some(ai)) => pi > ai
|
|
288
|
+
| _ => p > afterPos
|
|
289
|
+
}
|
|
282
290
|
let filtered = switch after {
|
|
283
291
|
| Some(afterPos) =>
|
|
284
292
|
let idx =
|
|
285
|
-
events->Array.findIndex(e => getPosition(e)->Option.mapOr(false, p => p
|
|
293
|
+
events->Array.findIndex(e => getPosition(e)->Option.mapOr(false, p => positionGt(p, afterPos)))
|
|
286
294
|
if idx >= 0 {
|
|
287
295
|
events->Array.slice(~start=idx, ~end=events->Array.length)
|
|
288
296
|
} else {
|
|
@@ -749,60 +757,27 @@ module MakeWithConfig = (
|
|
|
749
757
|
// UIFragmentRegistry QueryDb store ops — initialized lazily on first seed call.
|
|
750
758
|
let uiFragmentQueryDbOpsRef: ref<option<ReventlessCore.QueryDb_Adapter.operations>> = ref(None)
|
|
751
759
|
|
|
760
|
+
// Backend-aware storage for the UIFragment registry — the same functor every
|
|
761
|
+
// other read model uses. Replaces a hand-rolled memory-only store that ignored
|
|
762
|
+
// BackendState, so the registry now persists under SQLite too.
|
|
763
|
+
module UIFragmentStorage = QueryDbStorage_InMemory.Make(Bus)
|
|
764
|
+
|
|
752
765
|
let ensureUIFragmentRegistryQueryDbStore = () => {
|
|
753
766
|
switch uiFragmentQueryDbOpsRef.contents {
|
|
754
767
|
| Some(ops) => ops
|
|
755
768
|
| None =>
|
|
756
769
|
let name = ReventlessCore.UIFragmentRegistryReadModelSpec.name
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
obj->Dict.toArray->Array.forEach(((k, v)) => copy->Dict.set(k, v))
|
|
769
|
-
copy->Dict.set("id", JSON.Encode.string(id))
|
|
770
|
-
JSON.Encode.object(copy)
|
|
771
|
-
} else {
|
|
772
|
-
item
|
|
773
|
-
}
|
|
774
|
-
})
|
|
775
|
-
)
|
|
776
|
-
}
|
|
777
|
-
let ops: ReventlessCore.QueryDb_Adapter.operations = {
|
|
778
|
-
load: async id => Ok(store.contents->Dict.get(id)->Option.getOr([])),
|
|
779
|
-
loadStream: id =>
|
|
780
|
-
store.contents->Dict.get(id)->Option.getOr([])->Stream.fromIterable,
|
|
781
|
-
save: async (id, state, _, _) => {
|
|
782
|
-
store.contents->Dict.set(id, [state])
|
|
783
|
-
syncAll()
|
|
784
|
-
Ok()
|
|
785
|
-
},
|
|
786
|
-
saveBatch: async batch => {
|
|
787
|
-
batch->Array.forEach(((id, state, _)) => store.contents->Dict.set(id, [state]))
|
|
788
|
-
syncAll()
|
|
789
|
-
Ok()
|
|
790
|
-
},
|
|
791
|
-
count: async (_, _, inc) => Ok(inc),
|
|
792
|
-
delete: async (id, _) => {
|
|
793
|
-
store.contents->Dict.delete(id)
|
|
794
|
-
syncAll()
|
|
795
|
-
Ok()
|
|
796
|
-
},
|
|
797
|
-
deleteBatch: async ids => {
|
|
798
|
-
ids->Array.forEach(((id, _)) => store.contents->Dict.delete(id))
|
|
799
|
-
syncAll()
|
|
800
|
-
Ok()
|
|
801
|
-
},
|
|
802
|
-
}
|
|
803
|
-
Bus.registerQueryDb(name, ops)
|
|
804
|
-
Bus.registerQueryDbScan(name, () => allItems.contents)
|
|
805
|
-
Bus.registerQueryDbStream(name, () => allItems.contents->Stream.fromIterable)
|
|
770
|
+
// `make` registers the ops (and scan/stream) with the Bus for the active
|
|
771
|
+
// backend; retrieve them synchronously via getQueryDb rather than resolving
|
|
772
|
+
// the Pulumi Output.
|
|
773
|
+
let _ = UIFragmentStorage.make(
|
|
774
|
+
~name,
|
|
775
|
+
~indexes=[],
|
|
776
|
+
~api=(),
|
|
777
|
+
~apiRole=(),
|
|
778
|
+
~opts=({}: Pulumi.CustomResourceOptions.t),
|
|
779
|
+
)
|
|
780
|
+
let ops = Bus.getQueryDb(name)->Option.getOrThrow
|
|
806
781
|
uiFragmentQueryDbOpsRef := Some(ops)
|
|
807
782
|
ops
|
|
808
783
|
}
|
package/src/Platform.res.mjs
CHANGED
|
@@ -4,14 +4,12 @@ import * as S from "sury/src/S.res.mjs";
|
|
|
4
4
|
import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
|
|
5
5
|
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
6
6
|
import * as Stdlib_Bool from "@rescript/runtime/lib/es6/Stdlib_Bool.js";
|
|
7
|
-
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
8
7
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
9
8
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
10
9
|
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
11
10
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
12
11
|
import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
|
|
13
12
|
import * as Effect from "effect/Effect";
|
|
14
|
-
import * as Stream$1 from "effect/Stream";
|
|
15
13
|
import * as Pulumi from "@pulumi/pulumi";
|
|
16
14
|
import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
|
|
17
15
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
@@ -63,6 +61,7 @@ import * as AutomationSlice_Builder$ReventlessLocal from "./components/Automatio
|
|
|
63
61
|
import * as LocalRuntimeEnvironment$ReventlessLocal from "./adapter/Runtime/LocalRuntimeEnvironment.res.mjs";
|
|
64
62
|
import * as LocalScheduledPublisher$ReventlessLocal from "./adapter/Scheduler/LocalScheduledPublisher.res.mjs";
|
|
65
63
|
import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
|
|
64
|
+
import * as QueryDbStorage_InMemory$ReventlessLocal from "./adapter/QueryDb/QueryDbStorage_InMemory.res.mjs";
|
|
66
65
|
import * as LocalCommandTopicChannel$ReventlessLocal from "./adapter/CommandTopic/LocalCommandTopicChannel.res.mjs";
|
|
67
66
|
import * as LocalEventTopicPublisher$ReventlessLocal from "./adapter/EventTopic/LocalEventTopicPublisher.res.mjs";
|
|
68
67
|
import * as StateChangeSlice_Builder$ReventlessLocal from "./components/StateChangeSlice_Builder.res.mjs";
|
|
@@ -268,7 +267,15 @@ function MakeWithConfig(Config) {
|
|
|
268
267
|
let paginate = (events, getPosition) => {
|
|
269
268
|
let filtered;
|
|
270
269
|
if (after !== undefined) {
|
|
271
|
-
let idx = events.findIndex(e => Stdlib_Option.mapOr(getPosition(e), false, p =>
|
|
270
|
+
let idx = events.findIndex(e => Stdlib_Option.mapOr(getPosition(e), false, p => {
|
|
271
|
+
let match = Stdlib_Int.fromString(p, undefined);
|
|
272
|
+
let match$1 = Stdlib_Int.fromString(after, undefined);
|
|
273
|
+
if (match !== undefined && match$1 !== undefined) {
|
|
274
|
+
return match > match$1;
|
|
275
|
+
} else {
|
|
276
|
+
return p > after;
|
|
277
|
+
}
|
|
278
|
+
}));
|
|
272
279
|
filtered = idx >= 0 ? events.slice(idx, events.length) : [];
|
|
273
280
|
} else {
|
|
274
281
|
filtered = events;
|
|
@@ -704,89 +711,14 @@ function MakeWithConfig(Config) {
|
|
|
704
711
|
let uiFragmentQueryDbOpsRef = {
|
|
705
712
|
contents: undefined
|
|
706
713
|
};
|
|
714
|
+
let UIFragmentStorage = QueryDbStorage_InMemory$ReventlessLocal.Make(Bus);
|
|
707
715
|
let ensureUIFragmentRegistryQueryDbStore = () => {
|
|
708
716
|
let ops = uiFragmentQueryDbOpsRef.contents;
|
|
709
717
|
if (ops !== undefined) {
|
|
710
718
|
return ops;
|
|
711
719
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
};
|
|
715
|
-
let allItems = {
|
|
716
|
-
contents: []
|
|
717
|
-
};
|
|
718
|
-
let syncAll = () => {
|
|
719
|
-
allItems.contents = Object.entries(store.contents).flatMap(param => {
|
|
720
|
-
let id = param[0];
|
|
721
|
-
return param[1].map(item => {
|
|
722
|
-
let obj = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(item), {});
|
|
723
|
-
if (Stdlib_Option.isSome(obj["id"])) {
|
|
724
|
-
return item;
|
|
725
|
-
}
|
|
726
|
-
let copy = {};
|
|
727
|
-
Object.entries(obj).forEach(param => {
|
|
728
|
-
copy[param[0]] = param[1];
|
|
729
|
-
});
|
|
730
|
-
copy["id"] = id;
|
|
731
|
-
return copy;
|
|
732
|
-
});
|
|
733
|
-
});
|
|
734
|
-
};
|
|
735
|
-
let ops_load = async id => ({
|
|
736
|
-
TAG: "Ok",
|
|
737
|
-
_0: Stdlib_Option.getOr(store.contents[id], [])
|
|
738
|
-
});
|
|
739
|
-
let ops_loadStream = id => Stream$1.fromIterable(Stdlib_Option.getOr(store.contents[id], []));
|
|
740
|
-
let ops_save = async (id, state, param, param$1) => {
|
|
741
|
-
store.contents[id] = [state];
|
|
742
|
-
syncAll();
|
|
743
|
-
return {
|
|
744
|
-
TAG: "Ok",
|
|
745
|
-
_0: undefined
|
|
746
|
-
};
|
|
747
|
-
};
|
|
748
|
-
let ops_saveBatch = async batch => {
|
|
749
|
-
batch.forEach(param => {
|
|
750
|
-
store.contents[param[0]] = [param[1]];
|
|
751
|
-
});
|
|
752
|
-
syncAll();
|
|
753
|
-
return {
|
|
754
|
-
TAG: "Ok",
|
|
755
|
-
_0: undefined
|
|
756
|
-
};
|
|
757
|
-
};
|
|
758
|
-
let ops_count = async (param, param$1, inc) => ({
|
|
759
|
-
TAG: "Ok",
|
|
760
|
-
_0: inc
|
|
761
|
-
});
|
|
762
|
-
let ops_delete = async (id, param) => {
|
|
763
|
-
Stdlib_Dict.$$delete(store.contents, id);
|
|
764
|
-
syncAll();
|
|
765
|
-
return {
|
|
766
|
-
TAG: "Ok",
|
|
767
|
-
_0: undefined
|
|
768
|
-
};
|
|
769
|
-
};
|
|
770
|
-
let ops_deleteBatch = async ids => {
|
|
771
|
-
ids.forEach(param => Stdlib_Dict.$$delete(store.contents, param[0]));
|
|
772
|
-
syncAll();
|
|
773
|
-
return {
|
|
774
|
-
TAG: "Ok",
|
|
775
|
-
_0: undefined
|
|
776
|
-
};
|
|
777
|
-
};
|
|
778
|
-
let ops$1 = {
|
|
779
|
-
load: ops_load,
|
|
780
|
-
loadStream: ops_loadStream,
|
|
781
|
-
save: ops_save,
|
|
782
|
-
saveBatch: ops_saveBatch,
|
|
783
|
-
count: ops_count,
|
|
784
|
-
delete: ops_delete,
|
|
785
|
-
deleteBatch: ops_deleteBatch
|
|
786
|
-
};
|
|
787
|
-
Bus.registerQueryDb(UIFragmentRegistryReadModelSpec$ReventlessCore.name, ops$1);
|
|
788
|
-
Bus.registerQueryDbScan(UIFragmentRegistryReadModelSpec$ReventlessCore.name, () => allItems.contents);
|
|
789
|
-
Bus.registerQueryDbStream(UIFragmentRegistryReadModelSpec$ReventlessCore.name, () => Stream$1.fromIterable(allItems.contents));
|
|
720
|
+
UIFragmentStorage.make(UIFragmentRegistryReadModelSpec$ReventlessCore.name, [], undefined, undefined, undefined, undefined, {});
|
|
721
|
+
let ops$1 = Stdlib_Option.getOrThrow(Bus.getQueryDb(UIFragmentRegistryReadModelSpec$ReventlessCore.name), undefined);
|
|
790
722
|
uiFragmentQueryDbOpsRef.contents = ops$1;
|
|
791
723
|
return ops$1;
|
|
792
724
|
};
|
|
@@ -1921,7 +1853,15 @@ function Make($star) {
|
|
|
1921
1853
|
let paginate = (events, getPosition) => {
|
|
1922
1854
|
let filtered;
|
|
1923
1855
|
if (after !== undefined) {
|
|
1924
|
-
let idx = events.findIndex(e => Stdlib_Option.mapOr(getPosition(e), false, p =>
|
|
1856
|
+
let idx = events.findIndex(e => Stdlib_Option.mapOr(getPosition(e), false, p => {
|
|
1857
|
+
let match = Stdlib_Int.fromString(p, undefined);
|
|
1858
|
+
let match$1 = Stdlib_Int.fromString(after, undefined);
|
|
1859
|
+
if (match !== undefined && match$1 !== undefined) {
|
|
1860
|
+
return match > match$1;
|
|
1861
|
+
} else {
|
|
1862
|
+
return p > after;
|
|
1863
|
+
}
|
|
1864
|
+
}));
|
|
1925
1865
|
filtered = idx >= 0 ? events.slice(idx, events.length) : [];
|
|
1926
1866
|
} else {
|
|
1927
1867
|
filtered = events;
|
|
@@ -2356,89 +2296,14 @@ function Make($star) {
|
|
|
2356
2296
|
let uiFragmentQueryDbOpsRef = {
|
|
2357
2297
|
contents: undefined
|
|
2358
2298
|
};
|
|
2299
|
+
let UIFragmentStorage = QueryDbStorage_InMemory$ReventlessLocal.Make(Bus);
|
|
2359
2300
|
let ensureUIFragmentRegistryQueryDbStore = () => {
|
|
2360
2301
|
let ops = uiFragmentQueryDbOpsRef.contents;
|
|
2361
2302
|
if (ops !== undefined) {
|
|
2362
2303
|
return ops;
|
|
2363
2304
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
};
|
|
2367
|
-
let allItems = {
|
|
2368
|
-
contents: []
|
|
2369
|
-
};
|
|
2370
|
-
let syncAll = () => {
|
|
2371
|
-
allItems.contents = Object.entries(store.contents).flatMap(param => {
|
|
2372
|
-
let id = param[0];
|
|
2373
|
-
return param[1].map(item => {
|
|
2374
|
-
let obj = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(item), {});
|
|
2375
|
-
if (Stdlib_Option.isSome(obj["id"])) {
|
|
2376
|
-
return item;
|
|
2377
|
-
}
|
|
2378
|
-
let copy = {};
|
|
2379
|
-
Object.entries(obj).forEach(param => {
|
|
2380
|
-
copy[param[0]] = param[1];
|
|
2381
|
-
});
|
|
2382
|
-
copy["id"] = id;
|
|
2383
|
-
return copy;
|
|
2384
|
-
});
|
|
2385
|
-
});
|
|
2386
|
-
};
|
|
2387
|
-
let ops_load = async id => ({
|
|
2388
|
-
TAG: "Ok",
|
|
2389
|
-
_0: Stdlib_Option.getOr(store.contents[id], [])
|
|
2390
|
-
});
|
|
2391
|
-
let ops_loadStream = id => Stream$1.fromIterable(Stdlib_Option.getOr(store.contents[id], []));
|
|
2392
|
-
let ops_save = async (id, state, param, param$1) => {
|
|
2393
|
-
store.contents[id] = [state];
|
|
2394
|
-
syncAll();
|
|
2395
|
-
return {
|
|
2396
|
-
TAG: "Ok",
|
|
2397
|
-
_0: undefined
|
|
2398
|
-
};
|
|
2399
|
-
};
|
|
2400
|
-
let ops_saveBatch = async batch => {
|
|
2401
|
-
batch.forEach(param => {
|
|
2402
|
-
store.contents[param[0]] = [param[1]];
|
|
2403
|
-
});
|
|
2404
|
-
syncAll();
|
|
2405
|
-
return {
|
|
2406
|
-
TAG: "Ok",
|
|
2407
|
-
_0: undefined
|
|
2408
|
-
};
|
|
2409
|
-
};
|
|
2410
|
-
let ops_count = async (param, param$1, inc) => ({
|
|
2411
|
-
TAG: "Ok",
|
|
2412
|
-
_0: inc
|
|
2413
|
-
});
|
|
2414
|
-
let ops_delete = async (id, param) => {
|
|
2415
|
-
Stdlib_Dict.$$delete(store.contents, id);
|
|
2416
|
-
syncAll();
|
|
2417
|
-
return {
|
|
2418
|
-
TAG: "Ok",
|
|
2419
|
-
_0: undefined
|
|
2420
|
-
};
|
|
2421
|
-
};
|
|
2422
|
-
let ops_deleteBatch = async ids => {
|
|
2423
|
-
ids.forEach(param => Stdlib_Dict.$$delete(store.contents, param[0]));
|
|
2424
|
-
syncAll();
|
|
2425
|
-
return {
|
|
2426
|
-
TAG: "Ok",
|
|
2427
|
-
_0: undefined
|
|
2428
|
-
};
|
|
2429
|
-
};
|
|
2430
|
-
let ops$1 = {
|
|
2431
|
-
load: ops_load,
|
|
2432
|
-
loadStream: ops_loadStream,
|
|
2433
|
-
save: ops_save,
|
|
2434
|
-
saveBatch: ops_saveBatch,
|
|
2435
|
-
count: ops_count,
|
|
2436
|
-
delete: ops_delete,
|
|
2437
|
-
deleteBatch: ops_deleteBatch
|
|
2438
|
-
};
|
|
2439
|
-
Bus.registerQueryDb(UIFragmentRegistryReadModelSpec$ReventlessCore.name, ops$1);
|
|
2440
|
-
Bus.registerQueryDbScan(UIFragmentRegistryReadModelSpec$ReventlessCore.name, () => allItems.contents);
|
|
2441
|
-
Bus.registerQueryDbStream(UIFragmentRegistryReadModelSpec$ReventlessCore.name, () => Stream$1.fromIterable(allItems.contents));
|
|
2305
|
+
UIFragmentStorage.make(UIFragmentRegistryReadModelSpec$ReventlessCore.name, [], undefined, undefined, undefined, undefined, {});
|
|
2306
|
+
let ops$1 = Stdlib_Option.getOrThrow(Bus.getQueryDb(UIFragmentRegistryReadModelSpec$ReventlessCore.name), undefined);
|
|
2442
2307
|
uiFragmentQueryDbOpsRef.contents = ops$1;
|
|
2443
2308
|
return ops$1;
|
|
2444
2309
|
};
|
|
@@ -306,7 +306,16 @@ let makeStorage = (
|
|
|
306
306
|
result.contents
|
|
307
307
|
} catch {
|
|
308
308
|
| Failure(msg) => Error(msg)
|
|
309
|
-
|
|
|
309
|
+
| exn =>
|
|
310
|
+
// The deliberate append-condition failure above throws
|
|
311
|
+
// Failure("conflict: …"); a lost race on the computed position shows up here
|
|
312
|
+
// as a PRIMARY KEY / UNIQUE violation — also a genuine conflict. Every other
|
|
313
|
+
// failure (disk full, SQL error, locked db) must surface as a real error,
|
|
314
|
+
// not the retryable "conflict" sentinel that would be retried forever.
|
|
315
|
+
let msg = exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("")
|
|
316
|
+
msg->String.includes("constraint failed")
|
|
317
|
+
? Error("conflict")
|
|
318
|
+
: Error(msg == "" ? "storage error" : msg)
|
|
310
319
|
}
|
|
311
320
|
}
|
|
312
321
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
4
|
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
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";
|
|
7
8
|
import * as Stream from "effect/Stream";
|
|
@@ -262,11 +263,18 @@ function makeStorage(db, bus, publishToTopic, name, param, param$1, param$2) {
|
|
|
262
263
|
TAG: "Error",
|
|
263
264
|
_0: msg._1
|
|
264
265
|
};
|
|
265
|
-
}
|
|
266
|
+
}
|
|
267
|
+
let msg$1 = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(msg), Stdlib_JsExn.message), "");
|
|
268
|
+
if (msg$1.includes("constraint failed")) {
|
|
266
269
|
return {
|
|
267
270
|
TAG: "Error",
|
|
268
271
|
_0: "conflict"
|
|
269
272
|
};
|
|
273
|
+
} else {
|
|
274
|
+
return {
|
|
275
|
+
TAG: "Error",
|
|
276
|
+
_0: msg$1 === "" ? "storage error" : msg$1
|
|
277
|
+
};
|
|
270
278
|
}
|
|
271
279
|
}
|
|
272
280
|
};
|
|
@@ -17,7 +17,7 @@ let makeMemoryStorage = (~name as _name, ~opts as _) => {
|
|
|
17
17
|
let existing = events->Dict.get(id)->Option.getOr([])
|
|
18
18
|
let currentCount = existing->Array.length
|
|
19
19
|
if seqNr != currentCount {
|
|
20
|
-
(Error(
|
|
20
|
+
(Error(ReventlessCore.EventLog.Conflict), events)
|
|
21
21
|
} else {
|
|
22
22
|
events->Dict.set(id, existing->Array.concat(jsons))
|
|
23
23
|
(Ok(), events)
|
|
@@ -88,8 +88,19 @@ let makeStorage = (~db: SqliteDriver.t, ~name: string, ~opts as _) => {
|
|
|
88
88
|
})
|
|
89
89
|
Ok()
|
|
90
90
|
} catch {
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// The deliberate seq_nr check above throws Failure("conflict"); a lost race
|
|
92
|
+
// shows up as a PRIMARY KEY violation on (log_name, aggregate_id, seq_nr) —
|
|
93
|
+
// also a genuine OCC conflict. Every OTHER failure (disk full, SQL error,
|
|
94
|
+
// locked db) is a real StorageFailure, not the retryable Conflict sentinel.
|
|
95
|
+
| Failure(msg) =>
|
|
96
|
+
msg == "conflict"
|
|
97
|
+
? Error(ReventlessCore.EventLog.Conflict)
|
|
98
|
+
: Error(ReventlessCore.EventLog.StorageFailure(msg))
|
|
99
|
+
| exn =>
|
|
100
|
+
let msg = exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("")
|
|
101
|
+
msg->String.includes("constraint failed")
|
|
102
|
+
? Error(ReventlessCore.EventLog.Conflict)
|
|
103
|
+
: Error(ReventlessCore.EventLog.StorageFailure(msg == "" ? "storage error" : msg))
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
|
|
@@ -115,7 +126,9 @@ let makeStorage = (~db: SqliteDriver.t, ~name: string, ~opts as _) => {
|
|
|
115
126
|
| Ok() =>
|
|
116
127
|
seqNrRef := currentSeq + 1
|
|
117
128
|
Effect.succeed()
|
|
118
|
-
|
|
129
|
+
// appendStream's error channel is a string; map the typed append error.
|
|
130
|
+
| Error(ReventlessCore.EventLog.Conflict) => Effect.fail("conflict")
|
|
131
|
+
| Error(StorageFailure(msg)) => Effect.fail(msg)
|
|
119
132
|
}
|
|
120
133
|
)
|
|
121
134
|
})
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
3
5
|
import * as Effect from "effect/Effect";
|
|
4
6
|
import * as Stream from "effect/Stream";
|
|
5
7
|
import * as Pulumi from "@pulumi/pulumi";
|
|
@@ -84,14 +86,35 @@ function makeStorage(db, name, param) {
|
|
|
84
86
|
} catch (raw_msg) {
|
|
85
87
|
let msg = Primitive_exceptions.internalToException(raw_msg);
|
|
86
88
|
if (msg.RE_EXN_ID === "Failure") {
|
|
89
|
+
let msg$1 = msg._1;
|
|
90
|
+
if (msg$1 === "conflict") {
|
|
91
|
+
return {
|
|
92
|
+
TAG: "Error",
|
|
93
|
+
_0: "Conflict"
|
|
94
|
+
};
|
|
95
|
+
} else {
|
|
96
|
+
return {
|
|
97
|
+
TAG: "Error",
|
|
98
|
+
_0: {
|
|
99
|
+
TAG: "StorageFailure",
|
|
100
|
+
_0: msg$1
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let msg$2 = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(msg), Stdlib_JsExn.message), "");
|
|
106
|
+
if (msg$2.includes("constraint failed")) {
|
|
87
107
|
return {
|
|
88
108
|
TAG: "Error",
|
|
89
|
-
_0:
|
|
109
|
+
_0: "Conflict"
|
|
90
110
|
};
|
|
91
111
|
} else {
|
|
92
112
|
return {
|
|
93
113
|
TAG: "Error",
|
|
94
|
-
_0:
|
|
114
|
+
_0: {
|
|
115
|
+
TAG: "StorageFailure",
|
|
116
|
+
_0: msg$2 === "" ? "storage error" : msg$2
|
|
117
|
+
}
|
|
95
118
|
};
|
|
96
119
|
}
|
|
97
120
|
}
|
|
@@ -109,11 +132,16 @@ function makeStorage(db, name, param) {
|
|
|
109
132
|
return Stream.runForEach(stream, json => {
|
|
110
133
|
let currentSeq = seqNrRef.contents;
|
|
111
134
|
return Effect.flatMap(Effect.promise(() => append(currentSeq, id, [json])), result => {
|
|
112
|
-
if (result.TAG
|
|
113
|
-
|
|
135
|
+
if (result.TAG === "Ok") {
|
|
136
|
+
seqNrRef.contents = currentSeq + 1 | 0;
|
|
137
|
+
return Effect.succeed();
|
|
138
|
+
}
|
|
139
|
+
let msg = result._0;
|
|
140
|
+
if (typeof msg !== "object") {
|
|
141
|
+
return Effect.fail("conflict");
|
|
142
|
+
} else {
|
|
143
|
+
return Effect.fail(msg._0);
|
|
114
144
|
}
|
|
115
|
-
seqNrRef.contents = currentSeq + 1 | 0;
|
|
116
|
-
return Effect.succeed();
|
|
117
145
|
});
|
|
118
146
|
});
|
|
119
147
|
};
|
|
@@ -152,4 +180,4 @@ export {
|
|
|
152
180
|
makeStorage,
|
|
153
181
|
Make,
|
|
154
182
|
}
|
|
155
|
-
/*
|
|
183
|
+
/* Stdlib_JsExn Not a pure module */
|
package/src/adapter/LocalBus.res
CHANGED
|
@@ -265,7 +265,19 @@ module Impl = (C: BusConfig): T => {
|
|
|
265
265
|
let drainLoop = Effect.scoped(
|
|
266
266
|
PubSub.subscribe(hub)->Effect.flatMap(queue =>
|
|
267
267
|
Stream.fromQueue(queue)->Stream.runForEach(msg =>
|
|
268
|
-
|
|
268
|
+
// A throwing/rejecting handler must not (a) leave `done_` uncalled —
|
|
269
|
+
// the publisher's countdown would never reach zero and every publish
|
|
270
|
+
// on this topic would hang — nor (b) kill this drain fiber, which
|
|
271
|
+
// would stop consuming while the subscriber stays counted, hanging all
|
|
272
|
+
// future publishes too. So: convert a rejection into a typed error
|
|
273
|
+
// (`tryPromise`), log-and-recover (`catchAll`) so the stream keeps
|
|
274
|
+
// draining, and run `done_` via `ensuring` so the countdown always
|
|
275
|
+
// advances regardless of outcome.
|
|
276
|
+
Effect.tryPromise(~catch=e => e, () => handler(msg.service, msg.meta, msg.json))
|
|
277
|
+
->Effect.catchAll(err =>
|
|
278
|
+
Effect.sync(() => Console.error2("[LocalBus] subscriber handler failed:", err))
|
|
279
|
+
)
|
|
280
|
+
->Effect.ensuring(msg.done_)
|
|
269
281
|
)
|
|
270
282
|
),
|
|
271
283
|
)
|
|
@@ -372,7 +384,11 @@ module Impl = (C: BusConfig): T => {
|
|
|
372
384
|
let pending = queue.contents
|
|
373
385
|
queue.contents = []
|
|
374
386
|
pending->Array.forEach(json => {
|
|
375
|
-
let _ =
|
|
387
|
+
let _ =
|
|
388
|
+
handler(json, ())->Promise.catch(e => {
|
|
389
|
+
Console.error2("[LocalBus] parked command handler failed:", e)
|
|
390
|
+
Promise.resolve()
|
|
391
|
+
})
|
|
376
392
|
})
|
|
377
393
|
| None => ()
|
|
378
394
|
}
|
|
@@ -414,7 +430,11 @@ module Impl = (C: BusConfig): T => {
|
|
|
414
430
|
// and done_ is called immediately. This prevents a deadlock where publishEvent on the
|
|
415
431
|
// EP topic would block until the downstream aggregate command chain completes.
|
|
416
432
|
let makeFireAndForgetHandler = handler => (_, _, json) => {
|
|
417
|
-
let _ =
|
|
433
|
+
let _ =
|
|
434
|
+
handler(json, ())->Promise.catch(e => {
|
|
435
|
+
Console.error2("[LocalBus] fire-and-forget handler failed:", e)
|
|
436
|
+
Promise.resolve()
|
|
437
|
+
})
|
|
418
438
|
Promise.resolve()
|
|
419
439
|
}
|
|
420
440
|
|