@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +6 -6
  3. package/src/admin/Platform_Admin_Structure.res +4 -0
  4. package/src/admin/Platform_Admin_Structure.res.mjs +5 -0
  5. package/src/admin/Platform_ComponentDefinitionsApi.res +16 -1
  6. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +24 -1
  7. package/src/components/Aggregate/Aggregate_Callback.res +14 -2
  8. package/src/components/Aggregate/Aggregate_Callback.res.mjs +3 -3
  9. package/src/components/CommandTopic/CommandTopic.res.mjs +12 -0
  10. package/src/components/CommandTopic/CommandTopic_Helpers.res +88 -2
  11. package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +48 -2
  12. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +13 -1
  13. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
  14. package/src/plugin/component/Plugin_Builder.res +1 -0
  15. package/src/plugin/component/Plugin_Builder.res.mjs +1 -0
  16. package/src/plugin/component/Plugin_Structure.res +66 -51
  17. package/src/plugin/component/Plugin_Structure.res.mjs +61 -46
  18. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +21 -0
  19. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -0
  20. package/tests/commandtopic/CommandOutcomeHookTest.res +258 -0
  21. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +372 -0
  22. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +9 -1
  23. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +3 -3
  24. package/tests/message/MessageTest.res +1 -0
  25. package/tests/message/MessageTest.res.mjs +1 -0
  26. package/tests/plugin/PluginStructureTest.res +27 -0
  27. package/tests/plugin/PluginStructureTest.res.mjs +27 -0
  28. package/tests/plugin/StateChangeSlice/PsShipOrder.res +3 -1
  29. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -1
  30. 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.215",
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.127",
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.128",
39
40
  "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
40
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
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
- `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 chapter: String\n}`,
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 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 chapter: String\n}`,
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(reference, {errorCode, errorDetail})
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(reference, {entityId, eventCount: appendedEventCount})
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
- let reportAccepted = (reference: string, result: acceptedResult) =>
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 = (reference: string, result: rejectedResult) =>
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(cmdJson.meta.msgId, {errorCode, errorDetail})
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
  };