@reventlessdev/reventless-core 3.0.0-alpha.207 → 3.0.0-alpha.209

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 CHANGED
@@ -3,6 +3,23 @@
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.209 (2026-08-03)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** give slice command publishes a queue they can actually reach ([42fdb55](https://github.com/ReventlessDev/reventless-core/commit/42fdb55b751a197d586c49e3615ce12ffea42046))
11
+ ### Features
12
+
13
+ * **dcb:** route an outbound slice's command to the aggregate it targets ([897b37b](https://github.com/ReventlessDev/reventless-core/commit/897b37b46a4e856567646dfdc2f0652e81cf7388))
14
+
15
+
16
+ # 3.0.0-alpha.208 (2026-08-03)
17
+
18
+ ### Bug Fixes
19
+
20
+ * **aws:** make the slice callback protocol one declaration, not three ([d49e543](https://github.com/ReventlessDev/reventless-core/commit/d49e543f4715c955c75cec8bd691ee0927b7e2f4))
21
+
22
+
6
23
  # 3.0.0-alpha.207 (2026-08-03)
7
24
 
8
25
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.207",
3
+ "version": "3.0.0-alpha.209",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,13 +29,13 @@
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-fast-csv": "2.0.0-alpha.1",
32
33
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
33
- "@reventlessdev/rescript-node": "2.0.0-alpha.1",
34
34
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
35
- "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.1",
35
+ "@reventlessdev/rescript-node": "2.0.0-alpha.1",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
37
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
38
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.1",
38
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
39
39
  "@reventlessdev/reventless-infra": "3.0.0-alpha.124",
40
40
  "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
41
41
  "@reventlessdev/reventless-spec": "3.0.0-alpha.99"
@@ -78,7 +78,7 @@ function Make(RuntimeEnvironment) {
78
78
  A.Spec.eventSchema
79
79
  ]);
80
80
  Plugin_Helpers$ReventlessCore.registerAdminAggregateMutations(aggregates, Config.hooks);
81
- let dcbResult = DcbBuilder.construct(Platform_Admin_Structure$ReventlessCore.pluginId, Platform_Admin_Structure$ReventlessCore.pluginId, "Platform", true, undefined, undefined, aggregateEventTopics, aggregateProducedEvents, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, undefined, undefined, undefined, opts);
81
+ let dcbResult = DcbBuilder.construct(Platform_Admin_Structure$ReventlessCore.pluginId, Platform_Admin_Structure$ReventlessCore.pluginId, "Platform", true, undefined, undefined, aggregateEventTopics, aggregateProducedEvents, undefined, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, undefined, undefined, undefined, opts);
82
82
  let slicePublishJsons = dcbResult.dcbPublishJsons;
83
83
  if (slicePublishJsons !== undefined) {
84
84
  stateChangeSlices.forEach(Sc => {
@@ -66,7 +66,17 @@ module Make = (
66
66
  }
67
67
  }
68
68
 
69
- let createCommandTopic = (eventLog: SpecificEventLog.component, name, opts, ~memorySize, ~timeout) =>
69
+ let createCommandTopic = (eventLog: SpecificEventLog.component, name, opts, ~memorySize, ~timeout) => {
70
+ // Created outside the apply below, so the topic — and with it the channel's
71
+ // queue — exists while plugin construction is still synchronous. An outbound
72
+ // slice targeting this aggregate resolves its publish queue in a finalizer
73
+ // that runs before any Output settles; a queue created inside the apply is
74
+ // invisible there. Only the handler needs the event log's operations.
75
+ let commandTopic = SpecificCommandTopic.make(
76
+ ~name,
77
+ ~owner={kind: ComponentType.Aggregate, name: Spec.name},
78
+ ~opts,
79
+ )
70
80
  eventLog
71
81
  ->Component.operations
72
82
  ->Pulumi.Output.apply(eventLogOps => {
@@ -79,11 +89,6 @@ module Make = (
79
89
  let eventLog = eventLogOps
80
90
  },
81
91
  )
82
- let commandTopic = SpecificCommandTopic.make(
83
- ~name,
84
- ~owner={kind: ComponentType.Aggregate, name: Spec.name},
85
- ~opts,
86
- )
87
92
  let handler = SpecificCommandTopic.makeHandler(
88
93
  ~commandTopic,
89
94
  ~commandsHandler=AggregateCallback.handleCommands,
@@ -98,6 +103,7 @@ module Make = (
98
103
  )
99
104
  commandTopic
100
105
  })
106
+ }
101
107
 
102
108
  let createCommandGenerator = (
103
109
  commandTopic: Pulumi.Output.t<SpecificCommandTopic.component>,
@@ -30,25 +30,27 @@ function Make(Spec) {
30
30
  Id: Spec.Id,
31
31
  commandSchema: Spec.commandSchema
32
32
  })(SpecificEventCollector)(EventMappings)(AggregateRuntimeBuilder);
33
- let createCommandTopic = (eventLog, name, opts, memorySize, timeout) => Component$ReventlessCore.operations(eventLog).apply(eventLogOps => {
34
- let AggregateCallback = Aggregate_Callback$ReventlessCore.Make(Spec)(Behavior)({
35
- Spec: Spec,
36
- EventLog: SpecificEventLog,
37
- eventLog: eventLogOps
38
- });
33
+ let createCommandTopic = (eventLog, name, opts, memorySize, timeout) => {
39
34
  let commandTopic = SpecificCommandTopic.make(name, {
40
35
  kind: "Aggregate",
41
36
  name: Spec.name
42
37
  }, opts);
43
- let handler = SpecificCommandTopic.makeHandler(commandTopic, AggregateCallback.handleCommands);
44
- let eventLog$1 = Component$ReventlessCore.outputs(eventLog);
45
- let resources = [
46
- eventLog$1.resources,
47
- eventLog$1.eventTopic.resources
48
- ].flat();
49
- AggregateRuntimeBuilder.forCommandTopic(handler, none => SpecificCommandTopic.connect(none, resources, commandTopic), memorySize, timeout, commandTopic);
50
- return commandTopic;
51
- });
38
+ return Component$ReventlessCore.operations(eventLog).apply(eventLogOps => {
39
+ let AggregateCallback = Aggregate_Callback$ReventlessCore.Make(Spec)(Behavior)({
40
+ Spec: Spec,
41
+ EventLog: SpecificEventLog,
42
+ eventLog: eventLogOps
43
+ });
44
+ let handler = SpecificCommandTopic.makeHandler(commandTopic, AggregateCallback.handleCommands);
45
+ let eventLog$1 = Component$ReventlessCore.outputs(eventLog);
46
+ let resources = [
47
+ eventLog$1.resources,
48
+ eventLog$1.eventTopic.resources
49
+ ].flat();
50
+ AggregateRuntimeBuilder.forCommandTopic(handler, none => SpecificCommandTopic.connect(none, resources, commandTopic), memorySize, timeout, commandTopic);
51
+ return commandTopic;
52
+ });
53
+ };
52
54
  let createCommandGenerator = (commandTopic, api, name, opts, memorySize, timeout) => Output$Pulumi.flatMap(commandTopic, commandTopic => Component$ReventlessCore.operations(commandTopic).apply(param => {
53
55
  let publishJsonsAndWait = param.publishJsonsAndWait;
54
56
  let publishJsons = param.publishJsons;
@@ -31,6 +31,30 @@ type todoRow = {
31
31
  retryCount: int,
32
32
  }
33
33
 
34
+ /**
35
+ The runtime protocol between a built callback and whoever drives it — the same
36
+ arrangement as `OutboundTranslationSlice_Callback`, and for the same reason. The
37
+ AWS entry point builds callbacks dynamically and has to ascribe a structural type
38
+ at the JS boundary; pointing that ascription at these aliases is what keeps it
39
+ from becoming an unreconcilable second declaration of the shape.
40
+
41
+ Unlike outbound, phase 1 here takes *raw* envelope JSONs: the callback routes by
42
+ `meta.service` and unwraps `{id, meta, event}` itself, per-source, through the
43
+ registered Mappings.
44
+ */
45
+ type phase1 = (array<JSON.t>, Reventless.AutomationSlice.context) => unit
46
+
47
+ /** Phase 2: process all pending items, publishing commands. */
48
+ type phase2 = ReventlessInfra.CommandTopic.publishJsons => promise<unit>
49
+
50
+ /** The whole protocol as a value, for a driver that holds a built callback and
51
+ knows nothing of the Spec behind it. */
52
+ type runtime = {
53
+ todoItems: Dict.t<todoRow>,
54
+ phase1: phase1,
55
+ phase2: phase2,
56
+ }
57
+
34
58
  module type T = {
35
59
  module Spec: Reventless.AutomationSlice.Spec
36
60
  module Automation: Reventless.AutomationSlice.Automation with module Spec := Spec
@@ -40,10 +64,10 @@ module type T = {
40
64
 
41
65
  /** Phase 1: update TODO list from a batch of raw event JSONs. Internally
42
66
  decodes per-source via the registered Mappings. */
43
- let phase1: (array<JSON.t>, Reventless.AutomationSlice.context) => unit
67
+ let phase1: phase1
44
68
 
45
69
  /** Phase 2: process all pending items, publishing commands via publishJsons. */
46
- let phase2: ReventlessInfra.CommandTopic.publishJsons => promise<unit>
70
+ let phase2: phase2
47
71
  }
48
72
 
49
73
  module Make = (
@@ -94,6 +94,17 @@ module Make = (
94
94
  // also drives DCB GSI creation: an aggregate's events live on its own
95
95
  // EventLog and must not mint indexes on the DCB table.
96
96
  ~aggregateProducedEvents: array<(string, S.t<unknown>)>=[],
97
+ // Per-aggregate command publishers, keyed by Aggregate `Spec.name`. An
98
+ // OutboundTranslationSlice naming one in `targetName` publishes its command
99
+ // through that aggregate's CommandTopic instead of this plugin's DCB topic —
100
+ // the two are handled by different Lambdas, so sending an aggregate command
101
+ // to the DCB queue delivers it somewhere nothing handles it.
102
+ //
103
+ // Populated before this runs (`createAggregatesWithoutEventMappers`), so the
104
+ // publisher is in hand by the time a slice is built. DCB StateChangeSlice
105
+ // publishers are registered *after* and are deliberately absent: a slice
106
+ // targeting one wants the DCB topic, which is the fallback.
107
+ ~publishToAggregates: dict<Pulumi.Output.t<CommandTopic.publishJsons>>=Dict.make(),
97
108
  ~stateChangeSlices: array<module(StateChangeSlice.T)>,
98
109
  ~stateViewSlices: array<module(StateViewSlice.T)>,
99
110
  ~automationSlices: array<module(AutomationSlice.T)>,
@@ -629,12 +640,21 @@ module Make = (
629
640
  let outboundTranslationSlicesOutputs =
630
641
  outboundTranslationSlices
631
642
  ->Array.map((module(OTS: OutboundTranslationSlice.T)) => {
643
+ // Where this slice's inbound command goes. `targetName` naming an
644
+ // Aggregate routes to that aggregate's CommandTopic; anything else —
645
+ // a DCB slice, or no target at all — keeps the plugin's DCB topic,
646
+ // which is what every slice wanted before aggregates were reachable.
647
+ let targetPublishJsons =
648
+ OTS.Spec.targetName
649
+ ->Option.flatMap(target => publishToAggregates->Dict.get(target))
650
+ ->Option.getOr(publishJsons)
651
+
632
652
  let ots = OTS.make(
633
653
  ~dcbEventLog,
634
654
  // Same dict the AutomationSlices get, so an outbound slice can
635
655
  // name an Aggregate source by its Spec.name.
636
656
  ~allEventTopics,
637
- ~publishJsons,
657
+ ~publishJsons=targetPublishJsons,
638
658
  ~runtime=?componentRuntime->Dict.get(OTS.Spec.name),
639
659
  ~opts,
640
660
  )
@@ -83,13 +83,14 @@ let emptyResult = {
83
83
 
84
84
  function Make(DcbEventLogStorage) {
85
85
  return DcbEventTopicPublisher => (DcbCommandTopicChannel => (DcbCommandTopicChannelAsync => (RuntimeBuilder => (HooksConfig => {
86
- let construct = (name, childName, apiNamePrefixOpt, onAdminApiOpt, environmentOpt, platformNameOpt, aggregateEventTopicsOpt, aggregateProducedEventsOpt, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponentsOpt, componentRuntimeOpt, pluginStructure, opts) => {
86
+ let construct = (name, childName, apiNamePrefixOpt, onAdminApiOpt, environmentOpt, platformNameOpt, aggregateEventTopicsOpt, aggregateProducedEventsOpt, publishToAggregatesOpt, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponentsOpt, componentRuntimeOpt, pluginStructure, opts) => {
87
87
  let apiNamePrefix = apiNamePrefixOpt !== undefined ? apiNamePrefixOpt : name;
88
88
  let onAdminApi = onAdminApiOpt !== undefined ? onAdminApiOpt : false;
89
89
  let environment = environmentOpt !== undefined ? environmentOpt : "";
90
90
  let platformName = platformNameOpt !== undefined ? platformNameOpt : "";
91
91
  let aggregateEventTopics = aggregateEventTopicsOpt !== undefined ? aggregateEventTopicsOpt : ({});
92
92
  let aggregateProducedEvents = aggregateProducedEventsOpt !== undefined ? aggregateProducedEventsOpt : [];
93
+ let publishToAggregates = publishToAggregatesOpt !== undefined ? publishToAggregatesOpt : ({});
93
94
  let systemCallableComponents = systemCallableComponentsOpt !== undefined ? systemCallableComponentsOpt : [];
94
95
  let componentRuntime = componentRuntimeOpt !== undefined ? componentRuntimeOpt : ({});
95
96
  let hasDcb = stateChangeSlices.length !== 0;
@@ -330,7 +331,8 @@ function Make(DcbEventLogStorage) {
330
331
  ];
331
332
  }));
332
333
  let outboundTranslationSlicesOutputs = Object.fromEntries(outboundTranslationSlices.map(OTS => {
333
- let ots = OTS.make(dcbEventLog, allEventTopics, publishJsons, componentRuntime[OTS.Spec.name], opts);
334
+ let targetPublishJsons = Stdlib_Option.getOr(Stdlib_Option.flatMap(OTS.Spec.targetName, target => publishToAggregates[target]), publishJsons);
335
+ let ots = OTS.make(dcbEventLog, allEventTopics, targetPublishJsons, componentRuntime[OTS.Spec.name], opts);
334
336
  return [
335
337
  OTS.Spec.name,
336
338
  Component$ReventlessCore.outputs(ots)
@@ -22,6 +22,34 @@ type todoRow = {
22
22
  lastError?: string,
23
23
  }
24
24
 
25
+ /**
26
+ The runtime protocol between a built callback and whoever drives it.
27
+
28
+ Defined here, once, and referenced by every party — `module type T` below, the
29
+ in-process builder, and the AWS entry point, which cannot name `T` because it
30
+ builds callbacks dynamically per `HANDLER_CONFIG` entry and must ascribe a
31
+ structural type at the JS boundary.
32
+
33
+ That ascription is the reason these aliases exist. A driver that restates the
34
+ shape instead of referencing it becomes a second source of truth that the
35
+ compiler cannot reconcile with this one: when `phase1` gained its `sourceId`,
36
+ the AWS entry point kept passing bare events and nothing complained, because its
37
+ hand-written copy was internally consistent. The mismatch surfaced only at
38
+ runtime, as `undefined` destructured from a tuple that was never a tuple.
39
+ */
40
+ type phase1<'event> = array<(string, 'event)> => unit
41
+
42
+ /** Phase 2: translate all pending items, optionally publishing commands. */
43
+ type phase2 = ReventlessInfra.CommandTopic.publishJsons => promise<unit>
44
+
45
+ /** The whole protocol as a value — what a driver holding a built callback needs,
46
+ with no knowledge of the Spec it was built from. */
47
+ type runtime<'event> = {
48
+ todoItems: Dict.t<todoRow>,
49
+ phase1: phase1<'event>,
50
+ phase2: phase2,
51
+ }
52
+
25
53
  module type T = {
26
54
  module Spec: Reventless.OutboundTranslationSlice.Spec
27
55
  module Translation: Reventless.OutboundTranslationSlice.Translation with module Spec := Spec
@@ -32,10 +60,10 @@ module type T = {
32
60
  /** Phase 1: update TODO list from a batch of `(sourceId, event)` pairs
33
61
  (collect only, no resolve). `sourceId` is the envelope id of the entity the
34
62
  event was published for. */
35
- let phase1: array<(string, Spec.consumedEvent)> => unit
63
+ let phase1: phase1<Spec.consumedEvent>
36
64
 
37
65
  /** Phase 2: translate all pending items, optionally publishing commands via publishJsons. */
38
- let phase2: ReventlessInfra.CommandTopic.publishJsons => promise<unit>
66
+ let phase2: phase2
39
67
  }
40
68
 
41
69
  module Make = (
@@ -168,6 +168,9 @@ module Make = (
168
168
  ~platformName=Spec.platformName,
169
169
  ~aggregateEventTopics,
170
170
  ~aggregateProducedEvents,
171
+ // Carries the aggregate publishers registered just above, so an outbound
172
+ // slice can route its command to the aggregate it names.
173
+ ~publishToAggregates,
171
174
  ~stateChangeSlices,
172
175
  ~stateViewSlices,
173
176
  ~automationSlices,
@@ -156,7 +156,7 @@ function Make(Spec) {
156
156
  let DcbBuilder = Dcb_Builder$ReventlessCore.Make(DcbEventLogStorage)(DcbEventTopicPublisher)(DcbCommandTopicChannel)(DcbCommandTopicChannelAsync)(PluginRuntimeBuilder)({
157
157
  hooks: Spec.hooks
158
158
  });
159
- let dcbResult = DcbBuilder.construct(extra$1, childName, undefined, undefined, Spec.environment, Spec.platformName, aggregateEventTopics, aggregateProducedEvents, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponents, componentRuntime, pluginStructure, opts);
159
+ let dcbResult = DcbBuilder.construct(extra$1, childName, undefined, undefined, Spec.environment, Spec.platformName, aggregateEventTopics, aggregateProducedEvents, Plugin_Helpers$ReventlessCore.publishToAggregates, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponents, componentRuntime, pluginStructure, opts);
160
160
  let mutationEntriesFromAggregates = aggregates.flatMap(M => {
161
161
  let commandSchema = M.Spec.commandSchema;
162
162
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {