@reventlessdev/reventless-core 3.0.0-alpha.180 → 3.0.0-alpha.181

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 (115) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +7 -7
  3. package/src/Message.res +2 -1
  4. package/src/Message.res.mjs +5 -1
  5. package/src/admin/Platform_Admin_Structure.res +2 -0
  6. package/src/admin/Platform_Admin_Structure.res.mjs +2 -0
  7. package/src/admin/Platform_ComponentDefinitionsApi.res +2 -1
  8. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
  9. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res +1 -1
  10. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res.mjs +2 -1
  11. package/src/components/Api/ApiTargetStateHelpers.res +20 -0
  12. package/src/components/Api/ApiTargetStateHelpers.res.mjs +16 -0
  13. package/src/components/Api/GraphQL_FragmentGenerator.res +1 -0
  14. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +1 -0
  15. package/src/components/Api/SchemaType.res +3 -0
  16. package/src/components/Api/SchemaType.res.mjs +10 -2
  17. package/src/components/Api/SuryToJsonSchema.res +16 -0
  18. package/src/components/Api/SuryToJsonSchema.res.mjs +28 -0
  19. package/src/components/Dcb/Dcb_Builder.res +4 -5
  20. package/src/components/Dcb/Dcb_Builder.res.mjs +3 -5
  21. package/src/components/DcbEventLog/DcbEventLog_Operations.res +6 -1
  22. package/src/components/DcbEventLog/DcbEventLog_Operations.res.mjs +2 -1
  23. package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +25 -0
  24. package/src/components/InboundTranslationSlice/InboundTranslationSlice.res.mjs +31 -0
  25. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res +8 -8
  26. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs +26 -6
  27. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +44 -37
  28. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res.mjs +17 -25
  29. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +27 -2
  30. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -0
  31. package/src/components/StateViewSlice/StateViewSlice_Builder.res +20 -6
  32. package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +23 -5
  33. package/src/components/StateViewSlice/StateViewSlice_Callback.res +1 -1
  34. package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +5 -1
  35. package/src/plugin/component/Plugin_Helpers.res +4 -1
  36. package/src/plugin/component/Plugin_Structure.res +15 -1
  37. package/src/plugin/component/Plugin_Structure.res.mjs +4 -1
  38. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +2 -0
  39. package/tests/IdentityTest.res.mjs +174 -0
  40. package/tests/ResourceAttributionTest.res.mjs +75 -0
  41. package/tests/adapter/MonitoringTest.res.mjs +116 -0
  42. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +10 -0
  43. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +365 -0
  44. package/tests/admin/Platform_UIFragmentsApiTest.res.mjs +101 -0
  45. package/tests/aggregate/AggregateCacheTest.res.mjs +433 -0
  46. package/tests/aggregate/AggregateCallbackTest.res.mjs +143 -0
  47. package/tests/aggregate/AggregateCausationTest.res.mjs +47 -0
  48. package/tests/aggregate/AggregateConflictTest.res.mjs +386 -0
  49. package/tests/aggregate/AggregateFixtures.res.mjs +308 -0
  50. package/tests/aggregate/AggregateRejectionTest.res.mjs +166 -0
  51. package/tests/aggregate/AggregateSnapshotTest.res.mjs +645 -0
  52. package/tests/api/ApiNoApiTest.res.mjs +90 -0
  53. package/tests/api/GraphQL_StitcherTest.res.mjs +112 -0
  54. package/tests/api/SimpleTest.res.mjs +10 -0
  55. package/tests/api/SuryToJsonSchemaTest.res +68 -0
  56. package/tests/api/SuryToJsonSchemaTest.res.mjs +843 -0
  57. package/tests/commandgenerator/CommandGeneratorCallbackTest.res.mjs +160 -0
  58. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +197 -0
  59. package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +152 -0
  60. package/tests/commandtopic/CommandTopicCallbackTest.res.mjs +106 -0
  61. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +109 -0
  62. package/tests/counter/CounterCallbackTest.res.mjs +140 -0
  63. package/tests/counter/CounterFixtures.res.mjs +67 -0
  64. package/tests/dcb/DcbDecodeTest.res.mjs +194 -0
  65. package/tests/dcb/DcbEventLogOperationsTest.res.mjs +268 -0
  66. package/tests/dcb/DcbFixtures.res.mjs +534 -0
  67. package/tests/dcb/DcbScopeInferenceTest.res.mjs +480 -0
  68. package/tests/dcb/DcbStateChangeSliceTest.res.mjs +540 -0
  69. package/tests/dcb/DcbTagTest.res.mjs +867 -0
  70. package/tests/dcb/DcbValidationTest.res.mjs +476 -0
  71. package/tests/eventlog/EventLogFixtures.res.mjs +196 -0
  72. package/tests/eventlog/EventLogOperationsTest.res.mjs +192 -0
  73. package/tests/eventlog/EventLogRetryTest.res.mjs +195 -0
  74. package/tests/eventmapper/EventMapperCallbackTest.res.mjs +141 -0
  75. package/tests/eventmapper/EventMapperFixtures.res.mjs +440 -0
  76. package/tests/eventmapper/EventMapperTest.res.mjs +83 -0
  77. package/tests/eventtopic/EventTopicFixtures.res.mjs +65 -0
  78. package/tests/eventtopic/EventTopicOperationsTest.res.mjs +71 -0
  79. package/tests/extensionpoint/ExtensionPointCallbackTest.res.mjs +94 -0
  80. package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +205 -0
  81. package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +304 -0
  82. package/tests/extensionpoint/PluginDisconnectScheduleTest.res.mjs +92 -0
  83. package/tests/ftp/FTPTest.res.mjs +88 -0
  84. package/tests/logger/LogFormatTest.res.mjs +329 -0
  85. package/tests/logger/LoggerLazyTest.res.mjs +40 -0
  86. package/tests/message/MessageTest.res.mjs +218 -0
  87. package/tests/message/MetaEnvelopeTest.res.mjs +188 -0
  88. package/tests/plugin/HeartbeatDisconnectGraceTest.res.mjs +91 -0
  89. package/tests/plugin/ManifestVisibilityTest.res.mjs +215 -0
  90. package/tests/plugin/PluginSpecExperiment.res.mjs +31 -0
  91. package/tests/plugin/PluginStructureTest.res +55 -0
  92. package/tests/plugin/PluginStructureTest.res.mjs +467 -0
  93. package/tests/plugin/PluginVersionTest.res.mjs +30 -0
  94. package/tests/plugin/SchemaWalkerTest.res.mjs +160 -0
  95. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +62 -0
  96. package/tests/plugin/StateChangeSlice/PsShipOrder.res +4 -1
  97. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +77 -0
  98. package/tests/plugin/StateViewSlice/PsAnnotatedView.res +5 -4
  99. package/tests/plugin/StateViewSlice/PsAnnotatedView.res.mjs +110 -0
  100. package/tests/plugin/StateViewSlice/PsAvailableProductsView.res +1 -1
  101. package/tests/plugin/StateViewSlice/PsAvailableProductsView.res.mjs +70 -0
  102. package/tests/plugin/StateViewSlice/PsCustomersView.res +1 -1
  103. package/tests/plugin/StateViewSlice/PsCustomersView.res.mjs +70 -0
  104. package/tests/plugin/StateViewSlice/PsOrdersView.res +1 -1
  105. package/tests/plugin/StateViewSlice/PsOrdersView.res.mjs +82 -0
  106. package/tests/poc/StructuralSubtypingPoc.res.mjs +61 -0
  107. package/tests/projection/ProjectionOptimizeTest.res.mjs +75 -0
  108. package/tests/querydb/QueryDbFixtures.res.mjs +135 -0
  109. package/tests/querydb/QueryDbOperationsTest.res.mjs +168 -0
  110. package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res.mjs +77 -0
  111. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res.mjs +139 -0
  112. package/tests/util/CommandPublisherTest.res.mjs +78 -0
  113. package/tests/util/CsvStreamTest.res.mjs +51 -0
  114. package/tests/util/LruTest.res.mjs +105 -0
  115. package/tests/util/MetricsTest.res.mjs +53 -0
@@ -59,7 +59,11 @@ function Make(Spec) {
59
59
  };
60
60
  return {
61
61
  TAG: "Ok",
62
- _0: []
62
+ _0: {
63
+ requestId: requestId,
64
+ targetIds: [],
65
+ commandCount: 0
66
+ }
63
67
  };
64
68
  }
65
69
  let msgs = {
@@ -101,7 +105,10 @@ function Make(Spec) {
101
105
  };
102
106
  return {
103
107
  TAG: "Error",
104
- _0: msg$1
108
+ _0: {
109
+ requestId: requestId,
110
+ error: msg$1
111
+ }
105
112
  };
106
113
  }
107
114
  try {
@@ -116,7 +123,11 @@ function Make(Spec) {
116
123
  };
117
124
  return {
118
125
  TAG: "Ok",
119
- _0: targetIds
126
+ _0: {
127
+ requestId: requestId,
128
+ targetIds: targetIds,
129
+ commandCount: pairs$1.length
130
+ }
120
131
  };
121
132
  } catch (raw_exn$1) {
122
133
  let exn$1 = Primitive_exceptions.internalToException(raw_exn$1);
@@ -129,7 +140,10 @@ function Make(Spec) {
129
140
  };
130
141
  return {
131
142
  TAG: "Error",
132
- _0: msg$2
143
+ _0: {
144
+ requestId: requestId,
145
+ error: msg$2
146
+ }
133
147
  };
134
148
  }
135
149
  } else {
@@ -142,7 +156,10 @@ function Make(Spec) {
142
156
  };
143
157
  return {
144
158
  TAG: "Error",
145
- _0: msg$3
159
+ _0: {
160
+ requestId: requestId,
161
+ error: msg$3
162
+ }
146
163
  };
147
164
  }
148
165
  } else {
@@ -155,7 +172,10 @@ function Make(Spec) {
155
172
  };
156
173
  return {
157
174
  TAG: "Error",
158
- _0: msg$4
175
+ _0: {
176
+ requestId: requestId,
177
+ error: msg$4
178
+ }
159
179
  };
160
180
  }
161
181
  };
@@ -3,8 +3,6 @@
3
3
  //
4
4
  // Follows the AutomationSlice_Builder pattern for adapter injection.
5
5
 
6
- let log = Logger.fromEnv()
7
-
8
6
  module Make = (
9
7
  RuntimeEnvironment: Runtime.Environment,
10
8
  QueryDbStorage: QueryDb_Adapter.Storage,
@@ -80,18 +78,25 @@ module Make = (
80
78
  )
81
79
 
82
80
  let dcbEventTopicOutputs: EventTopic.outputs = (dcbEventLog->Component.outputs).eventTopic
83
- let allEventTopics = Dict.fromArray([(Spec.name, dcbEventTopicOutputs)])
84
-
85
- // Resolve publishJsons so it's available for Phase 2
86
- let publishJsonsRef: ref<option<ReventlessInfra.CommandTopic.publishJsons>> = ref(None)
87
- let _ = publishJsons->Pulumi.Output.apply(pj => {
88
- publishJsonsRef := Some(pj)
89
- })
90
-
81
+ // Key the topic by the source-name convention AutomationSlice and
82
+ // Dcb_Builder use (`<pluginName>DcbEventLog`) rather than by this slice's
83
+ // own name. No backend routes on the key — local subscribes on
84
+ // `resource.name`, AWS reads `Dict.valuesToArray` — but channel specs are
85
+ // merged with `Dict.assign`, so slice-specific keys make one topic appear
86
+ // once per slice instead of collapsing to a single entry.
87
+ let dcbEventLogName =
88
+ (dcbEventLog->Component.toPulumiResource).name->Option.getOr("") ++ "DcbEventLog"
89
+ let allEventTopics = Dict.fromArray([(dcbEventLogName, dcbEventTopicOutputs)])
90
+
91
+ // Build the EventCollector inside an Output.all2 so both `queryDbOps` and
92
+ // `publishJsonsFn` are captured in the same closure. The jsonEventsHandler
93
+ // then has direct access to `publishJsonsFn` — avoids racing a
94
+ // side-effect-only `publishJsons.apply` against the first event arrival
95
+ // (which silently dropped Phase 2 in the AutomationSlice's previous shape).
91
96
  let eventCollector =
92
- queryDb
93
- ->Component.operations
94
- ->Pulumi.Output.apply(queryDbOps => {
97
+ (queryDb->Component.operations, publishJsons)
98
+ ->Pulumi.Output.all2
99
+ ->Pulumi.Output.apply(((queryDbOps, publishJsonsFn)) => {
95
100
  let ec = SpecificEventCollector.make(
96
101
  ~name=Spec.name,
97
102
  ~eventTopics=allEventTopics,
@@ -104,7 +109,17 @@ module Make = (
104
109
  ->Stream.mapEffect(json =>
105
110
  Effect.sync(
106
111
  () => {
107
- let (eventType, dataDict) = json->Message.splitMessage
112
+ // Events arrive as `{id, meta, event}` envelopes (see
113
+ // Message.composeEventJson'). The decoder works on the inner
114
+ // `event` payload — TAG + fields at the top level — not the
115
+ // wrapper. Splitting the wrapper yields eventType "Unknown",
116
+ // which DcbDecode drops without warning.
117
+ let rawEvent =
118
+ json
119
+ ->JSON.Decode.object
120
+ ->Option.flatMap(d => d->Dict.get("event"))
121
+ ->Option.getOr(json)
122
+ let (eventType, dataDict) = rawEvent->Message.splitMessage
108
123
  switch decoder.decode(~eventType, ~data=dataDict) {
109
124
  | Some(event) => [event]
110
125
  | None => []
@@ -126,29 +141,21 @@ module Make = (
126
141
  // an inbound command whose downstream events fan back to this
127
142
  // same topic, awaiting would self-deadlock the bus — same
128
143
  // shape as AutomationSlice. Detach it; errors are logged.
129
- switch publishJsonsRef.contents {
130
- | Some(pj) =>
131
- let _ =
132
- Callback.phase2(pj)
133
- ->Promise.then(() => syncToQueryDb(queryDbOps))
134
- ->Promise.catch(exn => {
135
- let errMsg =
136
- exn
137
- ->JsExn.fromException
138
- ->Option.flatMap(JsExn.message)
139
- ->Option.getOr("unknown")
140
- EffectLogger.logError(
141
- ~comp=`OutboundTranslationSlice(${Spec.name})`,
142
- `detached phase 2 error: ${errMsg}`,
143
- )->Effect.runSync
144
- Promise.resolve()
145
- })
146
- | None =>
147
- log.error(
148
- ~comp=`OutboundTranslationSlice(${Spec.name})`,
149
- `publishJsons not yet resolved`,
150
- )
151
- }
144
+ let _ =
145
+ Callback.phase2(publishJsonsFn)
146
+ ->Promise.then(() => syncToQueryDb(queryDbOps))
147
+ ->Promise.catch(exn => {
148
+ let errMsg =
149
+ exn
150
+ ->JsExn.fromException
151
+ ->Option.flatMap(JsExn.message)
152
+ ->Option.getOr("unknown")
153
+ EffectLogger.logError(
154
+ ~comp=`OutboundTranslationSlice(${Spec.name})`,
155
+ `detached phase 2 error: ${errMsg}`,
156
+ )->Effect.runSync
157
+ Promise.resolve()
158
+ })
152
159
  },
153
160
  )
154
161
  )
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
5
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
6
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
6
7
  import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
@@ -13,7 +14,6 @@ import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
13
14
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
14
15
  import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/components/DcbDecode.res.mjs";
15
16
  import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
16
- import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
17
17
  import * as Message$ReventlessCore from "../../Message.res.mjs";
18
18
  import * as Component$ReventlessCore from "../Component.res.mjs";
19
19
  import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
@@ -24,8 +24,6 @@ import * as EventCollector_Builder$ReventlessCore from "../EventCollector/EventC
24
24
  import * as OutboundTranslationSlice$ReventlessCore from "./OutboundTranslationSlice.res.mjs";
25
25
  import * as OutboundTranslationSlice_Callback$ReventlessCore from "./OutboundTranslationSlice_Callback.res.mjs";
26
26
 
27
- let log = Logger$ReventlessCore.fromEnv();
28
-
29
27
  function Make(RuntimeEnvironment) {
30
28
  return QueryDbStorage => (QueryDbResolvers => (EventCollectorChannel => (EventCollectorRuntimeBuilder => (Api => {
31
29
  let Make = Spec => (Translation => {
@@ -64,23 +62,24 @@ function Make(RuntimeEnvironment) {
64
62
  name: Spec.name
65
63
  }, opts);
66
64
  let dcbEventTopicOutputs = Component$ReventlessCore.outputs(dcbEventLog).eventTopic;
65
+ let dcbEventLogName = Stdlib_Option.getOr(Component$ReventlessCore.toPulumiResource(dcbEventLog).__name, "") + "DcbEventLog";
67
66
  let allEventTopics = Object.fromEntries([[
68
- Spec.name,
67
+ dcbEventLogName,
69
68
  dcbEventTopicOutputs
70
69
  ]]);
71
- let publishJsonsRef = {
72
- contents: undefined
73
- };
74
- publishJsons.apply(pj => {
75
- publishJsonsRef.contents = pj;
76
- });
77
- let eventCollector = Component$ReventlessCore.operations(queryDb).apply(queryDbOps => {
70
+ let eventCollector = Pulumi.all([
71
+ Component$ReventlessCore.operations(queryDb),
72
+ publishJsons
73
+ ]).apply(param => {
74
+ let publishJsonsFn = param[1];
75
+ let queryDbOps = param[0];
78
76
  let ec = SpecificEventCollector.make(Spec.name, allEventTopics, {
79
77
  kind: "OutboundTranslationSlice",
80
78
  name: Spec.name
81
79
  }, opts);
82
80
  let jsonEventsHandler = stream => Effect.flatMap(Stream.runCollect(Stream$1.flatMap(Stream$1.mapEffect(stream, json => Effect.sync(() => {
83
- let match = Message$ReventlessCore.splitMessage(json);
81
+ let rawEvent = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["event"]), json);
82
+ let match = Message$ReventlessCore.splitMessage(rawEvent);
84
83
  let event = decoder.decode(match[0], match[1]);
85
84
  if (event !== undefined) {
86
85
  return [Primitive_option.valFromOption(event)];
@@ -90,17 +89,11 @@ function Make(RuntimeEnvironment) {
90
89
  })), events => Stream$1.fromIterable(events))), eventsArr => Effect.promise(async () => {
91
90
  Callback.phase1(eventsArr);
92
91
  await syncToQueryDb(queryDbOps);
93
- let pj = publishJsonsRef.contents;
94
- if (pj !== undefined) {
95
- Stdlib_Promise.$$catch(Callback.phase2(pj).then(() => syncToQueryDb(queryDbOps)), exn => {
96
- let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
97
- Effect.runSync(EffectLogger$ReventlessCore.logError(`OutboundTranslationSlice(` + Spec.name + `)`, undefined, `detached phase 2 error: ` + errMsg));
98
- return Promise.resolve();
99
- });
100
- return;
101
- } else {
102
- return log.error(`OutboundTranslationSlice(` + Spec.name + `)`, undefined, `publishJsons not yet resolved`);
103
- }
92
+ Stdlib_Promise.$$catch(Callback.phase2(publishJsonsFn).then(() => syncToQueryDb(queryDbOps)), exn => {
93
+ let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
94
+ Effect.runSync(EffectLogger$ReventlessCore.logError(`OutboundTranslationSlice(` + Spec.name + `)`, undefined, `detached phase 2 error: ` + errMsg));
95
+ return Promise.resolve();
96
+ });
104
97
  }));
105
98
  let handler = SpecificEventCollector.makeHandler(ec, jsonEventsHandler);
106
99
  let resources = Component$ReventlessCore.outputs(queryDb).resources;
@@ -140,7 +133,6 @@ function Make(RuntimeEnvironment) {
140
133
  }
141
134
 
142
135
  export {
143
- log,
144
136
  Make,
145
137
  }
146
- /* log Not a pure module */
138
+ /* Stream Not a pure module */
@@ -227,6 +227,18 @@ module Make = (
227
227
  // only the delta the conflicting writer added rather than full history.
228
228
  let seed = ref(cacheGet())
229
229
 
230
+ // One hit/miss sample per command, keyed on whether the warm-instance cache
231
+ // seeded this command's decision read. If Miss dominates Hit for a slice, the
232
+ // flat-capacity cache is thrashing a wide entity set — the operator's cue to
233
+ // disable it. Paired with DcbDecisionModelDeltaEventCount (the delta size a
234
+ // hit actually read); both consumed by the reventless-aws metric filters.
235
+ Metrics.emitCount(
236
+ ~metric=seed.contents->Option.isSome
237
+ ? "DcbDecisionModelCacheHit"
238
+ : "DcbDecisionModelCacheMiss",
239
+ ~slice=Spec.name,
240
+ )
241
+
230
242
  let rec attempt = (~retries) => {
231
243
  let (baseState, afterPos) = switch seed.contents {
232
244
  | Some((state, head)) => (state, head)
@@ -279,14 +291,27 @@ module Make = (
279
291
  },
280
292
  ]),
281
293
  ))
282
- ->Effect.tap(((_, _, reads)) =>
294
+ ->Effect.tap(((_, _, reads)) => {
295
+ // On a warm first attempt, record how many events the delta read
296
+ // returned — near-zero on a healthy hit; a large value means the entity
297
+ // churned between commands, so the cache saved little. Gated to the first
298
+ // attempt (retries at maxRetries) so each command yields at most one
299
+ // sample, aligned 1:1 with a CacheHit. Conflict-driven retry reads are
300
+ // covered by AppendRetry instead.
301
+ if cacheHit && retries == maxRetries {
302
+ Metrics.emitCount(
303
+ ~metric="DcbDecisionModelDeltaEventCount",
304
+ ~slice=Spec.name,
305
+ ~value=reads->Array.length,
306
+ )
307
+ }
283
308
  EffectLogger.logInfo(
284
309
  ~comp,
285
310
  `read${cacheHit ? " (cached, delta)" : ""}: ${reads->Array.length->Int.toString} event(s)${reads->Array.length == 0
286
311
  ? ""
287
312
  : ` [${reads->Array.join(", ")}]`}`,
288
313
  )
289
- )
314
+ })
290
315
  ->Effect.flatMap(((state, headPosition, _)) =>
291
316
  EffectLogger.logDebug(
292
317
  ~comp,
@@ -101,6 +101,7 @@ function Make(Spec) {
101
101
  let seed = {
102
102
  contents: cacheGet()
103
103
  };
104
+ Metrics$ReventlessCore.emitCount(Stdlib_Option.isSome(seed.contents) ? "DcbDecisionModelCacheHit" : "DcbDecisionModelCacheMiss", Spec.name, undefined);
104
105
  let attempt = retries => {
105
106
  let match = seed.contents;
106
107
  let match$1 = match !== undefined ? [
@@ -152,6 +153,9 @@ function Make(Spec) {
152
153
  ];
153
154
  }), param => {
154
155
  let reads = param[2];
156
+ if (cacheHit && retries === 3) {
157
+ Metrics$ReventlessCore.emitCount("DcbDecisionModelDeltaEventCount", Spec.name, reads.length);
158
+ }
155
159
  return EffectLogger$ReventlessCore.logInfo(comp, undefined, `read` + (
156
160
  cacheHit ? " (cached, delta)" : ""
157
161
  ) + `: ` + reads.length.toString() + ` event(s)` + (
@@ -95,16 +95,30 @@ module Make = (
95
95
  let total = events->Array.length->Int.toString
96
96
  events
97
97
  ->Array.mapWithIndex((json, i) => {
98
- let rawEvent =
99
- json
100
- ->JSON.Decode.object
101
- ->Option.flatMap(d => d->Dict.get("event"))
102
- ->Option.getOr(json)
98
+ let envelopeDict = json->JSON.Decode.object->Option.getOr(Dict.make())
99
+ let rawEvent = envelopeDict->Dict.get("event")->Option.getOr(json)
100
+ // Events arrive as `{id, meta, recordedAt, event}` envelopes
101
+ // (Message.composeEventJson' / ProjectionCheckpoint catch-up).
102
+ // Surface `meta` + `recordedAt` to the projection as `consumed`;
103
+ // fall back defensively so a malformed envelope never drops the event.
104
+ let meta = switch envelopeDict->Dict.get("meta") {
105
+ | Some(m) =>
106
+ switch m->S.parseJsonOrThrow(Message.metaSchema) {
107
+ | parsed => parsed
108
+ | exception _ => Message.generateMeta(~service=Spec.name)
109
+ }
110
+ | None => Message.generateMeta(~service=Spec.name)
111
+ }
112
+ let recordedAt =
113
+ envelopeDict
114
+ ->Dict.get("recordedAt")
115
+ ->Option.flatMap(JSON.Decode.string)
116
+ ->Option.getOr("")
103
117
  let (eventType, dataDict) = rawEvent->Message.splitMessage
104
118
  switch decoder.decode(~eventType, ~data=dataDict) {
105
119
  | Some(event) =>
106
120
  let actions =
107
- try Projection.project(event)
121
+ try Projection.project({event, meta, recordedAt})
108
122
  catch {
109
123
  | exn =>
110
124
  let errMsg =
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as S from "sury/src/S.res.mjs";
3
4
  import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
4
5
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
6
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
@@ -80,7 +81,20 @@ function Make(RuntimeEnvironment) {
80
81
  let jsonEventsHandler = stream => Effect.flatMap(Stream.runCollect(stream), events => {
81
82
  let total = events.length.toString();
82
83
  return Stdlib_Array.reduce(events.map((json, i) => {
83
- let rawEvent = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["event"]), json);
84
+ let envelopeDict = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(json), {});
85
+ let rawEvent = Stdlib_Option.getOr(envelopeDict["event"], json);
86
+ let m = envelopeDict["meta"];
87
+ let meta;
88
+ if (m !== undefined) {
89
+ try {
90
+ meta = S.parseJsonOrThrow(m, Message$ReventlessCore.metaSchema);
91
+ } catch (exn) {
92
+ meta = Message$ReventlessCore.generateMeta(Spec.name, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
93
+ }
94
+ } else {
95
+ meta = Message$ReventlessCore.generateMeta(Spec.name, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
96
+ }
97
+ let recordedAt = Stdlib_Option.getOr(Stdlib_Option.flatMap(envelopeDict["recordedAt"], Stdlib_JSON.Decode.string), "");
84
98
  let match = Message$ReventlessCore.splitMessage(rawEvent);
85
99
  let dataDict = match[1];
86
100
  let eventType = match[0];
@@ -90,10 +104,14 @@ function Make(RuntimeEnvironment) {
90
104
  }
91
105
  let actions;
92
106
  try {
93
- actions = Projection.project(Primitive_option.valFromOption(event));
107
+ actions = Projection.project({
108
+ event: Primitive_option.valFromOption(event),
109
+ meta: meta,
110
+ recordedAt: recordedAt
111
+ });
94
112
  } catch (raw_exn) {
95
- let exn = Primitive_exceptions.internalToException(raw_exn);
96
- let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
113
+ let exn$1 = Primitive_exceptions.internalToException(raw_exn);
114
+ let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn$1), Stdlib_JsExn.message), "unknown");
97
115
  Effect.runSync(EffectLogger$ReventlessCore.logError(comp, undefined, `project failed for ` + eventType + `: ` + errMsg));
98
116
  actions = [];
99
117
  }
@@ -140,4 +158,4 @@ export {
140
158
  FrameworkProjection,
141
159
  Make,
142
160
  }
143
- /* Stream Not a pure module */
161
+ /* S Not a pure module */
@@ -40,7 +40,7 @@ module Make = (
40
40
  | Some(event) =>
41
41
  idx := idx.contents + 1
42
42
  let id = raw.tags->Array.get(0)->Option.map(tag => tag.value)->Option.getOr("?")
43
- let actions = Projection.project(event)
43
+ let actions = Projection.project({event, meta: raw.meta, recordedAt: raw.recordedAt})
44
44
  let actionsStr = LogFormat.actionNames(actions)
45
45
  EffectLogger.logInfo(
46
46
  ~comp,
@@ -28,7 +28,11 @@ function Make(Spec) {
28
28
  let event = Primitive_option.valFromOption(decoded);
29
29
  idx.contents = idx.contents + 1 | 0;
30
30
  let id = Stdlib_Option.getOr(Stdlib_Option.map(raw.tags[0], tag => tag.value), "?");
31
- let actions = Projection.project(event);
31
+ let actions = Projection.project({
32
+ event: event,
33
+ meta: raw.meta,
34
+ recordedAt: raw.recordedAt
35
+ });
32
36
  let actionsStr = LogFormat$ReventlessCore.actionNames(actions);
33
37
  Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, raw.data, `handling event ` + idx.contents.toString() + `/` + count + `: ` + LogFormat$ReventlessCore.bold(raw.eventType) + `(` + id + `) ` + actionsStr));
34
38
  allActions.push(...actions);
@@ -932,7 +932,10 @@ type platformHooks = {
932
932
  // InboundTranslationSlice — phase 1: register SDL + stub.
933
933
  inboundMutationResolverHook?: (~fieldName: string, ~externalInputSchema: S.t<unknown>) => unit,
934
934
  // InboundTranslationSlice — phase 2: bind receive.
935
- inboundMutationBindReceiveHook?: (~fieldName: string, ~receive: JSON.t => promise<result<array<string>, string>>) => unit,
935
+ inboundMutationBindReceiveHook?: (
936
+ ~fieldName: string,
937
+ ~receive: JSON.t => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
938
+ ) => unit,
936
939
  // GraphQL type definitions.
937
940
  schemaTypeRegistrationHook?: array<string> => unit,
938
941
  // Subscription infrastructure (AWS): wire StateTopic + EventLogSubscription Lambdas.
@@ -171,6 +171,10 @@ let make = (
171
171
  // markAllowedStates). Look it up by variant name; back-compat
172
172
  // None when the variant lacks an @allowedStates annotation.
173
173
  let allowedStates = ApiAllowedStatesHelpers.getAllowedStates(parentSchema, ~variantName)
174
+ // Declared `@targetState` (the command's *to* status), read the same way
175
+ // as allowedStates. None ⇒ AutoUI's board resolver falls back to its
176
+ // name-stem heuristic.
177
+ let targetState = ApiTargetStateHelpers.getTargetState(parentSchema, ~variantName)
174
178
  // API-exposed iff the whole command isn't @noApi and this variant
175
179
  // isn't in its @noApi-variants set — mirrors the API-generation filter
176
180
  // (Plugin_Helpers / PluginBaseFragment). Drives the event-graph API badge.
@@ -185,9 +189,19 @@ let make = (
185
189
  schema: (v->S.toJSONSchema->Obj.magic: JSON.t)->JSON.stringify,
186
190
  level,
187
191
  aggregateIdField,
188
- mutationField: mutationFieldFor(variantName),
192
+ // A non-exposed (`@noApi`) variant has no callable mutation field. For a
193
+ // single-exposed-command slice, `mutationFieldFor` resolves *every*
194
+ // variant — including the `@noApi` one — to the slice's one mutation
195
+ // field, so emitting it here would hand the non-exposed variant a
196
+ // sibling's callable-looking field (e.g. `ReopenOrder` →
197
+ // `Ordering_CancelOrder`). Emit an empty sentinel instead; the variant
198
+ // stays listed with `apiExposed: false` for the event-graph badge, but
199
+ // no consumer can mistake it for a callable field. Exposed variants are
200
+ // byte-identical.
201
+ mutationField: apiExposed ? mutationFieldFor(variantName) : "",
189
202
  references,
190
203
  allowedStates,
204
+ targetState,
191
205
  apiExposed: Some(apiExposed),
192
206
  }: Reventless.Plugin.commandDef)
193
207
  }
@@ -13,6 +13,7 @@ import * as Api_Naming$ReventlessCore from "../../components/Api/Api_Naming.res.
13
13
  import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
14
14
  import * as ApiNoApiHelpers$ReventlessCore from "../../components/Api/ApiNoApiHelpers.res.mjs";
15
15
  import * as SuryToJsonSchema$ReventlessCore from "../../components/Api/SuryToJsonSchema.res.mjs";
16
+ import * as ApiTargetStateHelpers$ReventlessCore from "../../components/Api/ApiTargetStateHelpers.res.mjs";
16
17
  import * as ApiAllowedStatesHelpers$ReventlessCore from "../../components/Api/ApiAllowedStatesHelpers.res.mjs";
17
18
 
18
19
  let log = Logger$ReventlessCore.fromEnv();
@@ -147,6 +148,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
147
148
  }));
148
149
  });
149
150
  let allowedStates = ApiAllowedStatesHelpers$ReventlessCore.getAllowedStates(parentSchema, variantName);
151
+ let targetState = ApiTargetStateHelpers$ReventlessCore.getTargetState(parentSchema, variantName);
150
152
  let apiExposed = false;
151
153
  if (!ApiNoApiHelpers$ReventlessCore.isNoApi(parentSchema)) {
152
154
  let excluded = ApiNoApiHelpers$ReventlessCore.getExcludedVariants(parentSchema);
@@ -157,9 +159,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
157
159
  schema: JSON.stringify(S.toJSONSchema(v)),
158
160
  level: match[0],
159
161
  aggregateIdField: match[1],
160
- mutationField: mutationFieldFor(variantName),
162
+ mutationField: apiExposed ? mutationFieldFor(variantName) : "",
161
163
  references: references,
162
164
  allowedStates: allowedStates,
165
+ targetState: targetState,
163
166
  apiExposed: apiExposed
164
167
  };
165
168
  };
@@ -46,6 +46,8 @@ let stateSchema$1 = S.Metadata.set(stateSchema, StateAnnotations$Reventless.stat
46
46
  collapsed: [],
47
47
  scan: ["kind"],
48
48
  scanSort: [],
49
+ semantic: [],
50
+ metric: [],
49
51
  status: undefined,
50
52
  groupBy: "kind",
51
53
  visibility: undefined