@reventlessdev/reventless-core 3.0.0-alpha.215 → 3.0.0-alpha.216
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 +8 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin_Structure.res +4 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +5 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +16 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +24 -1
- package/src/components/Aggregate/Aggregate_Callback.res +14 -2
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +3 -3
- package/src/components/CommandTopic/CommandTopic.res.mjs +12 -0
- package/src/components/CommandTopic/CommandTopic_Helpers.res +88 -2
- package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +48 -2
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +13 -1
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
- package/src/plugin/component/Plugin_Builder.res +1 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +1 -0
- package/src/plugin/component/Plugin_Structure.res +66 -51
- package/src/plugin/component/Plugin_Structure.res.mjs +61 -46
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +21 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res +258 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +372 -0
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +9 -1
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +3 -3
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +1 -0
- package/tests/plugin/PluginStructureTest.res +27 -0
- package/tests/plugin/PluginStructureTest.res.mjs +27 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res +3 -1
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -1
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.216 (2026-08-08)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core,spec:** surface a write-side component's declared error types ([c9c2505](https://github.com/ReventlessDev/reventless-core/commit/c9c25057c70595fe27d73447c9aef9b451f86168))
|
|
11
|
+
* **core:** add a runtime hook for command outcomes ([f2092a8](https://github.com/ReventlessDev/reventless-core/commit/f2092a8f9a8d8d7feee4f3de19bea7297a250cbd))
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
# 3.0.0-alpha.215 (2026-08-08)
|
|
7
15
|
|
|
8
16
|
### 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.216",
|
|
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-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
|
-
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
32
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
33
31
|
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.2",
|
|
32
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
34
33
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
34
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
35
35
|
"@reventlessdev/rescript-node": "2.0.0-alpha.2",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
37
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
37
38
|
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.2",
|
|
38
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
39
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.128",
|
|
39
40
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
40
|
-
"@reventlessdev/
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.101"
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.102"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -98,6 +98,10 @@ let pluginAggregate: writableDef = {
|
|
|
98
98
|
linkedViews: ["Plugins"],
|
|
99
99
|
consistencyRead: None,
|
|
100
100
|
events: [],
|
|
101
|
+
// Derived rather than left empty like `events` above: the two admin commands can
|
|
102
|
+
// be refused, and `[]` here would read as "this aggregate never rejects". Uses
|
|
103
|
+
// the same walk `Plugin_Structure` applies to every other write side.
|
|
104
|
+
errors: Plugin_Structure.extractErrorDefs(PluginSpec.errorSchema->S.castToUnknown),
|
|
101
105
|
chapter: None,
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -7,6 +7,8 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
7
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
8
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
9
9
|
import * as Api_Naming$ReventlessCore from "../components/Api/Api_Naming.res.mjs";
|
|
10
|
+
import * as PluginSpec$ReventlessCore from "../plugin/lifecycle/PluginSpec.res.mjs";
|
|
11
|
+
import * as Plugin_Structure$ReventlessCore from "../plugin/component/Plugin_Structure.res.mjs";
|
|
10
12
|
import * as SuryToJsonSchema$ReventlessCore from "../components/Api/SuryToJsonSchema.res.mjs";
|
|
11
13
|
import * as PluginBaseFragment$ReventlessCore from "../plugin/api/PluginBaseFragment.res.mjs";
|
|
12
14
|
import * as PluginsReadModelSpec$ReventlessCore from "../plugin/lifecycle/PluginsReadModelSpec.res.mjs";
|
|
@@ -105,6 +107,8 @@ let pluginAggregate_linkedViews = ["Plugins"];
|
|
|
105
107
|
|
|
106
108
|
let pluginAggregate_events = [];
|
|
107
109
|
|
|
110
|
+
let pluginAggregate_errors = Plugin_Structure$ReventlessCore.extractErrorDefs(PluginSpec$ReventlessCore.errorSchema);
|
|
111
|
+
|
|
108
112
|
let pluginAggregate = {
|
|
109
113
|
name: "Plugin",
|
|
110
114
|
commands: pluginAggregate_commands,
|
|
@@ -113,6 +117,7 @@ let pluginAggregate = {
|
|
|
113
117
|
linkedViews: pluginAggregate_linkedViews,
|
|
114
118
|
consistencyRead: undefined,
|
|
115
119
|
events: pluginAggregate_events,
|
|
120
|
+
errors: pluginAggregate_errors,
|
|
116
121
|
chapter: undefined
|
|
117
122
|
};
|
|
118
123
|
|
|
@@ -20,7 +20,11 @@ let sdlTypes: array<string> = [
|
|
|
20
20
|
`type Platform_FieldReference {\n fieldName: String!\n entity: String!\n plugin: String\n}`,
|
|
21
21
|
`type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n}`,
|
|
22
22
|
`type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
23
|
-
|
|
23
|
+
// Same fields as Platform_EventDef, kept a distinct type because a refusal is not
|
|
24
|
+
// a fact: a caller selecting `errors` is asking what a command can be rejected
|
|
25
|
+
// with, and the two lists must stay independently evolvable.
|
|
26
|
+
`type Platform_ErrorDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
27
|
+
`type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n errors: [Platform_ErrorDef!]!\n chapter: String\n}`,
|
|
24
28
|
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n statusField: String\n visibility: String\n chapter: String\n}`,
|
|
25
29
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
|
|
26
30
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
@@ -102,6 +106,13 @@ let encodeEventDef = (e: eventDef): JSON.t =>
|
|
|
102
106
|
("references", e.references->Array.map(encodeFieldReference)->JSON.Encode.array),
|
|
103
107
|
])->JSON.Encode.object
|
|
104
108
|
|
|
109
|
+
let encodeErrorDef = (e: errorDef): JSON.t =>
|
|
110
|
+
Dict.fromArray([
|
|
111
|
+
("name", JSON.Encode.string(e.name)),
|
|
112
|
+
("schema", JSON.Encode.string(e.schema)),
|
|
113
|
+
("references", e.references->Array.map(encodeFieldReference)->JSON.Encode.array),
|
|
114
|
+
])->JSON.Encode.object
|
|
115
|
+
|
|
105
116
|
let encodeWritableDef = (w: writableDef): JSON.t =>
|
|
106
117
|
Dict.fromArray([
|
|
107
118
|
("name", JSON.Encode.string(w.name)),
|
|
@@ -115,6 +126,10 @@ let encodeWritableDef = (w: writableDef): JSON.t =>
|
|
|
115
126
|
("consumedEventTypes", encodeStrings(w.consumedEventTypes)),
|
|
116
127
|
// Phase 6.3: emitted-event field schemas (None → [] on the wire).
|
|
117
128
|
("events", w.events->Array.map(encodeEventDef)->JSON.Encode.array),
|
|
129
|
+
// Declared errors — the refusals a caller has to handle. `[]` is the honest
|
|
130
|
+
// answer for a component that declares none; the structure is re-derived on
|
|
131
|
+
// every build, so it never stands in for "cannot say".
|
|
132
|
+
("errors", w.errors->Array.map(encodeErrorDef)->JSON.Encode.array),
|
|
118
133
|
("chapter", w.chapter->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
119
134
|
])->JSON.Encode.object
|
|
120
135
|
|
|
@@ -8,7 +8,8 @@ let sdlTypes = [
|
|
|
8
8
|
`type Platform_FieldReference {\n fieldName: String!\n entity: String!\n plugin: String\n}`,
|
|
9
9
|
`type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n}`,
|
|
10
10
|
`type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
11
|
-
`type
|
|
11
|
+
`type Platform_ErrorDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
12
|
+
`type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n errors: [Platform_ErrorDef!]!\n chapter: String\n}`,
|
|
12
13
|
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n statusField: String\n visibility: String\n chapter: String\n}`,
|
|
13
14
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
|
|
14
15
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
@@ -152,6 +153,23 @@ function encodeEventDef(e) {
|
|
|
152
153
|
]);
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
function encodeErrorDef(e) {
|
|
157
|
+
return Object.fromEntries([
|
|
158
|
+
[
|
|
159
|
+
"name",
|
|
160
|
+
e.name
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"schema",
|
|
164
|
+
e.schema
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
"references",
|
|
168
|
+
e.references.map(encodeFieldReference)
|
|
169
|
+
]
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
|
|
155
173
|
function encodeWritableDef(w) {
|
|
156
174
|
return Object.fromEntries([
|
|
157
175
|
[
|
|
@@ -182,6 +200,10 @@ function encodeWritableDef(w) {
|
|
|
182
200
|
"events",
|
|
183
201
|
w.events.map(encodeEventDef)
|
|
184
202
|
],
|
|
203
|
+
[
|
|
204
|
+
"errors",
|
|
205
|
+
w.errors.map(encodeErrorDef)
|
|
206
|
+
],
|
|
185
207
|
[
|
|
186
208
|
"chapter",
|
|
187
209
|
Stdlib_Option.mapOr(w.chapter, null, prim => prim)
|
|
@@ -341,6 +363,7 @@ export {
|
|
|
341
363
|
isPublicQueryable,
|
|
342
364
|
encodeQueryableDef,
|
|
343
365
|
encodeEventDef,
|
|
366
|
+
encodeErrorDef,
|
|
344
367
|
encodeWritableDef,
|
|
345
368
|
encodeAutomationSliceDef,
|
|
346
369
|
encodeOutboundTranslationSliceDef,
|
|
@@ -284,13 +284,25 @@ module Make = (
|
|
|
284
284
|
outcomes->Array.map(((reference, outcome, _meta)) =>
|
|
285
285
|
switch outcome {
|
|
286
286
|
| CmdRejected({errorCode, errorDetail}) =>
|
|
287
|
-
CommandTopic_Helpers.reportRejected(
|
|
287
|
+
CommandTopic_Helpers.reportRejected(
|
|
288
|
+
~component=Spec.name,
|
|
289
|
+
~cause=DomainRejection,
|
|
290
|
+
reference,
|
|
291
|
+
{errorCode, errorDetail},
|
|
292
|
+
)
|
|
288
293
|
Ok(reference)
|
|
289
294
|
| CmdOk(_) if appendSucceeded =>
|
|
290
|
-
CommandTopic_Helpers.reportAccepted(
|
|
295
|
+
CommandTopic_Helpers.reportAccepted(
|
|
296
|
+
~component=Spec.name,
|
|
297
|
+
reference,
|
|
298
|
+
{entityId, eventCount: appendedEventCount},
|
|
299
|
+
)
|
|
291
300
|
Ok(reference)
|
|
292
301
|
| CmdOk(_) =>
|
|
302
|
+
// The decision succeeded and the append did not — infrastructure, not the model.
|
|
293
303
|
CommandTopic_Helpers.reportRejected(
|
|
304
|
+
~component=Spec.name,
|
|
305
|
+
~cause=InfrastructureFailure,
|
|
294
306
|
reference,
|
|
295
307
|
{errorCode: "AppendFailed", errorDetail: appendErrorDetail},
|
|
296
308
|
)
|
|
@@ -200,7 +200,7 @@ function Make(Spec) {
|
|
|
200
200
|
let reference = param[0];
|
|
201
201
|
if (outcome.TAG === "CmdOk") {
|
|
202
202
|
if (appendSucceeded) {
|
|
203
|
-
CommandTopic_Helpers$ReventlessCore.reportAccepted(reference, {
|
|
203
|
+
CommandTopic_Helpers$ReventlessCore.reportAccepted(Spec.name, reference, {
|
|
204
204
|
entityId: entityId,
|
|
205
205
|
eventCount: appendedEventCount
|
|
206
206
|
});
|
|
@@ -209,7 +209,7 @@ function Make(Spec) {
|
|
|
209
209
|
_0: reference
|
|
210
210
|
};
|
|
211
211
|
} else {
|
|
212
|
-
CommandTopic_Helpers$ReventlessCore.reportRejected(reference, {
|
|
212
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "InfrastructureFailure", reference, {
|
|
213
213
|
errorCode: "AppendFailed",
|
|
214
214
|
errorDetail: appendErrorDetail
|
|
215
215
|
});
|
|
@@ -219,7 +219,7 @@ function Make(Spec) {
|
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
-
CommandTopic_Helpers$ReventlessCore.reportRejected(reference, {
|
|
222
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "DomainRejection", reference, {
|
|
223
223
|
errorCode: outcome.errorCode,
|
|
224
224
|
errorDetail: outcome.errorDetail
|
|
225
225
|
});
|
|
@@ -23,6 +23,14 @@ function filter(allCommandTopics, names) {
|
|
|
23
23
|
|
|
24
24
|
let componentType = "CommandTopic";
|
|
25
25
|
|
|
26
|
+
let commandOutcomeHook = CommandTopic_Helpers$ReventlessCore.commandOutcomeHook;
|
|
27
|
+
|
|
28
|
+
let registerCommandOutcome = CommandTopic_Helpers$ReventlessCore.registerCommandOutcome;
|
|
29
|
+
|
|
30
|
+
let clearCommandOutcome = CommandTopic_Helpers$ReventlessCore.clearCommandOutcome;
|
|
31
|
+
|
|
32
|
+
let fireCommandOutcome = CommandTopic_Helpers$ReventlessCore.fireCommandOutcome;
|
|
33
|
+
|
|
26
34
|
let acceptedResultChannel = CommandTopic_Helpers$ReventlessCore.acceptedResultChannel;
|
|
27
35
|
|
|
28
36
|
let rejectedResultChannel = CommandTopic_Helpers$ReventlessCore.rejectedResultChannel;
|
|
@@ -50,6 +58,10 @@ let getHandlers = CommandTopic_Helpers$ReventlessCore.getHandlers;
|
|
|
50
58
|
export {
|
|
51
59
|
componentType,
|
|
52
60
|
NotPublishedToChannel,
|
|
61
|
+
commandOutcomeHook,
|
|
62
|
+
registerCommandOutcome,
|
|
63
|
+
clearCommandOutcome,
|
|
64
|
+
fireCommandOutcome,
|
|
53
65
|
acceptedResultChannel,
|
|
54
66
|
rejectedResultChannel,
|
|
55
67
|
reportAccepted,
|
|
@@ -18,6 +18,71 @@ is the JSON-stringified error payload (or empty string for payload-less variants
|
|
|
18
18
|
*/
|
|
19
19
|
type rejectedResult = {errorCode: string, errorDetail: string}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
Why a command was refused.
|
|
23
|
+
|
|
24
|
+
`DomainRejection` is `Behavior.decide` returning `Error` — the model declining the command,
|
|
25
|
+
which is the model working. `InfrastructureFailure` is the decision never landing: an event-log
|
|
26
|
+
append that failed outright or exhausted its conflict retries. Both arrive on `reportRejected`,
|
|
27
|
+
so without this a consumer would read a broken event log as a business rejection.
|
|
28
|
+
*/
|
|
29
|
+
type refusalCause = DomainRejection | InfrastructureFailure
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
What became of one command. Constructors are prefixed to stay clear of `commandOutcome`'s
|
|
33
|
+
`Accepted`/`Rejected` below — that type is the producer-facing result shape (and the GraphQL
|
|
34
|
+
`CommandResult` union); this one is the observation, and carries the refusal cause it doesn't.
|
|
35
|
+
*/
|
|
36
|
+
type observedOutcome =
|
|
37
|
+
| OutcomeAccepted({entityId: option<string>, eventCount: int})
|
|
38
|
+
| OutcomeRejected({errorCode: string, errorDetail: string, cause: refusalCause})
|
|
39
|
+
|
|
40
|
+
/** `reference` is the command's `meta.msgId` on every current transport. */
|
|
41
|
+
type commandOutcomeReport = {
|
|
42
|
+
component: string,
|
|
43
|
+
reference: string,
|
|
44
|
+
outcome: observedOutcome,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type commandOutcomeHook = commandOutcomeReport => unit
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
Module-level outcome hook, in the shape the `RuntimeExtension` cold-start seam already reaches
|
|
51
|
+
— so an extension registers it from `onColdStart` alongside the interception and publish hooks,
|
|
52
|
+
and no new registration path is needed. None = no-op (default).
|
|
53
|
+
*/
|
|
54
|
+
let commandOutcomeHook: ref<option<commandOutcomeHook>> = ref(None)
|
|
55
|
+
|
|
56
|
+
let registerCommandOutcome = (hook: commandOutcomeHook) => {
|
|
57
|
+
commandOutcomeHook.contents = Some(hook)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let clearCommandOutcome = () => {
|
|
61
|
+
commandOutcomeHook.contents = None
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fired from reportAccepted/reportRejected below, which both write-side kinds call
|
|
65
|
+
// unconditionally — so this sees fire-and-forget dispatches too, not just the ones a
|
|
66
|
+
// producer awaited on the side-channels.
|
|
67
|
+
//
|
|
68
|
+
// A throwing hook is logged and swallowed: the outcome it observes has already happened,
|
|
69
|
+
// and letting an observer fail a completed command would make an extension an outage
|
|
70
|
+
// (the same failure policy the cold-start seam settled on).
|
|
71
|
+
let fireCommandOutcome = (~component: string, ~reference: string, outcome: observedOutcome) =>
|
|
72
|
+
commandOutcomeHook.contents->Option.forEach(hook =>
|
|
73
|
+
switch hook({component, reference, outcome}) {
|
|
74
|
+
| () => ()
|
|
75
|
+
| exception err =>
|
|
76
|
+
EffectLogger.logError(
|
|
77
|
+
~comp=`CommandTopic(${component})`,
|
|
78
|
+
`command-outcome hook threw (ignored): ${err
|
|
79
|
+
->JsExn.fromException
|
|
80
|
+
->Option.flatMap(JsExn.message)
|
|
81
|
+
->Option.getOr("unknown")}`,
|
|
82
|
+
)->Effect.runSync
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
21
86
|
// Side-channel for publishJsonsAndWait result propagation.
|
|
22
87
|
// Set by runInlineAndCollect before invoking the handler; cleared after.
|
|
23
88
|
// Callback implementations call reportAccepted during inline dispatch.
|
|
@@ -28,11 +93,32 @@ let acceptedResultChannel: ref<option<(string, acceptedResult) => unit>> = ref(N
|
|
|
28
93
|
// `Rejected` outcomes that carry the real error code and detail.
|
|
29
94
|
let rejectedResultChannel: ref<option<(string, rejectedResult) => unit>> = ref(None)
|
|
30
95
|
|
|
31
|
-
|
|
96
|
+
// `~component` is the owning component's `Spec.name`: an outcome isn't interpretable without
|
|
97
|
+
// knowing which component produced it, and the side-channel (keyed per inline dispatch) never
|
|
98
|
+
// had to say. `~cause` is required rather than defaulted so a new rejection site has to state
|
|
99
|
+
// which kind it is instead of silently reporting infrastructure as a domain decision.
|
|
100
|
+
let reportAccepted = (~component: string, reference: string, result: acceptedResult) => {
|
|
32
101
|
acceptedResultChannel.contents->Option.forEach(cb => cb(reference, result))
|
|
102
|
+
fireCommandOutcome(
|
|
103
|
+
~component,
|
|
104
|
+
~reference,
|
|
105
|
+
OutcomeAccepted({entityId: result.entityId, eventCount: result.eventCount}),
|
|
106
|
+
)
|
|
107
|
+
}
|
|
33
108
|
|
|
34
|
-
let reportRejected = (
|
|
109
|
+
let reportRejected = (
|
|
110
|
+
~component: string,
|
|
111
|
+
~cause: refusalCause,
|
|
112
|
+
reference: string,
|
|
113
|
+
result: rejectedResult,
|
|
114
|
+
) => {
|
|
35
115
|
rejectedResultChannel.contents->Option.forEach(cb => cb(reference, result))
|
|
116
|
+
fireCommandOutcome(
|
|
117
|
+
~component,
|
|
118
|
+
~reference,
|
|
119
|
+
OutcomeRejected({errorCode: result.errorCode, errorDetail: result.errorDetail, cause}),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
36
122
|
|
|
37
123
|
// Placed here (rather than CommandTopic.res) so runInlineAndCollect can use commandOutcome
|
|
38
124
|
// without importing Adapter.res → @pulumi/pulumi. Re-exported by CommandTopic via include.
|
|
@@ -1,11 +1,42 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Effect from "effect/Effect";
|
|
6
7
|
import * as Stream from "effect/Stream";
|
|
7
8
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
8
9
|
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
10
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
11
|
+
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
12
|
+
|
|
13
|
+
let commandOutcomeHook = {
|
|
14
|
+
contents: undefined
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function registerCommandOutcome(hook) {
|
|
18
|
+
commandOutcomeHook.contents = hook;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function clearCommandOutcome() {
|
|
22
|
+
commandOutcomeHook.contents = undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fireCommandOutcome(component, reference, outcome) {
|
|
26
|
+
Stdlib_Option.forEach(commandOutcomeHook.contents, hook => {
|
|
27
|
+
try {
|
|
28
|
+
hook({
|
|
29
|
+
component: component,
|
|
30
|
+
reference: reference,
|
|
31
|
+
outcome: outcome
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
} catch (raw_err) {
|
|
35
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
36
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError(`CommandTopic(` + component + `)`, undefined, `command-outcome hook threw (ignored): ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(err), Stdlib_JsExn.message), "unknown")));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
9
40
|
|
|
10
41
|
let acceptedResultChannel = {
|
|
11
42
|
contents: undefined
|
|
@@ -15,12 +46,23 @@ let rejectedResultChannel = {
|
|
|
15
46
|
contents: undefined
|
|
16
47
|
};
|
|
17
48
|
|
|
18
|
-
function reportAccepted(reference, result) {
|
|
49
|
+
function reportAccepted(component, reference, result) {
|
|
19
50
|
Stdlib_Option.forEach(acceptedResultChannel.contents, cb => cb(reference, result));
|
|
51
|
+
fireCommandOutcome(component, reference, {
|
|
52
|
+
TAG: "OutcomeAccepted",
|
|
53
|
+
entityId: result.entityId,
|
|
54
|
+
eventCount: result.eventCount
|
|
55
|
+
});
|
|
20
56
|
}
|
|
21
57
|
|
|
22
|
-
function reportRejected(reference, result) {
|
|
58
|
+
function reportRejected(component, cause, reference, result) {
|
|
23
59
|
Stdlib_Option.forEach(rejectedResultChannel.contents, cb => cb(reference, result));
|
|
60
|
+
fireCommandOutcome(component, reference, {
|
|
61
|
+
TAG: "OutcomeRejected",
|
|
62
|
+
errorCode: result.errorCode,
|
|
63
|
+
errorDetail: result.errorDetail,
|
|
64
|
+
cause: cause
|
|
65
|
+
});
|
|
24
66
|
}
|
|
25
67
|
|
|
26
68
|
function commandOutcomeToJson(outcome) {
|
|
@@ -179,6 +221,10 @@ function getHandlers(typeName) {
|
|
|
179
221
|
}
|
|
180
222
|
|
|
181
223
|
export {
|
|
224
|
+
commandOutcomeHook,
|
|
225
|
+
registerCommandOutcome,
|
|
226
|
+
clearCommandOutcome,
|
|
227
|
+
fireCommandOutcome,
|
|
182
228
|
acceptedResultChannel,
|
|
183
229
|
rejectedResultChannel,
|
|
184
230
|
reportAccepted,
|
|
@@ -322,6 +322,7 @@ module Make = (
|
|
|
322
322
|
// No append, but the read snapshot is valid — cache it for the next command.
|
|
323
323
|
cachePut((state, headPosition))
|
|
324
324
|
CommandTopic_Helpers.reportAccepted(
|
|
325
|
+
~component=Spec.name,
|
|
325
326
|
cmdJson.meta.msgId,
|
|
326
327
|
switch entityId {
|
|
327
328
|
| Some(eid) => {entityId: eid, eventCount: 0}
|
|
@@ -373,6 +374,7 @@ module Make = (
|
|
|
373
374
|
// next command's delta read picks our events up from `headPosition`.
|
|
374
375
|
cachePut((state, headPosition))
|
|
375
376
|
CommandTopic_Helpers.reportAccepted(
|
|
377
|
+
~component=Spec.name,
|
|
376
378
|
cmdJson.meta.msgId,
|
|
377
379
|
switch entityId {
|
|
378
380
|
| Some(eid) => {entityId: eid, eventCount: rawEvents->Array.length}
|
|
@@ -412,7 +414,12 @@ module Make = (
|
|
|
412
414
|
if errorCode == "Conflict" {
|
|
413
415
|
Metrics.emitCount(~metric="AppendConflict", ~slice=Spec.name)
|
|
414
416
|
}
|
|
417
|
+
// Both arms here are storage, not the model: a surfaced Conflict is
|
|
418
|
+
// contention the retry loop couldn't absorb, and AppendFailed is the
|
|
419
|
+
// store failing. `decide` had already said Ok.
|
|
415
420
|
CommandTopic_Helpers.reportRejected(
|
|
421
|
+
~component=Spec.name,
|
|
422
|
+
~cause=InfrastructureFailure,
|
|
416
423
|
cmdJson.meta.msgId,
|
|
417
424
|
{errorCode, errorDetail: errDetail},
|
|
418
425
|
)
|
|
@@ -433,7 +440,12 @@ module Make = (
|
|
|
433
440
|
payloadDict->Dict.toArray->Array.length == 0
|
|
434
441
|
? ""
|
|
435
442
|
: payloadDict->JSON.Encode.object->JSON.stringify
|
|
436
|
-
CommandTopic_Helpers.reportRejected(
|
|
443
|
+
CommandTopic_Helpers.reportRejected(
|
|
444
|
+
~component=Spec.name,
|
|
445
|
+
~cause=DomainRejection,
|
|
446
|
+
cmdJson.meta.msgId,
|
|
447
|
+
{errorCode, errorDetail},
|
|
448
|
+
)
|
|
437
449
|
EffectLogger.logError(
|
|
438
450
|
~comp,
|
|
439
451
|
`decide rejected: ${errorCode} ${errorDetail}`,
|
|
@@ -173,7 +173,7 @@ function Make(Spec) {
|
|
|
173
173
|
state,
|
|
174
174
|
headPosition
|
|
175
175
|
]);
|
|
176
|
-
CommandTopic_Helpers$ReventlessCore.reportAccepted(cmdJson.meta.msgId, entityId !== undefined ? ({
|
|
176
|
+
CommandTopic_Helpers$ReventlessCore.reportAccepted(Spec.name, cmdJson.meta.msgId, entityId !== undefined ? ({
|
|
177
177
|
entityId: entityId,
|
|
178
178
|
eventCount: 0
|
|
179
179
|
}) : ({
|
|
@@ -223,7 +223,7 @@ function Make(Spec) {
|
|
|
223
223
|
state,
|
|
224
224
|
headPosition
|
|
225
225
|
]);
|
|
226
|
-
CommandTopic_Helpers$ReventlessCore.reportAccepted(cmdJson.meta.msgId, entityId !== undefined ? ({
|
|
226
|
+
CommandTopic_Helpers$ReventlessCore.reportAccepted(Spec.name, cmdJson.meta.msgId, entityId !== undefined ? ({
|
|
227
227
|
entityId: entityId,
|
|
228
228
|
eventCount: rawEvents.length
|
|
229
229
|
}) : ({
|
|
@@ -257,7 +257,7 @@ function Make(Spec) {
|
|
|
257
257
|
if (errorCode === "Conflict") {
|
|
258
258
|
Metrics$ReventlessCore.emitCount("AppendConflict", Spec.name, undefined);
|
|
259
259
|
}
|
|
260
|
-
CommandTopic_Helpers$ReventlessCore.reportRejected(cmdJson.meta.msgId, {
|
|
260
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "InfrastructureFailure", cmdJson.meta.msgId, {
|
|
261
261
|
errorCode: errorCode,
|
|
262
262
|
errorDetail: errDetail
|
|
263
263
|
});
|
|
@@ -277,7 +277,7 @@ function Make(Spec) {
|
|
|
277
277
|
let match = Message$ReventlessCore.splitMessage(errorJson);
|
|
278
278
|
let payloadDict = match[1];
|
|
279
279
|
let errorDetail = Object.entries(payloadDict).length === 0 ? "" : JSON.stringify(payloadDict);
|
|
280
|
-
CommandTopic_Helpers$ReventlessCore.reportRejected(cmdJson.meta.msgId, {
|
|
280
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "DomainRejection", cmdJson.meta.msgId, {
|
|
281
281
|
errorCode: errorCode,
|
|
282
282
|
errorDetail: errorDetail
|
|
283
283
|
});
|
|
@@ -529,6 +529,7 @@ module Make = (
|
|
|
529
529
|
{
|
|
530
530
|
commandTypes: extractTypes(Scs.Spec.commandSchema),
|
|
531
531
|
eventTypes: extractEventTypes(Scs.Spec.eventSchema->S.castToUnknown),
|
|
532
|
+
errorTypes: extractTypes(Scs.Spec.errorSchema),
|
|
532
533
|
consumedEventTypes: extractEventTypes(Scs.Spec.consumedEventSchema->S.castToUnknown),
|
|
533
534
|
chapter: ?chapterFor(Scs.Spec.name),
|
|
534
535
|
},
|
|
@@ -373,6 +373,7 @@ function Make(Spec) {
|
|
|
373
373
|
stateSchemas[Scs.Spec.name] = {
|
|
374
374
|
commandTypes: DcbTag$Reventless.extractAllVariantNames(Scs.Spec.commandSchema),
|
|
375
375
|
eventTypes: DcbTag$Reventless.extractVariantNames(Scs.Spec.eventSchema),
|
|
376
|
+
errorTypes: DcbTag$Reventless.extractAllVariantNames(Scs.Spec.errorSchema),
|
|
376
377
|
consumedEventTypes: DcbTag$Reventless.extractVariantNames(Scs.Spec.consumedEventSchema),
|
|
377
378
|
chapter: chapterByName[Scs.Spec.name]
|
|
378
379
|
};
|