@reventlessdev/reventless-core 3.0.0-alpha.300 → 3.0.0-alpha.301
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 +14 -0
- package/README.md +3 -3
- package/package.json +9 -9
- package/src/{Behavior.res → Decision.res} +1 -1
- package/src/Message.res +1 -1
- package/src/ResourceAttribution.res +8 -8
- package/src/adapter/Auth/Auth_ActiveRole.res +1 -1
- package/src/adapter/Auth/Auth_RefusalVocabulary.res +1 -1
- package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +1 -1
- package/src/adapter/RuntimeExtension/RuntimeExtension.res +3 -3
- package/src/admin/Platform_BakedManifest.res +1 -1
- package/src/admin/Platform_PluginStructuresApi.res +1 -1
- package/src/admin/UiFragmentRegistry/StateChange/{UiFragmentRegistry_Behavior.res → UiFragmentRegistry_Decision.res} +1 -1
- package/src/admin/UiFragmentRegistry/StateChange/{UiFragmentRegistry_Behavior.res.mjs → UiFragmentRegistry_Decision.res.mjs} +1 -1
- package/src/components/Aggregate/Aggregate_Builder.res +2 -2
- package/src/components/Aggregate/Aggregate_Builder.res.mjs +2 -2
- package/src/components/Aggregate/Aggregate_Callback.res +17 -17
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +11 -11
- package/src/components/Api/ApiNoApiHelpers.res +1 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res +1 -1
- package/src/components/Api/SchemaType.res +1 -1
- package/src/components/CommandTopic/CommandTopic_Helpers.res +4 -4
- package/src/components/EventLog/EventLog_Operations.res +2 -2
- package/src/components/StateChangeSlice/StateChangeSlice.res +1 -1
- package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +3 -3
- package/src/components/StateChangeSlice/StateChangeSlice_Builder.res.mjs +3 -3
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +9 -9
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
- package/src/plugin/component/Plugin_Structure.res +1 -1
- package/src/plugin/lifecycle/{PluginBehavior.res → PluginDecision.res} +4 -4
- package/src/plugin/lifecycle/{PluginBehavior.res.mjs → PluginDecision.res.mjs} +1 -1
- package/tests/aggregate/AggregateCacheTest.res +4 -4
- package/tests/aggregate/AggregateCacheTest.res.mjs +2 -2
- package/tests/aggregate/AggregateCallbackTest.res +2 -2
- package/tests/aggregate/AggregateCallbackTest.res.mjs +1 -1
- package/tests/aggregate/AggregateConflictTest.res +4 -4
- package/tests/aggregate/AggregateConflictTest.res.mjs +2 -2
- package/tests/aggregate/AggregateFixtures.res +5 -5
- package/tests/aggregate/AggregateFixtures.res.mjs +2 -2
- package/tests/aggregate/AggregateRejectionTest.res +1 -1
- package/tests/aggregate/AggregateSnapshotTest.res +7 -7
- package/tests/aggregate/AggregateSnapshotTest.res.mjs +4 -4
- package/tests/api/PsInternalFieldsReadModel.res +1 -1
- package/tests/api/TaggedUnionSchemaTest.res +1 -1
- package/tests/commandgenerator/CommandGeneratorCallbackTest.res +1 -1
- package/tests/commandgenerator/CommandGeneratorFixtures.res +2 -2
- package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +2 -2
- package/tests/commandgenerator/OwnerActingTest.res +4 -4
- package/tests/commandgenerator/OwnerActingTest.res.mjs +4 -4
- package/tests/commandgenerator/OwnerStampingTest.res +1 -1
- package/tests/commandtopic/CommandOutcomeHookTest.res +2 -2
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +3 -3
- package/tests/dcb/DcbStateChangeSliceTest.res +5 -5
- package/tests/dcb/DcbStateChangeSliceTest.res.mjs +5 -5
- package/tests/plugin/PluginStructureAccessTest.res +1 -1
- package/tests/plugin/PluginStructureAccessTest.res.mjs +2 -2
- package/tests/plugin/PluginStructureTest.res +15 -15
- package/tests/plugin/PluginStructureTest.res.mjs +20 -20
- package/tests/projection/LifecycleTrailTest.res +1 -1
- /package/src/{Behavior.res.mjs → Decision.res.mjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.301 (2026-10-21)
|
|
7
|
+
|
|
8
|
+
* feat!: the write-side logic module is called the decision, and a codemod moves an app over ([cb573ff](https://github.com/ReventlessDev/reventless-core/commit/cb573ff5b928c7d7f3e75988dfaa65066f8c95eb))
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* <Spec>_Behavior.res, @@reventless.behavior,
|
|
13
|
+
Reventless.Behavior.T, StateChangeSlice.Behavior, Behavior_GWT and
|
|
14
|
+
thenRefused no longer exist. Run `npx reventless-rename-to-decision` at an
|
|
15
|
+
app's root, then rebuild; comments are not rewritten. The PPX must be the
|
|
16
|
+
matching release.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.300 (2026-10-19)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package @reventlessdev/reventless-core
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
The **provider-agnostic core** of [Reventless](https://docs.reventless.dev) — a
|
|
11
11
|
spec-driven, event-sourced CQRS framework written in [ReScript](https://rescript-lang.org).
|
|
12
12
|
This package is the runtime you model your domain against: the type-safe building blocks
|
|
13
|
-
for commands, events, projections, and the
|
|
13
|
+
for commands, events, projections, and the decisions that evolve state — with **no** cloud
|
|
14
14
|
or storage dependency. The provider-specific adapters live in separate packages
|
|
15
15
|
(see [Where it fits](#where-it-fits)).
|
|
16
16
|
|
|
@@ -19,8 +19,8 @@ or storage dependency. The provider-specific adapters live in separate packages
|
|
|
19
19
|
ReScript modules, consumed by adding the package to your `rescript.json` `dependencies`:
|
|
20
20
|
|
|
21
21
|
- **`Message`** — command / event message contracts and their service interface.
|
|
22
|
-
- **`
|
|
23
|
-
(`evolve(state, event) => state`)
|
|
22
|
+
- **`Decision`** — the write side's decision module type (the decider pattern):
|
|
23
|
+
event-sourced state transitions (`evolve(state, event) => state`) and `decide`.
|
|
24
24
|
- **`Projection`** / **`ProjectionMapper`** — read models projected from event streams.
|
|
25
25
|
- **`Mapper`** / **`Mapper1toN`** / **`MapperNto1`** — cross-component event translation
|
|
26
26
|
(encode/decode between a source and a target contract).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.301",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -28,17 +28,17 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"sury": "11.0.0",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
|
-
"@reventlessdev/rescript-
|
|
32
|
-
"@reventlessdev/rescript-
|
|
33
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.
|
|
34
|
-
"@reventlessdev/rescript-
|
|
31
|
+
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.23",
|
|
32
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
33
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
34
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.23",
|
|
35
35
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.15",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
37
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.24",
|
|
38
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.198",
|
|
37
39
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
|
|
38
|
-
"@reventlessdev/reventless-
|
|
39
|
-
"@reventlessdev/reventless-
|
|
40
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.169",
|
|
41
|
-
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.23"
|
|
40
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.42",
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.170"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.1",
|
package/src/Message.res
CHANGED
|
@@ -308,7 +308,7 @@ let metaKeys = [
|
|
|
308
308
|
|
|
309
309
|
`StoredEvent` is the logical shape (nested `meta:meta`). The on-disk shape is
|
|
310
310
|
a flat top-level dict so DynamoDB GSIs can project individual meta attributes
|
|
311
|
-
(the §1.2/§3.2
|
|
311
|
+
(the §1.2/§3.2 behavior today, preserved here). These two helpers are the
|
|
312
312
|
single bridge between the two — used by both EventLog (aggregate) and
|
|
313
313
|
DcbEventLog (DCB) storage paths.
|
|
314
314
|
*/
|
|
@@ -7,11 +7,11 @@ drifting into private spellings of the same fact.
|
|
|
7
7
|
|
|
8
8
|
The three facts are deliberately independent:
|
|
9
9
|
|
|
10
|
-
- `ComponentType.t` — the *
|
|
10
|
+
- `ComponentType.t` — the *modeling* kind (Aggregate, StateViewSlice, …): what
|
|
11
11
|
the element IS in the domain model.
|
|
12
|
-
- `Role.t` — the *deployment
|
|
13
|
-
this particular resource does for that element. One component owns
|
|
14
|
-
|
|
12
|
+
- `Role.t` — the *deployment role* (EventLog, QueryDb, Runtime, …): what job
|
|
13
|
+
this particular resource does for that element. One component owns resources in
|
|
14
|
+
several roles, so kind alone can't identify a resource.
|
|
15
15
|
- `Scope.t` — the level the resource is attributed to: an individual component,
|
|
16
16
|
a plugin's shared substrate, or platform substrate.
|
|
17
17
|
|
|
@@ -40,8 +40,8 @@ module Scope = {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
The deployment
|
|
44
|
-
for support
|
|
43
|
+
The deployment role a resource plays. `Other(string)` is the escape hatch
|
|
44
|
+
for support roles and future ones (same pattern as `Monitoring.unitKind`).
|
|
45
45
|
*/
|
|
46
46
|
module Role = {
|
|
47
47
|
type t =
|
|
@@ -105,11 +105,11 @@ module Role = {
|
|
|
105
105
|
/**
|
|
106
106
|
The model component a provisioned resource belongs to.
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
Role builders (EventLog, QueryDb, CommandTopic, EventTopic, EventCollector) are
|
|
109
109
|
shared building blocks applied as functors by many different component kinds — a
|
|
110
110
|
QueryDb is instantiated by ReadModel, StateViewSlice, AutomationSlice, both
|
|
111
111
|
translation slices and Counter. The adapter that provisions the physical resource
|
|
112
|
-
sits at the bottom of that chain and would otherwise only know its own
|
|
112
|
+
sits at the bottom of that chain and would otherwise only know its own role, so
|
|
113
113
|
`kind` would collapse onto `role` and the owning component would be unrecoverable.
|
|
114
114
|
The owning builder passes this record down instead.
|
|
115
115
|
|
|
@@ -69,7 +69,7 @@ boundary — and a table copied into two test suites is exactly the drift it exi
|
|
|
69
69
|
to prevent.
|
|
70
70
|
|
|
71
71
|
`expected` is the group set the minted token must carry. `None` means the request
|
|
72
|
-
must be **refused** rather than
|
|
72
|
+
must be **refused** rather than honored, ignored, or reduced to the empty set —
|
|
73
73
|
the distinction §3 turns on. A path with no client to refuse (the trigger, reading
|
|
74
74
|
stored state) resolves `None` by minting `membership` and marking
|
|
75
75
|
[staleRoleClaim]; a path answering a live request refuses it.
|
|
@@ -120,7 +120,7 @@ Classify a refusal without knowing which adapter produced it.
|
|
|
120
120
|
|
|
121
121
|
Pass what the response actually carried: its status, the `errorType` of a GraphQL
|
|
122
122
|
error if there was one, and `extensions.code` if there was one. Returns `None`
|
|
123
|
-
when the response is not a refusal this contract
|
|
123
|
+
when the response is not a refusal this contract recognizes — which includes a
|
|
124
124
|
successful response, so a caller must not read `None` as "allowed".
|
|
125
125
|
|
|
126
126
|
Ordering matters and is the point of the function. The transport status is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
Deploy-time hook: every event log announces itself as it is provisioned, so an
|
|
3
|
-
extension can attach
|
|
3
|
+
extension can attach behavior to it inside the program that owns it.
|
|
4
4
|
|
|
5
5
|
No-op by default. Zero behavioral change unless an extension registers a backend
|
|
6
6
|
via `use` before the platform/plugin build. The seam fires once per log at
|
|
@@ -8,7 +8,7 @@ No-op by default. Zero behavioral change unless an extension registers one via
|
|
|
8
8
|
no code runs.
|
|
9
9
|
|
|
10
10
|
`Monitoring` and `EventLogProvisioning` are the deploy-time half of this shape —
|
|
11
|
-
they let an extension attach
|
|
11
|
+
they let an extension attach behavior to *what gets provisioned*. This is the
|
|
12
12
|
runtime half: *what runs inside it*. Without it,
|
|
13
13
|
`CommandGenerator_Callback.registerCommandInterceptor`,
|
|
14
14
|
`QueryDb_Callback.registerQueryInterceptor` and the two
|
|
@@ -34,7 +34,7 @@ to internals would make it a second, informal handler API — and the four hooks
|
|
|
34
34
|
exists to reach already receive their own per-request payloads
|
|
35
35
|
(`commandInterceptor` gets `~componentName` / `~tag` / `~args` on every call).
|
|
36
36
|
|
|
37
|
-
`~runtimeKind` is the
|
|
37
|
+
`~runtimeKind` is the modeling kind of the component the runtime runs — the same
|
|
38
38
|
`ComponentType.t` every AWS runtime builder already passes to
|
|
39
39
|
`RuntimeEnvironment_Lambda.makeFromCodeAsset`, so there is no third taxonomy to
|
|
40
40
|
keep in step with `Monitoring.unitKind` and `ResourceAttribution.Role`.
|
|
@@ -67,7 +67,7 @@ runtime handles its first request.
|
|
|
67
67
|
seam reachable in a *deployed* runtime: registering a first-class module only
|
|
68
68
|
populates the deploy program's process, and the Lambda is a different one, so the
|
|
69
69
|
framework bundles the module's package into the code archive and names the module
|
|
70
|
-
in the runtime's config for the entry shell to import. Spec and
|
|
70
|
+
in the runtime's config for the entry shell to import. Spec and decision modules
|
|
71
71
|
are carried into runtimes the same way. The convention matches
|
|
72
72
|
`@@reventless.spec`'s injected `let moduleUrl`, so a ReScript extension can lift
|
|
73
73
|
it from there.
|
|
@@ -57,7 +57,7 @@ let describe = (e: error): string =>
|
|
|
57
57
|
// gives. Unlike the other two, what it selects is not filtered here — there is
|
|
58
58
|
// nothing in a `pluginStructure` to filter, because the pages do not exist until
|
|
59
59
|
// a shell builds them. It travels to the shell instead, which is the only place
|
|
60
|
-
// that can
|
|
60
|
+
// that can honor it.
|
|
61
61
|
type selection = {
|
|
62
62
|
plugin: string,
|
|
63
63
|
views: option<array<string>>,
|
|
@@ -43,7 +43,7 @@ let sdlTypes: array<string> = [
|
|
|
43
43
|
// reason the capability above is: a plugin built against a newer framework,
|
|
44
44
|
// naming a posture this schema has never heard of, still decodes.
|
|
45
45
|
//
|
|
46
|
-
// It records ORIGIN, never
|
|
46
|
+
// It records ORIGIN, never behavior — a grafted file is the host's to edit
|
|
47
47
|
// afterwards — so a reader must not paint a declared graft as a verified one.
|
|
48
48
|
`type Platform_TraitDeclaration {\n trait: String!\n version: String!\n posture: String!\n component: String!\n}`,
|
|
49
49
|
// `extensionPoints` / `requiredStores` / `requiredStoreDeclarations` /
|
|
@@ -68,7 +68,7 @@ let Spec;
|
|
|
68
68
|
|
|
69
69
|
let initialState;
|
|
70
70
|
|
|
71
|
-
let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChange/
|
|
71
|
+
let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry_Decision.res.mjs";
|
|
72
72
|
|
|
73
73
|
export {
|
|
74
74
|
Spec,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Make = (
|
|
2
2
|
Spec: Reventless.Aggregate.Spec,
|
|
3
|
-
|
|
3
|
+
Decision: Decision.T with module Spec := Spec,
|
|
4
4
|
EventMappings: EventMapper.Mappings with module Target := Spec,
|
|
5
5
|
RuntimeEnvironment: Runtime.Environment,
|
|
6
6
|
CommandGeneratorResolvers: CommandGenerator_Adapter.Resolvers
|
|
@@ -87,7 +87,7 @@ module Make = (
|
|
|
87
87
|
->Pulumi.Output.apply(eventLogOps => {
|
|
88
88
|
module AggregateCallback = Aggregate_Callback.Make(
|
|
89
89
|
Spec,
|
|
90
|
-
|
|
90
|
+
Decision,
|
|
91
91
|
{
|
|
92
92
|
module Spec = Spec
|
|
93
93
|
module EventLog = SpecificEventLog
|
|
@@ -16,7 +16,7 @@ import * as CommandGenerator_Builder$ReventlessCore from "../CommandGenerator/Co
|
|
|
16
16
|
import * as CommandGenerator_Callback$ReventlessCore from "../CommandGenerator/CommandGenerator_Callback.res.mjs";
|
|
17
17
|
|
|
18
18
|
function Make(Spec) {
|
|
19
|
-
return
|
|
19
|
+
return Decision => (EventMappings => (RuntimeEnvironment => (CommandGeneratorResolvers => (CommandTopicChannel => (EventLogStorage => (EventTopicPublisher => (EventCollectorChannel => (AggregateRuntimeBuilder => (HooksConfig => {
|
|
20
20
|
let SpecificEventLog = EventLog_Builder$ReventlessCore.Make(Spec)(EventLogStorage)(EventTopicPublisher);
|
|
21
21
|
let SpecificCommandTopic = CommandTopic_Builder$ReventlessCore.Make({
|
|
22
22
|
Id: Spec.Id,
|
|
@@ -36,7 +36,7 @@ function Make(Spec) {
|
|
|
36
36
|
name: Spec.name
|
|
37
37
|
}, opts);
|
|
38
38
|
return Component$ReventlessCore.operations(eventLog).apply(eventLogOps => {
|
|
39
|
-
let AggregateCallback = Aggregate_Callback$ReventlessCore.Make(Spec)(
|
|
39
|
+
let AggregateCallback = Aggregate_Callback$ReventlessCore.Make(Spec)(Decision)({
|
|
40
40
|
Spec: Spec,
|
|
41
41
|
EventLog: SpecificEventLog,
|
|
42
42
|
eventLog: eventLogOps
|
|
@@ -16,7 +16,7 @@ module type T = {
|
|
|
16
16
|
|
|
17
17
|
module Make = (
|
|
18
18
|
Spec: Reventless.Aggregate.Spec,
|
|
19
|
-
|
|
19
|
+
Decision: Decision.T with module Spec := Spec,
|
|
20
20
|
Ops: Ops with module Spec = Spec,
|
|
21
21
|
): (T with module Spec = Spec) => {
|
|
22
22
|
module Spec = Spec
|
|
@@ -82,9 +82,9 @@ module Make = (
|
|
|
82
82
|
| CmdRefused(CommandTopic_Helpers.ownershipRefusal)
|
|
83
83
|
|
|
84
84
|
// Folds a single command into the running accumulator: (currentState, perCommandOutcomes).
|
|
85
|
-
// A domain rejection from
|
|
85
|
+
// A domain rejection from Decision.decide is recorded as `CmdRejected` (no state change,
|
|
86
86
|
// surviving commands in the batch continue); a successful decide records `CmdOk(events)`
|
|
87
|
-
// and advances state via `
|
|
87
|
+
// and advances state via `Decision.evolve`.
|
|
88
88
|
//
|
|
89
89
|
// `~seq` is the replayed sequence number the batch decides from, carried in so
|
|
90
90
|
// the line below can name the state rather than serialise it.
|
|
@@ -94,7 +94,7 @@ module Make = (
|
|
|
94
94
|
let processCommand = (~seq: int) =>
|
|
95
95
|
(
|
|
96
96
|
(state, owners, outcomes): (
|
|
97
|
-
|
|
97
|
+
Decision.state,
|
|
98
98
|
array<string>,
|
|
99
99
|
array<(string, cmdOutcome, Message.meta)>,
|
|
100
100
|
),
|
|
@@ -131,9 +131,9 @@ module Make = (
|
|
|
131
131
|
)->Effect.runSync
|
|
132
132
|
(state, owners, Array.concat(outcomes, [(reference, CmdRefused(refused), meta)]))
|
|
133
133
|
| None =>
|
|
134
|
-
switch
|
|
134
|
+
switch Decision.decide(state, command'.command) {
|
|
135
135
|
| Ok(generatedEvents) =>
|
|
136
|
-
let newState = generatedEvents->Array.reduce(state,
|
|
136
|
+
let newState = generatedEvents->Array.reduce(state, Decision.evolve)
|
|
137
137
|
(
|
|
138
138
|
newState,
|
|
139
139
|
generatedEvents->Array.reduce(owners, recordEventOwners),
|
|
@@ -179,7 +179,7 @@ module Make = (
|
|
|
179
179
|
// the fixed capacity (a per-aggregate knob is a future refinement — see
|
|
180
180
|
// docs/plans/done/aggregate-snapshotting.md).
|
|
181
181
|
let replayCacheCapacity = 100
|
|
182
|
-
let replayCache: Lru.t<string, (
|
|
182
|
+
let replayCache: Lru.t<string, (Decision.state, int, array<string>)> = Lru.make(
|
|
183
183
|
~capacity=replayCacheCapacity,
|
|
184
184
|
)
|
|
185
185
|
|
|
@@ -195,8 +195,8 @@ module Make = (
|
|
|
195
195
|
// Off for an owner-marked aggregate: a snapshot holds the state alone, so a
|
|
196
196
|
// replay seeded from one would never see the event that recorded the owner,
|
|
197
197
|
// and the ownership check would pass every caller.
|
|
198
|
-
let snapshotConfig = actsOnOwned ? None :
|
|
199
|
-
if actsOnOwned &&
|
|
198
|
+
let snapshotConfig = actsOnOwned ? None : Decision.snapshot
|
|
199
|
+
if actsOnOwned && Decision.snapshot->Option.isSome {
|
|
200
200
|
EffectLogger.logWarn(
|
|
201
201
|
~comp,
|
|
202
202
|
"snapshots disabled: this aggregate records an @owner, which a snapshot does not carry",
|
|
@@ -223,7 +223,7 @@ module Make = (
|
|
|
223
223
|
// State ⇄ JSON via the configured stateSchema. Both swallow sury failures to
|
|
224
224
|
// `None` — an unserializable state just skips the snapshot write, and an
|
|
225
225
|
// undecodable snapshot falls back to full replay; neither can fail a command.
|
|
226
|
-
let encodeState = (state:
|
|
226
|
+
let encodeState = (state: Decision.state): option<JSON.t> =>
|
|
227
227
|
switch snapshotConfig {
|
|
228
228
|
| Some({stateSchema}) =>
|
|
229
229
|
switch state->Reventless.Util_Sury.toJson(stateSchema) {
|
|
@@ -233,7 +233,7 @@ module Make = (
|
|
|
233
233
|
| None => None
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
let decodeState = (json: JSON.t): option<
|
|
236
|
+
let decodeState = (json: JSON.t): option<Decision.state> =>
|
|
237
237
|
switch snapshotConfig {
|
|
238
238
|
| Some({stateSchema}) =>
|
|
239
239
|
switch json->Reventless.Util_Sury.fromJson(stateSchema) {
|
|
@@ -250,7 +250,7 @@ module Make = (
|
|
|
250
250
|
let coldReadState = id => {
|
|
251
251
|
let idStr = id->Spec.Id.toString
|
|
252
252
|
let seedEffect = switch snapshotConfig {
|
|
253
|
-
| None => Effect.succeed((
|
|
253
|
+
| None => Effect.succeed((Decision.initialState, 0))
|
|
254
254
|
| Some(_) =>
|
|
255
255
|
Effect.promise(() => Ops.eventLog.latestSnapshot(id))->Effect.map(snapResult =>
|
|
256
256
|
switch snapResult {
|
|
@@ -262,28 +262,28 @@ module Make = (
|
|
|
262
262
|
~comp,
|
|
263
263
|
`snapshot ignored (undecodable): id=${idStr}, seq=${snap.seqNr->Int.toString} — full replay`,
|
|
264
264
|
)->Effect.runSync
|
|
265
|
-
(
|
|
265
|
+
(Decision.initialState, 0)
|
|
266
266
|
}
|
|
267
267
|
| Ok(Some(snap)) =>
|
|
268
268
|
EffectLogger.logWarn(
|
|
269
269
|
~comp,
|
|
270
270
|
`snapshot ignored (schema drift): id=${idStr}, seq=${snap.EventLog.seqNr->Int.toString} — full replay`,
|
|
271
271
|
)->Effect.runSync
|
|
272
|
-
(
|
|
273
|
-
| Ok(None) => (
|
|
272
|
+
(Decision.initialState, 0)
|
|
273
|
+
| Ok(None) => (Decision.initialState, 0)
|
|
274
274
|
| Error(msg) =>
|
|
275
275
|
EffectLogger.logWarn(
|
|
276
276
|
~comp,
|
|
277
277
|
`snapshot read failed (ignored): id=${idStr}: ${msg} — full replay`,
|
|
278
278
|
)->Effect.runSync
|
|
279
|
-
(
|
|
279
|
+
(Decision.initialState, 0)
|
|
280
280
|
}
|
|
281
281
|
)
|
|
282
282
|
}
|
|
283
283
|
seedEffect->Effect.flatMap(((seedState, seedSeq)) =>
|
|
284
284
|
Ops.eventLog.replayStream(id, ~fromSeq=seedSeq)
|
|
285
285
|
->Stream.runFold((seedState, seedSeq, []), ((st, n, owners), ev) => (
|
|
286
|
-
|
|
286
|
+
Decision.evolve(st, ev),
|
|
287
287
|
n + 1,
|
|
288
288
|
owners->recordEventOwners(ev),
|
|
289
289
|
))
|
|
@@ -20,7 +20,7 @@ import * as SchemaWalker$ReventlessCore from "../../plugin/component/SchemaWalke
|
|
|
20
20
|
import * as CommandTopic_Helpers$ReventlessCore from "../CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
21
21
|
|
|
22
22
|
function Make(Spec) {
|
|
23
|
-
return
|
|
23
|
+
return Decision => (Ops => {
|
|
24
24
|
let comp = `Aggregate(` + Spec.name + `)`;
|
|
25
25
|
let groupTopicItemsByIdStream = stream => Effect.map(Stream.runFold(stream, {}, (dict, item) => {
|
|
26
26
|
let id = Spec.Id.toString(item.command.id);
|
|
@@ -73,10 +73,10 @@ function Make(Spec) {
|
|
|
73
73
|
]])
|
|
74
74
|
];
|
|
75
75
|
}
|
|
76
|
-
let generatedEvents =
|
|
76
|
+
let generatedEvents = Decision.decide(state, command$p.command);
|
|
77
77
|
if (generatedEvents.TAG === "Ok") {
|
|
78
78
|
let generatedEvents$1 = generatedEvents._0;
|
|
79
|
-
let newState = Stdlib_Array.reduce(generatedEvents$1, state,
|
|
79
|
+
let newState = Stdlib_Array.reduce(generatedEvents$1, state, Decision.evolve);
|
|
80
80
|
return [
|
|
81
81
|
newState,
|
|
82
82
|
Stdlib_Array.reduce(generatedEvents$1, owners, recordEventOwners),
|
|
@@ -112,8 +112,8 @@ function Make(Spec) {
|
|
|
112
112
|
});
|
|
113
113
|
let replayCache = Lru$ReventlessCore.make(100);
|
|
114
114
|
let resetCache = () => Lru$ReventlessCore.clear(replayCache);
|
|
115
|
-
let snapshotConfig = actsOnOwned ? undefined :
|
|
116
|
-
if (actsOnOwned && Stdlib_Option.isSome(
|
|
115
|
+
let snapshotConfig = actsOnOwned ? undefined : Decision.snapshot;
|
|
116
|
+
if (actsOnOwned && Stdlib_Option.isSome(Decision.snapshot)) {
|
|
117
117
|
Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, "snapshots disabled: this aggregate records an @owner, which a snapshot does not carry"));
|
|
118
118
|
}
|
|
119
119
|
let stateSchemaHash = snapshotConfig !== undefined ? HashObject(Object.fromEntries([[
|
|
@@ -153,7 +153,7 @@ function Make(Spec) {
|
|
|
153
153
|
let snap = snapResult._0;
|
|
154
154
|
if (snap === undefined) {
|
|
155
155
|
return [
|
|
156
|
-
|
|
156
|
+
Decision.initialState,
|
|
157
157
|
0
|
|
158
158
|
];
|
|
159
159
|
}
|
|
@@ -167,24 +167,24 @@ function Make(Spec) {
|
|
|
167
167
|
} else {
|
|
168
168
|
Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `snapshot ignored (undecodable): id=` + idStr + `, seq=` + snap.seqNr.toString() + ` — full replay`));
|
|
169
169
|
return [
|
|
170
|
-
|
|
170
|
+
Decision.initialState,
|
|
171
171
|
0
|
|
172
172
|
];
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `snapshot ignored (schema drift): id=` + idStr + `, seq=` + snap.seqNr.toString() + ` — full replay`));
|
|
176
176
|
return [
|
|
177
|
-
|
|
177
|
+
Decision.initialState,
|
|
178
178
|
0
|
|
179
179
|
];
|
|
180
180
|
}
|
|
181
181
|
Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `snapshot read failed (ignored): id=` + idStr + `: ` + snapResult._0 + ` — full replay`));
|
|
182
182
|
return [
|
|
183
|
-
|
|
183
|
+
Decision.initialState,
|
|
184
184
|
0
|
|
185
185
|
];
|
|
186
186
|
}) : Effect.succeed([
|
|
187
|
-
|
|
187
|
+
Decision.initialState,
|
|
188
188
|
0
|
|
189
189
|
]);
|
|
190
190
|
return Effect.flatMap(seedEffect, param => {
|
|
@@ -194,7 +194,7 @@ function Make(Spec) {
|
|
|
194
194
|
seedSeq,
|
|
195
195
|
[]
|
|
196
196
|
], (param, ev) => [
|
|
197
|
-
|
|
197
|
+
Decision.evolve(param[0], ev),
|
|
198
198
|
param[1] + 1 | 0,
|
|
199
199
|
recordEventOwners(param[2], ev)
|
|
200
200
|
]), param => {
|
|
@@ -5,7 +5,7 @@ let isNoApi = (commandSchema: S.t<unknown>): bool =>
|
|
|
5
5
|
|
|
6
6
|
// Variants excluded by a mark on the member itself. This is the half that
|
|
7
7
|
// survives a spread: splicing copies members, not the metadata of the union they
|
|
8
|
-
// came from, so a command a trait marked internal is only still
|
|
8
|
+
// came from, so a command a trait marked internal is only still recognizable here.
|
|
9
9
|
let excludedByMembers = (commandSchema: S.t<unknown>): array<string> =>
|
|
10
10
|
switch commandSchema {
|
|
11
11
|
| AnyOf({anyOf}) =>
|
|
@@ -96,7 +96,7 @@ let rec fromSchemaType = (
|
|
|
96
96
|
// names come from the schema — see `Reventless.TaggedUnion` — so the same union
|
|
97
97
|
// is the same type wherever it appears, which is what lets AppSync merge each
|
|
98
98
|
// source API's copy back into one, and what lets the write path stamp a
|
|
99
|
-
// `__typename` the SDL will
|
|
99
|
+
// `__typename` the SDL will recognize.
|
|
100
100
|
| TaggedUnion(name, arms) =>
|
|
101
101
|
if asInput {
|
|
102
102
|
// GraphQL has no input unions. A command that wants one takes its arms as
|
|
@@ -421,7 +421,7 @@ let trailShape = (
|
|
|
421
421
|
let shape = ArrayOf(ObjectRef(entryName, fields))
|
|
422
422
|
// Walking the entries by shape skips the semantic dispatch `fromSury` does,
|
|
423
423
|
// so the marker is re-applied here. Without it the trail reaches a consumer
|
|
424
|
-
// as an unremarkable array of objects, and
|
|
424
|
+
// as an unremarkable array of objects, and recognizing it would come down to
|
|
425
425
|
// the field's name.
|
|
426
426
|
switch Reventless.Semantic.get(schema) {
|
|
427
427
|
| Some(sem) => Semantic(sem, shape)
|
|
@@ -12,7 +12,7 @@ during inline `publishJsonsAndWait` execution and read by `runInlineAndCollect`
|
|
|
12
12
|
type acceptedResult = {entityId?: string, eventCount: int}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
Carries an encoded domain rejection from `
|
|
15
|
+
Carries an encoded domain rejection from `Decision.decide` back to a synchronous producer.
|
|
16
16
|
`errorCode` is the variant tag of `Spec.errorSchema` (e.g. `"AlreadyExists"`); `errorDetail`
|
|
17
17
|
is the JSON-stringified error payload (or empty string for payload-less variants).
|
|
18
18
|
*/
|
|
@@ -21,7 +21,7 @@ type rejectedResult = {errorCode: string, errorDetail: string}
|
|
|
21
21
|
/**
|
|
22
22
|
Why a command was refused.
|
|
23
23
|
|
|
24
|
-
`DomainRejection` is `
|
|
24
|
+
`DomainRejection` is `Decision.decide` returning `Error` — the model declining the command,
|
|
25
25
|
which is the model working. `InfrastructureFailure` is the decision never landing: an event-log
|
|
26
26
|
append that failed outright or exhausted its conflict retries. Both arrive on `reportRejected`,
|
|
27
27
|
so without this a consumer would read a broken event log as a business rejection.
|
|
@@ -92,7 +92,7 @@ let fireCommandOutcome = (~component: string, ~reference: string, outcome: obser
|
|
|
92
92
|
let acceptedResultChannel: ref<option<(string, acceptedResult) => unit>> = ref(None)
|
|
93
93
|
|
|
94
94
|
// Sibling side-channel for domain rejections — populated by callbacks when
|
|
95
|
-
// `
|
|
95
|
+
// `Decision.decide` returns `Error`. Read by runInlineAndCollect to build
|
|
96
96
|
// `Rejected` outcomes that carry the real error code and detail.
|
|
97
97
|
let rejectedResultChannel: ref<option<(string, rejectedResult) => unit>> = ref(None)
|
|
98
98
|
|
|
@@ -234,7 +234,7 @@ let runInlineAndCollect = async (
|
|
|
234
234
|
rejectedResultChannel.contents = None
|
|
235
235
|
jsons->Array.mapWithIndex((cmdJson, i) => {
|
|
236
236
|
let msgId = cmdJson.meta.msgId
|
|
237
|
-
// Domain rejection from
|
|
237
|
+
// Domain rejection from Decision.decide takes precedence over both Accepted
|
|
238
238
|
// and the synthesized "Conflict" — it carries the most informative error.
|
|
239
239
|
switch rejectedResults->Dict.get(msgId) {
|
|
240
240
|
| Some({errorCode, errorDetail}) =>
|
|
@@ -17,8 +17,8 @@ module type T = {
|
|
|
17
17
|
|
|
18
18
|
// Retry schedule for transient storage errors.
|
|
19
19
|
// Exponential backoff: 100ms, ~200ms, ~400ms, ~800ms, ~1600ms — max 5 retries.
|
|
20
|
-
// Only retries on
|
|
21
|
-
// Retry only
|
|
20
|
+
// Only retries on recognized transient error messages; permanent errors propagate immediately.
|
|
21
|
+
// Retry only recognized transient storage failures — never a Conflict (a retry
|
|
22
22
|
// of the same append would just conflict again; the OCC retry happens a layer up
|
|
23
23
|
// via replay+re-decide).
|
|
24
24
|
let isTransient = (e: EventLog.appendError) =>
|
|
@@ -17,7 +17,7 @@ let toResolvedOutputs = (outputs: outputs): Pulumi.Output.t<
|
|
|
17
17
|
|
|
18
18
|
module type T = {
|
|
19
19
|
module Spec: Reventless.StateChangeSlice.Spec
|
|
20
|
-
module
|
|
20
|
+
module Decision: Reventless.StateChangeSlice.Decision with module Spec := Spec
|
|
21
21
|
/** `true` when built with `Platform.StateChangeSlice.MakeAsync` — uses FIFO channel, returns `CommandPending`. */
|
|
22
22
|
let isAsync: bool
|
|
23
23
|
type component = Component.t<t, outputs, operations>
|
|
@@ -2,13 +2,13 @@ let log = Logger.fromEnv()
|
|
|
2
2
|
|
|
3
3
|
module Make = (
|
|
4
4
|
Spec: Reventless.StateChangeSlice.Spec,
|
|
5
|
-
|
|
5
|
+
Decision: Reventless.StateChangeSlice.Decision with module Spec := Spec,
|
|
6
6
|
): StateChangeSlice.T => {
|
|
7
7
|
module Spec = Spec
|
|
8
|
-
module
|
|
8
|
+
module Decision = Decision
|
|
9
9
|
let isAsync = false
|
|
10
10
|
type component = StateChangeSlice.component
|
|
11
|
-
module Callback = StateChangeSlice_Callback.Make(Spec,
|
|
11
|
+
module Callback = StateChangeSlice_Callback.Make(Spec, Decision)
|
|
12
12
|
|
|
13
13
|
let makeJsonHandler = (
|
|
14
14
|
~tagKeysByEventType,
|
|
@@ -16,8 +16,8 @@ import * as StateChangeSlice_Callback$ReventlessCore from "./StateChangeSlice_Ca
|
|
|
16
16
|
let log = Logger$ReventlessCore.fromEnv();
|
|
17
17
|
|
|
18
18
|
function Make(Spec) {
|
|
19
|
-
return
|
|
20
|
-
let Callback = StateChangeSlice_Callback$ReventlessCore.Make(Spec)(
|
|
19
|
+
return Decision => {
|
|
20
|
+
let Callback = StateChangeSlice_Callback$ReventlessCore.Make(Spec)(Decision);
|
|
21
21
|
let makeJsonHandler = (tagKeysByEventType, crossPartitionTagKeys, partitionTag, dcbEventLogOps) => (stream => {
|
|
22
22
|
let decodedStream = Stream.flatMap(Stream.mapEffect(stream, param => {
|
|
23
23
|
let reference = param.reference;
|
|
@@ -67,7 +67,7 @@ function Make(Spec) {
|
|
|
67
67
|
};
|
|
68
68
|
return {
|
|
69
69
|
Spec: Spec,
|
|
70
|
-
|
|
70
|
+
Decision: Decision,
|
|
71
71
|
isAsync: false,
|
|
72
72
|
make: make
|
|
73
73
|
};
|