@reventlessdev/reventless-core 3.0.0-alpha.237 → 3.0.0-alpha.238
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 +12 -0
- package/package.json +7 -7
- package/src/Message.res +1 -1
- package/src/adapter/Monitoring/Monitoring.res +1 -1
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res +1 -1
- package/src/components/Aggregate/Aggregate_Callback.res +2 -2
- package/src/components/Api/ApiAllowedStatesHelpers.res +2 -3
- package/src/components/Api/ApiTargetStateHelpers.res +4 -3
- package/src/components/Dcb/Dcb_Builder.res +79 -16
- package/src/components/Dcb/Dcb_Builder.res.mjs +41 -5
- package/src/components/EventLog/EventLog.res +1 -1
- package/src/plugin/component/Plugin_Structure.res +269 -7
- package/src/plugin/component/Plugin_Structure.res.mjs +128 -1
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +1 -1
- package/tests/aggregate/AggregateCacheTest.res +1 -1
- package/tests/aggregate/AggregateSnapshotTest.res +1 -1
- package/tests/commandgenerator/OwnerStampingTest.res +57 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +19 -0
- package/tests/message/MessageTest.res +1 -1
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +1 -1
- package/tests/plugin/PluginStructureTest.res +164 -3
- package/tests/plugin/PluginStructureTest.res.mjs +170 -1
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res +31 -0
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +73 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res +16 -1
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -2
- package/tests/plugin/StateViewSlice/PsShipmentsView.res +26 -0
- package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +101 -0
- package/tests/util/CsvStreamTest.res +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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.238 (2026-08-16)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core:** stamp the owner from each DCB slice's own command schema ([6edbdf4](https://github.com/ReventlessDev/reventless-core/commit/6edbdf4680f2535597c3b560215aa514ceed27eb))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **core:** check a declared lifecycle edge against the states that exist ([d427d92](https://github.com/ReventlessDev/reventless-core/commit/d427d92f8912b4dc9a9407c3c0cec3200d41a5f1))
|
|
14
|
+
* **core:** report a lifecycle state no command can reach ([eb24dbd](https://github.com/ReventlessDev/reventless-core/commit/eb24dbd90df07185f0f4785c4bb726f5b617e7b2))
|
|
15
|
+
* **ppx:** declare a command's lifecycle edge once, as [@transition](https://github.com/transition) ([dd35130](https://github.com/ReventlessDev/reventless-core/commit/dd3513014f14b71879ee21263c6755d3c3d95096))
|
|
16
|
+
|
|
17
|
+
|
|
6
18
|
# 3.0.0-alpha.237 (2026-08-16)
|
|
7
19
|
|
|
8
20
|
### Features
|
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.238",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,21 +29,21 @@
|
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
32
|
-
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
33
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.7",
|
|
34
32
|
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.7",
|
|
33
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
35
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
36
|
-
"@reventlessdev/rescript-
|
|
35
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.7",
|
|
37
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
37
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.7",
|
|
38
38
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
39
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.143",
|
|
40
39
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.31",
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.116",
|
|
41
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.144"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
45
45
|
"sury-ppx": "11.0.0-alpha.2",
|
|
46
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
46
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.68"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"rescript": "12.3.0"
|
package/src/Message.res
CHANGED
|
@@ -69,7 +69,7 @@ let toCommandSchema' = (idSchema, commandSchema) =>
|
|
|
69
69
|
// before a nested `@schema` field existed still decodes instead of bricking the
|
|
70
70
|
// aggregate's log replay. These two entry points thread the memoized envelope schema;
|
|
71
71
|
// `Reventless.Message.decode` (the other central decoder) is tolerant at the source.
|
|
72
|
-
// See docs/plans/platform-infrastructure-in-plugin-list.md (durable fix option 2).
|
|
72
|
+
// See docs/plans/done/platform-infrastructure-in-plugin-list.md (durable fix option 2).
|
|
73
73
|
let decodeEvent' = (json, idSchema, eventSchema) =>
|
|
74
74
|
json->parseJsonTolerant(toEventSchema'(idSchema, eventSchema))
|
|
75
75
|
let decodeCommand' = (json, idSchema, commandSchema) =>
|
|
@@ -10,7 +10,7 @@ concern; this only exposes the choke point through which the framework says
|
|
|
10
10
|
"I just provisioned an execution unit" and leaves what to do about it to the
|
|
11
11
|
listener.
|
|
12
12
|
|
|
13
|
-
See `docs/plans/monitoring-hook-seam.md`.
|
|
13
|
+
See `docs/plans/done/monitoring-hook-seam.md`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// UiFragmentRegistry StateChangeSlice — the write side of the platform UI-fragment
|
|
2
2
|
// registry, extracted off the Plugin aggregate (see
|
|
3
|
-
// docs/plans/event-sourced-fragment-registries.md).
|
|
3
|
+
// docs/plans/done/event-sourced-fragment-registries.md).
|
|
4
4
|
//
|
|
5
5
|
// Commands are driven by the plugin runtime's connect handshake and routed here by the
|
|
6
6
|
// admin PluginExtensionPoint's UI-fragment mapping (RegisterUiFragment on ConnectPlugin,
|
|
@@ -114,7 +114,7 @@ module Make = (
|
|
|
114
114
|
// at append time → the conflict branch invalidates the entry and the retry
|
|
115
115
|
// replays cold. Mirrors the StateChangeSlice decision-model cache, including
|
|
116
116
|
// the fixed capacity (a per-aggregate knob is a future refinement — see
|
|
117
|
-
// docs/plans/aggregate-snapshotting.md).
|
|
117
|
+
// docs/plans/done/aggregate-snapshotting.md).
|
|
118
118
|
let replayCacheCapacity = 100
|
|
119
119
|
let replayCache: Lru.t<string, (Behavior.state, int)> = Lru.make(
|
|
120
120
|
~capacity=replayCacheCapacity,
|
|
@@ -122,7 +122,7 @@ module Make = (
|
|
|
122
122
|
|
|
123
123
|
let resetCache = () => replayCache->Lru.clear
|
|
124
124
|
|
|
125
|
-
// Persisted-snapshot configuration (docs/plans/aggregate-snapshotting.md).
|
|
125
|
+
// Persisted-snapshot configuration (docs/plans/done/aggregate-snapshotting.md).
|
|
126
126
|
// `None` (the default) keeps full replay; `Some({interval, stateSchema})`
|
|
127
127
|
// seeds cold replays from the latest persisted snapshot and writes a fresh
|
|
128
128
|
// one every `interval` events. Snapshots are a read optimization only — the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Read per-variant `allowedStates` metadata that the reventless-ppx
|
|
2
|
-
// `@
|
|
2
|
+
// an `@transition` from-set attaches to a command schema. Consumed by
|
|
3
3
|
// `Plugin_Structure.toCommandDef` when building the `commandDef` records that
|
|
4
4
|
// land in `Platform_ComponentDefinitions` so AutoUI can filter the per-row command
|
|
5
5
|
// menu by the row's status.
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
open ReventlessInfra.Api
|
|
8
8
|
|
|
9
9
|
/** Returns the allowed-state list for a given command-variant name, or None
|
|
10
|
-
when the variant
|
|
11
|
-
— the command shows on every row). */
|
|
10
|
+
when the variant declares no `@transition` (the command shows on every row). */
|
|
12
11
|
let getAllowedStates = (
|
|
13
12
|
commandSchema: S.t<unknown>,
|
|
14
13
|
~variantName: string,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Read per-variant `targetState` metadata that the reventless-ppx
|
|
2
|
-
// `@
|
|
2
|
+
// an `@transition` target attaches to a command schema. Consumed by
|
|
3
3
|
// `Plugin_Structure.toCommandDef` when building the `commandDef` records that
|
|
4
4
|
// land in `Platform_ComponentDefinitions`, so AutoUI's board drag resolver can
|
|
5
5
|
// move a row by a *declared* transition instead of a name-stem guess. Mirrors
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
open ReventlessInfra.Api
|
|
9
9
|
|
|
10
10
|
/** Returns the declared target state for a given command-variant name, or None
|
|
11
|
-
when the variant
|
|
12
|
-
|
|
11
|
+
when the variant declares no target — either because it carries no
|
|
12
|
+
`@transition` at all, or because it declares a from-set only, which is a
|
|
13
|
+
command saying it does not move the row. */
|
|
13
14
|
let getTargetState = (
|
|
14
15
|
commandSchema: S.t<unknown>,
|
|
15
16
|
~variantName: string,
|
|
@@ -312,7 +312,7 @@ module Make = (
|
|
|
312
312
|
// sweeps up sibling products. Cross-partition fan-out and capacity reads are
|
|
313
313
|
// preserved (generalised rule 3). Safety guard: if any slice's partition is
|
|
314
314
|
// ambiguous we keep the annotated values for the whole boundary rather than
|
|
315
|
-
// thread a partial inference. See docs/plans/dcb-tag-scope-inference.md.
|
|
315
|
+
// thread a partial inference. See docs/plans/done/dcb-tag-scope-inference.md.
|
|
316
316
|
let inferenceShapes =
|
|
317
317
|
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) =>
|
|
318
318
|
Reventless.DcbTag.sliceShapeFromSchemas(
|
|
@@ -761,28 +761,80 @@ module Make = (
|
|
|
761
761
|
// + AppSync direct invocations (CommandGenerator.payload format)
|
|
762
762
|
let dcbHandlerBase = DcbCommandTopic.makeFilteringHandler(dcbCommandTopic)
|
|
763
763
|
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
//
|
|
764
|
+
// generateCommand for AppSync direct invocations, one per command constructor.
|
|
765
|
+
//
|
|
766
|
+
// Every one of them shares this CommandTopic's publishJsons — the reason a
|
|
767
|
+
// single function used to suffice — and they differ in exactly one argument:
|
|
768
|
+
// the schema. That argument is not only a validator. `makeGenerateCommand`
|
|
769
|
+
// also reads it to find the fields a command marks as its owner, and stamps
|
|
770
|
+
// them with the caller. Handing it the permissive `S.json` (as this did, on
|
|
771
|
+
// the still-true grounds that AppSync has already validated against the SDL)
|
|
772
|
+
// makes that lookup answer "no owner fields" for every command there is — so
|
|
773
|
+
// the write publishes whatever owner the CLIENT sent, silently, and the row
|
|
774
|
+
// is wrong from then on.
|
|
775
|
+
//
|
|
776
|
+
// Per-slice decoding still happens inside `buildSliceHandler`; nothing here
|
|
777
|
+
// re-validates. The schema travels for what it says about ownership.
|
|
778
|
+
let dcbCommandSchemasByTag = {
|
|
779
|
+
let byTag = Dict.make()
|
|
780
|
+
stateChangeSlices->Array.forEach((module(S: StateChangeSlice.T)) => {
|
|
781
|
+
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
782
|
+
if !(ApiNoApiHelpers.isNoApi(commandSchema)) {
|
|
783
|
+
Api_Naming.sliceMutationFields(
|
|
784
|
+
~plugin=apiNamePrefix,
|
|
785
|
+
~slice=S.Spec.name,
|
|
786
|
+
~commandSchema,
|
|
787
|
+
)->Array.forEach(((_, tag)) => {
|
|
788
|
+
// The resolver injects the TAG alone, so two slices claiming one
|
|
789
|
+
// constructor name would make the stamp ambiguous — and the wrong
|
|
790
|
+
// arm of that ambiguity writes a row owned by nobody's decision.
|
|
791
|
+
// Multi-command slices cannot reach here (their mutation fields
|
|
792
|
+
// collide first), which leaves single-command slices, where the
|
|
793
|
+
// field names differ and nothing else would notice.
|
|
794
|
+
switch byTag->Dict.get(tag) {
|
|
795
|
+
| Some(_) =>
|
|
796
|
+
log.error(
|
|
797
|
+
~comp="Dcb_Builder",
|
|
798
|
+
`Two DCB slices in "${name}" both declare the command "${tag}". ` ++
|
|
799
|
+
"Owner stamping resolves a command by that name alone, so it cannot " ++
|
|
800
|
+
"tell them apart. Rename one of the constructors.",
|
|
801
|
+
)
|
|
802
|
+
| None => byTag->Dict.set(tag, commandSchema)
|
|
803
|
+
}
|
|
804
|
+
})
|
|
805
|
+
}
|
|
806
|
+
})
|
|
807
|
+
byTag
|
|
808
|
+
}
|
|
809
|
+
|
|
768
810
|
let dcbGenerateCommandOutput =
|
|
769
811
|
dcbCommandTopic
|
|
770
812
|
->Component.operations
|
|
771
|
-
->Pulumi.Output.apply(ops =>
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
813
|
+
->Pulumi.Output.apply(ops => {
|
|
814
|
+
let make = commandSchema =>
|
|
815
|
+
CommandGenerator_Callback.makeGenerateCommand(
|
|
816
|
+
~publishJsons=ops.publishJsons,
|
|
817
|
+
~publishJsonsAndWait=?ops.publishJsonsAndWait,
|
|
818
|
+
~serviceName=name,
|
|
819
|
+
~commandSchema,
|
|
820
|
+
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
821
|
+
~stripIdFromParams=false,
|
|
822
|
+
)
|
|
823
|
+
let byTag = Dict.make()
|
|
824
|
+
dcbCommandSchemasByTag
|
|
825
|
+
->Dict.toArray
|
|
826
|
+
->Array.forEach(((tag, schema)) => byTag->Dict.set(tag, make(schema)))
|
|
827
|
+
// A command no slice claims keeps the permissive generator — what every
|
|
828
|
+
// command got before this change — so nothing that reaches this handler
|
|
829
|
+
// by another route starts failing. It also cannot stamp, which is why
|
|
830
|
+
// the unknown case is logged rather than left to look like a success.
|
|
831
|
+
(byTag, make(S.json->S.castToUnknown))
|
|
832
|
+
})
|
|
781
833
|
|
|
782
834
|
let dcbHandler =
|
|
783
835
|
(dcbHandlerBase, inboundReceiversOutput, dcbGenerateCommandOutput)
|
|
784
836
|
->Pulumi.Output.all3
|
|
785
|
-
->Pulumi.Output.apply(((baseHandler, receivers,
|
|
837
|
+
->Pulumi.Output.apply(((baseHandler, receivers, (generatorsByTag, generateFallback))) => {
|
|
786
838
|
let composite = (event, ctx) => {
|
|
787
839
|
let raw: dict<JSON.t> = event->Obj.magic
|
|
788
840
|
switch raw->Dict.get("__inboundTranslation") {
|
|
@@ -810,6 +862,17 @@ module Make = (
|
|
|
810
862
|
switch (raw->Dict.get("command"), raw->Dict.get("arguments")) {
|
|
811
863
|
| (Some(JSON.String(_)), Some(_)) =>
|
|
812
864
|
let payload: CommandGenerator.payload = event->Obj.magic
|
|
865
|
+
let generateCommand = switch generatorsByTag->Dict.get(payload.command) {
|
|
866
|
+
| Some(g) => g
|
|
867
|
+
| None =>
|
|
868
|
+
log.warn(
|
|
869
|
+
~comp="Dcb_Builder",
|
|
870
|
+
`No DCB slice in "${name}" declares the command "${payload.command}", ` ++
|
|
871
|
+
"so it is dispatched without an owner stamp. Any field the command " ++
|
|
872
|
+
"marks as its owner keeps the value the caller sent.",
|
|
873
|
+
)
|
|
874
|
+
generateFallback
|
|
875
|
+
}
|
|
813
876
|
(generateCommand(payload)->Effect.map(msgId => msgId->Obj.magic))->Obj.magic
|
|
814
877
|
| _ => baseHandler(event, ctx)
|
|
815
878
|
}
|
|
@@ -422,13 +422,42 @@ function Make(DcbEventLogStorage) {
|
|
|
422
422
|
]);
|
|
423
423
|
})).apply(pairs => Object.fromEntries(pairs));
|
|
424
424
|
let dcbHandlerBase = DcbCommandTopic.makeFilteringHandler(dcbCommandTopic);
|
|
425
|
-
let
|
|
425
|
+
let byTag = {};
|
|
426
|
+
stateChangeSlices.forEach(S => {
|
|
427
|
+
let commandSchema = S.Spec.commandSchema;
|
|
428
|
+
if (!ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
|
|
429
|
+
Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).forEach(param => {
|
|
430
|
+
let tag = param[1];
|
|
431
|
+
let match = byTag[tag];
|
|
432
|
+
if (match !== undefined) {
|
|
433
|
+
return log.error("Dcb_Builder", undefined, `Two DCB slices in "` + name + `" both declare the command "` + tag + `". ` + "Owner stamping resolves a command by that name alone, so it cannot tell them apart. Rename one of the constructors.");
|
|
434
|
+
} else {
|
|
435
|
+
byTag[tag] = commandSchema;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
let dcbGenerateCommandOutput = Component$ReventlessCore.operations(dcbCommandTopic).apply(ops => {
|
|
443
|
+
let make = commandSchema => CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(ops.publishJsons, ops.publishJsonsAndWait, name, commandSchema, "StateChangeSlice", false);
|
|
444
|
+
let byTag$1 = {};
|
|
445
|
+
Object.entries(byTag).forEach(param => {
|
|
446
|
+
byTag$1[param[0]] = make(param[1]);
|
|
447
|
+
});
|
|
448
|
+
return [
|
|
449
|
+
byTag$1,
|
|
450
|
+
make(S.json)
|
|
451
|
+
];
|
|
452
|
+
});
|
|
426
453
|
let dcbHandler = Pulumi.all([
|
|
427
454
|
dcbHandlerBase,
|
|
428
455
|
inboundReceiversOutput,
|
|
429
456
|
dcbGenerateCommandOutput
|
|
430
457
|
]).apply(param => {
|
|
431
|
-
let
|
|
458
|
+
let match = param[2];
|
|
459
|
+
let generateFallback = match[1];
|
|
460
|
+
let generatorsByTag = match[0];
|
|
432
461
|
let receivers = param[1];
|
|
433
462
|
let baseHandler = param[0];
|
|
434
463
|
return (event, ctx) => {
|
|
@@ -448,11 +477,18 @@ function Make(DcbEventLogStorage) {
|
|
|
448
477
|
}
|
|
449
478
|
let match$1 = event["command"];
|
|
450
479
|
let match$2 = event["arguments"];
|
|
451
|
-
if (
|
|
452
|
-
return
|
|
453
|
-
}
|
|
480
|
+
if (match$1 === undefined) {
|
|
481
|
+
return baseHandler(event, ctx);
|
|
482
|
+
}
|
|
483
|
+
if (typeof match$1 !== "string") {
|
|
484
|
+
return baseHandler(event, ctx);
|
|
485
|
+
}
|
|
486
|
+
if (match$2 === undefined) {
|
|
454
487
|
return baseHandler(event, ctx);
|
|
455
488
|
}
|
|
489
|
+
let g = generatorsByTag[event.command];
|
|
490
|
+
let generateCommand = g !== undefined ? g : (log.warn("Dcb_Builder", undefined, `No DCB slice in "` + name + `" declares the command "` + event.command + `", ` + "so it is dispatched without an owner stamp. Any field the command marks as its owner keeps the value the caller sent."), generateFallback);
|
|
491
|
+
return Effect.map(generateCommand(event), msgId => msgId);
|
|
456
492
|
};
|
|
457
493
|
});
|
|
458
494
|
let dcbResources = Object.values(stateChangeSlicesOutputs).flatMap(outputs => outputs.resources).concat(Object.values(inboundTranslationSlicesOutputs).flatMap(outputs => outputs.resources.concat(outputs.queryDb.resources)));
|
|
@@ -25,7 +25,7 @@ type appendError =
|
|
|
25
25
|
// staleness — consumers ignore a snapshot whose hash differs from their current
|
|
26
26
|
// state schema and fall back to full replay. Snapshots are a read optimization
|
|
27
27
|
// only; the OCC append remains the sole consistency primitive
|
|
28
|
-
// (docs/plans/aggregate-snapshotting.md).
|
|
28
|
+
// (docs/plans/done/aggregate-snapshotting.md).
|
|
29
29
|
type snapshot = {seqNr: int, state: JSON.t, schemaHash: string}
|
|
30
30
|
|
|
31
31
|
type append<'id, 'event> = (int, 'id, array<'event>) => promise<result<unit, appendError>>
|