@reventlessdev/reventless-aws 3.0.0-alpha.263 → 3.0.0-alpha.264

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,13 @@
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.264 (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
+
12
+
6
13
  # 3.0.0-alpha.263 (2026-08-03)
7
14
 
8
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.263",
3
+ "version": "3.0.0-alpha.264",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -16,13 +16,13 @@
16
16
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
17
17
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.65",
18
18
  "@reventlessdev/rescript-node": "2.0.0-alpha.1",
19
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
20
19
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
20
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
21
+ "@reventlessdev/reventless-core": "3.0.0-alpha.209",
21
22
  "@reventlessdev/reventless-infra": "3.0.0-alpha.124",
22
- "@reventlessdev/reventless-core": "3.0.0-alpha.208",
23
- "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
24
23
  "@reventlessdev/reventless-spec": "3.0.0-alpha.99",
25
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.72"
24
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.73",
25
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.30"
26
26
  },
27
27
  "devDependencies": {
28
28
  "rescript": "12.3.0",
package/src/Platform.res CHANGED
@@ -794,12 +794,17 @@ module MakeWithConfig = (
794
794
  let tableResource = outputs.resources->Array.getUnsafe(0)
795
795
  PluginRuntime_Builder.registerDcbTableName(tableResource.name)
796
796
  },
797
- // DCB CommandTopic created hook — extracts SQS queue URL for slice builders.
797
+ // DCB CommandTopic created hook — extracts SQS queue URL for slice builders,
798
+ // and the queue resource so the slice Lambda's role is granted
799
+ // `sqs:SendMessage` on it.
798
800
  onDcbCommandTopicCreated: dcbCommandTopicUnknown => {
799
801
  let commandTopic = dcbCommandTopicUnknown->asDcbCommandTopicComponent
800
802
  let channel = commandTopic->ReventlessCore.CommandTopic_Adapter.channel
801
803
  let channelParts = channel.parts->asSqsChannelParts
802
- AutomationSliceRuntime_Builder_Single.setDcbQueueUrl(channelParts.queue.id)
804
+ AutomationSliceRuntime_Builder_Single.setDcbQueueUrl(
805
+ ~resource=?channel.resources->Array.get(0),
806
+ channelParts.queue.id,
807
+ )
803
808
  },
804
809
  // DCB slices created hook — finalize slice Lambdas.
805
810
  onDcbSlicesCreated: dcbEventLogUnknown => {
@@ -542,7 +542,7 @@ function MakeWithConfig(Config) {
542
542
  let hooks_onDcbCommandTopicCreated = dcbCommandTopicUnknown => {
543
543
  let channel = dcbCommandTopicUnknown.channel;
544
544
  let channelParts = channel.parts;
545
- AutomationSliceRuntime_Builder_Single$ReventlessAws.setDcbQueueUrl(channelParts.queue.id);
545
+ AutomationSliceRuntime_Builder_Single$ReventlessAws.setDcbQueueUrl(undefined, channel.resources[0], channelParts.queue.id);
546
546
  };
547
547
  let hooks_onDcbSlicesCreated = dcbEventLogUnknown => {
548
548
  StateViewSliceRuntime_Builder_Single$ReventlessAws.finishWithDcbEventLog(dcbEventLogUnknown);
@@ -1796,7 +1796,7 @@ function Make($star) {
1796
1796
  let hooks_onDcbCommandTopicCreated = dcbCommandTopicUnknown => {
1797
1797
  let channel = dcbCommandTopicUnknown.channel;
1798
1798
  let channelParts = channel.parts;
1799
- AutomationSliceRuntime_Builder_Single$ReventlessAws.setDcbQueueUrl(channelParts.queue.id);
1799
+ AutomationSliceRuntime_Builder_Single$ReventlessAws.setDcbQueueUrl(undefined, channel.resources[0], channelParts.queue.id);
1800
1800
  };
1801
1801
  let hooks_onDcbSlicesCreated = dcbEventLogUnknown => {
1802
1802
  StateViewSliceRuntime_Builder_Single$ReventlessAws.finishWithDcbEventLog(dcbEventLogUnknown);
@@ -65,10 +65,15 @@ let make: ReventlessCore.CommandTopic_Adapter.channelMaker<
65
65
  )
66
66
 
67
67
  let resolvedQueueOutput = queue->Util_SQS.toResolvedQueueOutput
68
+ let queueResource = queue->Util_SQS.toResource(~tags=tags->Pulumi.Output.fromInput)
69
+
70
+ // Recorded while the queue is still concrete, so a component publishing to
71
+ // this owner from a Lambda can be given the URL and flavor as configuration.
72
+ CommandTopicRegistry.register(~owner, ~queueUrl=queue.id, ~resource=queueResource, ~isFifo=false)
68
73
 
69
74
  {
70
75
  ReventlessCore.CommandTopic_Adapter.parts: {queue: queue},
71
- resources: [queue->Util_SQS.toResource(~tags=tags->Pulumi.Output.fromInput)],
76
+ resources: [queueResource],
72
77
  publishJsons: resolvedQueueOutput->Pulumi.Output.apply(resolvedQueue =>
73
78
  resolvedQueue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS, ...)
74
79
  ),
@@ -12,6 +12,7 @@ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
12
12
  import * as Util_SQS$ReventlessAws from "../../util/Util_SQS.res.mjs";
13
13
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
14
14
  import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
15
+ import * as CommandTopicRegistry$ReventlessAws from "./CommandTopicRegistry.res.mjs";
15
16
  import * as Util_DeadLetterQueue$ReventlessAws from "../../util/Util_DeadLetterQueue.res.mjs";
16
17
  import * as CommandTopicChannel_Helpers$ReventlessAws from "./CommandTopicChannel_Helpers.res.mjs";
17
18
  import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "./CommandTopicChannel_SQS_Runtime.res.mjs";
@@ -48,11 +49,13 @@ function make(name, owner, opts) {
48
49
  sqsManagedSseEnabled: false
49
50
  }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
50
51
  let resolvedQueueOutput = Util_SQS$ReventlessAws.toResolvedQueueOutput(queue);
52
+ let queueResource = Util_SQS$ReventlessAws.toResource(tags, queue);
53
+ CommandTopicRegistry$ReventlessAws.register(owner, queue.id, queueResource, false);
51
54
  return {
52
55
  parts: {
53
56
  queue: queue
54
57
  },
55
- resources: [Util_SQS$ReventlessAws.toResource(tags, queue)],
58
+ resources: [queueResource],
56
59
  publishJsons: resolvedQueueOutput.apply(resolvedQueue => CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS")),
57
60
  publishJsonsStream: resolvedQueueOutput.apply(resolvedQueue => {
58
61
  let publishJsons = CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS");
@@ -44,6 +44,11 @@ let make: ReventlessCore.CommandTopic_Adapter.channelMaker<
44
44
  )
45
45
 
46
46
  let resolvedQueueOutput = queue->Util_SQS.toResolvedQueueOutput
47
+ let queueResource = queue->Util_SQS.toResource(~tags=tags->Pulumi.Output.fromInput)
48
+
49
+ // Async aggregates and StateChangeSlices land here; same capture so a slice
50
+ // targeting one is routed to it rather than to the plugin's DCB topic.
51
+ CommandTopicRegistry.register(~owner, ~queueUrl=queue.id, ~resource=queueResource, ~isFifo=false)
47
52
 
48
53
  {
49
54
  parts: {queue: queue},
@@ -13,6 +13,7 @@ import * as Util_SQS$ReventlessAws from "../../util/Util_SQS.res.mjs";
13
13
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
14
14
  import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
15
15
  import * as Util_SQS_FIFO$ReventlessAws from "../../util/Util_SQS_FIFO.res.mjs";
16
+ import * as CommandTopicRegistry$ReventlessAws from "./CommandTopicRegistry.res.mjs";
16
17
  import * as Util_DeadLetterQueue$ReventlessAws from "../../util/Util_DeadLetterQueue.res.mjs";
17
18
  import * as CommandTopicChannel_SQS$ReventlessAws from "./CommandTopicChannel_SQS.res.mjs";
18
19
  import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "./CommandTopicChannel_SQS_Runtime.res.mjs";
@@ -31,6 +32,8 @@ function make(name, owner, opts) {
31
32
  sqsManagedSseEnabled: false
32
33
  }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
33
34
  let resolvedQueueOutput = Util_SQS$ReventlessAws.toResolvedQueueOutput(queue);
35
+ let queueResource = Util_SQS$ReventlessAws.toResource(tags, queue);
36
+ CommandTopicRegistry$ReventlessAws.register(owner, queue.id, queueResource, false);
34
37
  return {
35
38
  parts: {
36
39
  queue: queue
@@ -33,10 +33,15 @@ let make: ReventlessCore.CommandTopic_Adapter.channelMaker<
33
33
  )
34
34
 
35
35
  let resolvedQueueOutput = queue->Util_SQS.toResolvedQueueOutput
36
+ let queueResource = queue->Util_SQS_FIFO.toResource(~tags=tags->Pulumi.Output.fromInput)
37
+
38
+ // Same capture as the standard variant, and the reason the flag exists: only
39
+ // the creating module knows this queue needs a MessageGroupId.
40
+ CommandTopicRegistry.register(~owner, ~queueUrl=queue.id, ~resource=queueResource, ~isFifo=true)
36
41
 
37
42
  {
38
43
  parts: {queue: queue},
39
- resources: [queue->Util_SQS_FIFO.toResource(~tags=tags->Pulumi.Output.fromInput)],
44
+ resources: [queueResource],
40
45
  publishJsons: resolvedQueueOutput->Pulumi.Output.apply(resolvedQueue =>
41
46
  resolvedQueue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS_FIFO, ...)
42
47
  ),
@@ -13,6 +13,7 @@ import * as Util_SQS$ReventlessAws from "../../util/Util_SQS.res.mjs";
13
13
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
14
14
  import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
15
15
  import * as Util_SQS_FIFO$ReventlessAws from "../../util/Util_SQS_FIFO.res.mjs";
16
+ import * as CommandTopicRegistry$ReventlessAws from "./CommandTopicRegistry.res.mjs";
16
17
  import * as Util_DeadLetterQueue$ReventlessAws from "../../util/Util_DeadLetterQueue.res.mjs";
17
18
  import * as CommandTopicChannel_SQS$ReventlessAws from "./CommandTopicChannel_SQS.res.mjs";
18
19
  import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "./CommandTopicChannel_SQS_Runtime.res.mjs";
@@ -31,11 +32,13 @@ function make(name, owner, opts) {
31
32
  sqsManagedSseEnabled: false
32
33
  }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
33
34
  let resolvedQueueOutput = Util_SQS$ReventlessAws.toResolvedQueueOutput(queue);
35
+ let queueResource = Util_SQS_FIFO$ReventlessAws.toResource(tags, queue);
36
+ CommandTopicRegistry$ReventlessAws.register(owner, queue.id, queueResource, true);
34
37
  return {
35
38
  parts: {
36
39
  queue: queue
37
40
  },
38
- resources: [Util_SQS_FIFO$ReventlessAws.toResource(tags, queue)],
41
+ resources: [queueResource],
39
42
  publishJsons: resolvedQueueOutput.apply(resolvedQueue => CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS_FIFO")),
40
43
  publishJsonsStream: resolvedQueueOutput.apply(resolvedQueue => {
41
44
  let publishJsons = CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS_FIFO");
@@ -45,10 +45,15 @@ let make: ReventlessCore.CommandTopic_Adapter.channelMaker<
45
45
  )
46
46
 
47
47
  let resolvedQueueOutput = queue->Util_SQS.toResolvedQueueOutput
48
+ let queueResource = queue->Util_SQS.toResource(~tags=tags->Pulumi.Output.fromInput)
49
+
50
+ // This is the maker Aggregates use, so it is the one an OutboundTranslationSlice
51
+ // naming an Aggregate in `targetName` needs to find.
52
+ CommandTopicRegistry.register(~owner, ~queueUrl=queue.id, ~resource=queueResource, ~isFifo=false)
48
53
 
49
54
  {
50
55
  ReventlessCore.CommandTopic_Adapter.parts: {queue: queue},
51
- resources: [queue->Util_SQS.toResource(~tags=tags->Pulumi.Output.fromInput)],
56
+ resources: [queueResource],
52
57
  publishJsons: resolvedQueueOutput->Pulumi.Output.apply(resolvedQueue =>
53
58
  resolvedQueue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS, ...)
54
59
  ),
@@ -11,6 +11,7 @@ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
11
11
  import * as Util_SQS$ReventlessAws from "../../util/Util_SQS.res.mjs";
12
12
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
13
13
  import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
14
+ import * as CommandTopicRegistry$ReventlessAws from "./CommandTopicRegistry.res.mjs";
14
15
  import * as Util_DeadLetterQueue$ReventlessAws from "../../util/Util_DeadLetterQueue.res.mjs";
15
16
  import * as CommandTopicChannel_SQS$ReventlessAws from "./CommandTopicChannel_SQS.res.mjs";
16
17
  import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "./CommandTopicChannel_SQS_Runtime.res.mjs";
@@ -28,11 +29,13 @@ function make(name, owner, opts) {
28
29
  sqsManagedSseEnabled: false
29
30
  }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
30
31
  let resolvedQueueOutput = Util_SQS$ReventlessAws.toResolvedQueueOutput(queue);
32
+ let queueResource = Util_SQS$ReventlessAws.toResource(tags, queue);
33
+ CommandTopicRegistry$ReventlessAws.register(owner, queue.id, queueResource, false);
31
34
  return {
32
35
  parts: {
33
36
  queue: queue
34
37
  },
35
- resources: [Util_SQS$ReventlessAws.toResource(tags, queue)],
38
+ resources: [queueResource],
36
39
  publishJsons: resolvedQueueOutput.apply(resolvedQueue => CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS")),
37
40
  publishJsonsStream: resolvedQueueOutput.apply(resolvedQueue => {
38
41
  let publishJsons = CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS");
@@ -0,0 +1,50 @@
1
+ // Where each component's CommandTopic queue lives, recorded as the queue is
2
+ // created.
3
+ //
4
+ // Runtime code cannot be handed a publisher. A slice Lambda is a compiled
5
+ // EntryPoint configured through `HANDLER_CONFIG`, not a serialized closure —
6
+ // that separation is what fixed the cold-start crashes from closures mixing
7
+ // layer and runtime SDK versions — so a `publishJsons` function cannot cross
8
+ // into it. The same choice has to travel as *data*: which queue, and whether it
9
+ // is FIFO.
10
+ //
11
+ // Captured here rather than recovered later, because recovering it does not
12
+ // work. A component's `resources` array reaches callers inside a
13
+ // `Pulumi.Output.t`, and although every `resource` field is typed
14
+ // `Pulumi.Output.t<string>`, Pulumi flattens nested Outputs at resolution — so
15
+ // within the enclosing apply those fields are already plain strings and calling
16
+ // `.apply` on one throws. At creation the queue is still concrete: `.id` is a
17
+ // flat Output, and the creating module's identity already settles the flavor.
18
+ //
19
+ // Keyed by owner *name* — an Aggregate's `Spec.name` — which is the same key an
20
+ // OutboundTranslationSlice names in `targetName`.
21
+
22
+ type entry = {
23
+ /** The queue URL, for a runtime publisher rebuilt from configuration. */
24
+ queueUrl: Pulumi.Output.t<string>,
25
+ /** The queue as a deploy-time resource, so a caller can add it to a channel
26
+ spec and pick up `connectLambda`'s existing `sqs:SendMessage` grant. */
27
+ resource: ReventlessInfra.Adapter.resource,
28
+ /** FIFO queues need a `MessageGroupId` and standard queues reject one, so this
29
+ cannot be assumed by the caller — it is a property of the queue that only
30
+ the module which created it knows. */
31
+ isFifo: bool,
32
+ }
33
+
34
+ let byOwner: dict<entry> = Dict.make()
35
+
36
+ let register = (
37
+ ~owner: option<ReventlessCore.ResourceAttribution.owner>,
38
+ ~queueUrl: Pulumi.Output.t<string>,
39
+ ~resource: ReventlessInfra.Adapter.resource,
40
+ ~isFifo: bool,
41
+ ) =>
42
+ owner->Option.forEach(({name}) =>
43
+ byOwner->Dict.set(name, {queueUrl, resource, isFifo})
44
+ )
45
+
46
+ /** The CommandTopic of the component with this name, if one has been created.
47
+ `None` for a target that is not an Aggregate in this plugin — a DCB
48
+ StateChangeSlice, or a name that matches nothing — where the caller keeps
49
+ the plugin-wide DCB command topic. */
50
+ let get = (name: string) => byOwner->Dict.get(name)
@@ -0,0 +1,26 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+
5
+ let byOwner = {};
6
+
7
+ function register(owner, queueUrl, resource, isFifo) {
8
+ Stdlib_Option.forEach(owner, param => {
9
+ byOwner[param.name] = {
10
+ queueUrl: queueUrl,
11
+ resource: resource,
12
+ isFifo: isFifo
13
+ };
14
+ });
15
+ }
16
+
17
+ function get(name) {
18
+ return byOwner[name];
19
+ }
20
+
21
+ export {
22
+ byOwner,
23
+ register,
24
+ get,
25
+ }
26
+ /* No side effect */
@@ -27,6 +27,10 @@ type handlerEntry = {
27
27
  callbackType: string,
28
28
  queryDbTableName: string,
29
29
  dcbQueueUrl: string,
30
+ /** Whether `dcbQueueUrl` is a FIFO queue. Absent in a config written before
31
+ the flavor travelled with the URL, where `false` — a standard queue — is
32
+ what these command topics actually are. */
33
+ commandQueueIsFifo: option<bool>,
30
34
  sourceUrn: string,
31
35
  context: option<Reventless.AutomationSlice.context>,
32
36
  }
@@ -53,6 +57,7 @@ let decodeEntry = (json: JSON.t): option<handlerEntry> =>
53
57
  callbackType: h->strOf("callbackType")->Option.getOr("automation"),
54
58
  queryDbTableName: h->strOf("queryDbTableName")->Option.getOr(""),
55
59
  dcbQueueUrl: h->strOf("dcbQueueUrl")->Option.getOr(""),
60
+ commandQueueIsFifo: h->Dict.get("commandQueueIsFifo")->Option.flatMap(JSON.Decode.bool),
56
61
  sourceUrn: h->strOf("sourceUrn")->Option.getOr(""),
57
62
  context: h->Dict.get("context")->Option.flatMap(decodeContext),
58
63
  })
@@ -88,9 +93,13 @@ let defaultContext = (sliceName: string): Reventless.AutomationSlice.context =>
88
93
 
89
94
  // ── Shared plumbing ─────────────────────────────────────────────────────────
90
95
 
91
- let makePublishJsons = (queueUrl: string): ReventlessCore.CommandTopic.publishJsons => {
96
+ // The flavor comes from the handler config rather than being assumed. It used to
97
+ // be hardcoded FIFO while the command topics are standard queues, and SQS rejects
98
+ // the MessageGroupId a FIFO publisher attaches — a failure nothing had reached,
99
+ // because the role had no sqs:SendMessage to get that far.
100
+ let makePublishJsons = (queueUrl: string, ~isFifo: bool): ReventlessCore.CommandTopic.publishJsons => {
92
101
  let queue: Util_SQS_Runtime.resolvedQueue = {id: queueUrl, name: queueUrl, arn: ""}
93
- queue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS_FIFO)
102
+ queue->CommandTopicChannel_SQS_Runtime.publishJsons(isFifo ? AWS.SQS_FIFO : AWS.SQS)
94
103
  }
95
104
 
96
105
  // Overwrite-sync the in-memory TODO list into the slice's view table. Rows are
@@ -223,7 +232,7 @@ let makeAutomationRegisteredHandler = (
223
232
  makeAutomationJsonEventsHandler(
224
233
  ~context=entry.context->Option.getOr(defaultContext(sliceName)),
225
234
  ~callback,
226
- ~publishJsons=makePublishJsons(entry.dcbQueueUrl),
235
+ ~publishJsons=makePublishJsons(entry.dcbQueueUrl, ~isFifo=entry.commandQueueIsFifo->Option.getOr(false)),
227
236
  ~syncTodoItems=makeSyncTodoItems(
228
237
  ~queryDbTableName=entry.queryDbTableName,
229
238
  ~todoItems=callback.todoItems,
@@ -244,7 +253,7 @@ let makeOutboundRegisteredHandler = (
244
253
  makeOutboundJsonEventsHandler(
245
254
  ~consumedEventSchema,
246
255
  ~callback,
247
- ~publishJsons=makePublishJsons(entry.dcbQueueUrl),
256
+ ~publishJsons=makePublishJsons(entry.dcbQueueUrl, ~isFifo=entry.commandQueueIsFifo->Option.getOr(false)),
248
257
  ~syncTodoItems=makeSyncTodoItems(
249
258
  ~queryDbTableName=entry.queryDbTableName,
250
259
  ~todoItems=callback.todoItems,
@@ -36,6 +36,7 @@ function decodeEntry(json) {
36
36
  callbackType: Stdlib_Option.getOr(strOf(h, "callbackType"), "automation"),
37
37
  queryDbTableName: Stdlib_Option.getOr(strOf(h, "queryDbTableName"), ""),
38
38
  dcbQueueUrl: Stdlib_Option.getOr(strOf(h, "dcbQueueUrl"), ""),
39
+ commandQueueIsFifo: Stdlib_Option.flatMap(h["commandQueueIsFifo"], Stdlib_JSON.Decode.bool),
39
40
  sourceUrn: Stdlib_Option.getOr(strOf(h, "sourceUrn"), ""),
40
41
  context: Stdlib_Option.flatMap(h["context"], decodeContext)
41
42
  }));
@@ -64,13 +65,13 @@ function defaultContext(sliceName) {
64
65
  };
65
66
  }
66
67
 
67
- function makePublishJsons(queueUrl) {
68
+ function makePublishJsons(queueUrl, isFifo) {
68
69
  let queue = {
69
70
  id: queueUrl,
70
71
  name: queueUrl,
71
72
  arn: ""
72
73
  };
73
- return CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(queue, "SQS_FIFO");
74
+ return CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(queue, isFifo ? "SQS_FIFO" : "SQS");
74
75
  }
75
76
 
76
77
  function makeSyncTodoItems(queryDbTableName, todoItems, rowSchema) {
@@ -121,14 +122,14 @@ function makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJso
121
122
 
122
123
  function makeAutomationRegisteredHandler(entry, sliceName, callback) {
123
124
  return {
124
- handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeAutomationJsonEventsHandler(Stdlib_Option.getOr(entry.context, defaultContext(sliceName)), callback, makePublishJsons(entry.dcbQueueUrl), makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, AutomationSlice_Callback$ReventlessCore.todoRowSchema))),
125
+ handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeAutomationJsonEventsHandler(Stdlib_Option.getOr(entry.context, defaultContext(sliceName)), callback, makePublishJsons(entry.dcbQueueUrl, Stdlib_Option.getOr(entry.commandQueueIsFifo, false)), makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, AutomationSlice_Callback$ReventlessCore.todoRowSchema))),
125
126
  comp: `AutomationSlice(` + sliceName + `)`
126
127
  };
127
128
  }
128
129
 
129
130
  function makeOutboundRegisteredHandler(entry, sliceName, consumedEventSchema, callback) {
130
131
  return {
131
- handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeOutboundJsonEventsHandler(consumedEventSchema, callback, makePublishJsons(entry.dcbQueueUrl), makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchema))),
132
+ handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeOutboundJsonEventsHandler(consumedEventSchema, callback, makePublishJsons(entry.dcbQueueUrl, Stdlib_Option.getOr(entry.commandQueueIsFifo, false)), makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchema))),
132
133
  comp: `OutboundTranslationSlice(` + sliceName + `)`
133
134
  };
134
135
  }
@@ -38,12 +38,44 @@ type sliceInfo = {
38
38
  // slice that declares no sources (the default) and for any that names the log
39
39
  // explicitly, which is why it is not derivable from `sourceTopics` being empty.
40
40
  consumesDcbLog: bool,
41
+ // Which component's CommandTopic this slice publishes its inbound command to.
42
+ // `None` keeps the plugin's DCB queue — right for a slice targeting a DCB
43
+ // StateChangeSlice, and what the shared Lambda used unconditionally before an
44
+ // outbound slice could target an Aggregate, whose commands a different Lambda
45
+ // consumes.
46
+ //
47
+ // Resolved in the finalizer rather than here: an Aggregate's CommandTopic is
48
+ // created *after* the slices that target it, so a lookup at registration time
49
+ // finds nothing. Only the name is knowable this early.
50
+ commandTargetName: option<string>,
41
51
  }
42
52
 
43
53
  let bundledInfos: dict<sliceInfo> = Dict.make()
44
54
 
45
55
  let dcbQueueUrlRef: ref<option<Pulumi.Output.t<string>>> = ref(None)
46
- let setDcbQueueUrl = url => dcbQueueUrlRef := Some(url)
56
+ // The DCB command topic's own flavor, for the slices that publish to it. Set
57
+ // beside the URL rather than assumed: the runtime publisher used to hardcode
58
+ // FIFO, and these queues are standard, which SQS rejects.
59
+ let dcbQueueIsFifo = ref(false)
60
+ // The DCB command topic queues as deploy-time resources, so the finalizer can
61
+ // put them on the channel spec and `connectLambda`'s `sqs:SendMessage` grant
62
+ // covers the slices that publish to the DCB fallback. Accumulated rather than
63
+ // kept singular: the hook fires once per DCB command topic (sync, and async
64
+ // when async StateChangeSlices exist), and granting on both is correct.
65
+ let dcbQueueResources: array<ReventlessInfra.Adapter.resource> = []
66
+ // First call wins for the URL and flavor. The hook fires once per DCB command
67
+ // topic and a plugin with async StateChangeSlices fires it twice; the sync topic
68
+ // is created first and is the one a slice's command belongs on, so letting the
69
+ // async FIFO topic overwrite would send slice commands to a FIFO queue with no
70
+ // MessageGroupId — which SQS rejects. The resources keep accumulating: the grant
71
+ // is wanted on every DCB command topic.
72
+ let setDcbQueueUrl = (~isFifo=false, ~resource=?, url) => {
73
+ if dcbQueueUrlRef.contents->Option.isNone {
74
+ dcbQueueUrlRef := Some(url)
75
+ dcbQueueIsFifo := isFifo
76
+ }
77
+ resource->Option.forEach(r => dcbQueueResources->Array.push(r)->ignore)
78
+ }
47
79
  // The admin/plugin DCB command-topic FIFO URL captured by the
48
80
  // onDcbCommandTopicCreated hook. Read by the admin EventCollector's reactive
49
81
  // ApiFragmentRegistry push (2e) to dispatch RecordApiFragmentPush.
@@ -62,6 +94,7 @@ let registerAutomationSlice = (
62
94
  ~context=?,
63
95
  ~sourceTopics=Dict.make(),
64
96
  ~consumesDcbLog=true,
97
+ ~commandTargetName=?,
65
98
  ) =>
66
99
  bundledInfos->Dict.set(
67
100
  name,
@@ -74,6 +107,7 @@ let registerAutomationSlice = (
74
107
  context,
75
108
  sourceTopics,
76
109
  consumesDcbLog,
110
+ commandTargetName,
77
111
  },
78
112
  )
79
113
 
@@ -358,9 +392,25 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
358
392
  let handlerOutputs: array<Pulumi.Output.t<string>> = []
359
393
  let packageDirs: dict<string> = Dict.make()
360
394
  let allQueryDbResources: array<ReventlessInfra.Adapter.resource> = []
395
+ // Whether any slice publishes to the DCB fallback rather than a target
396
+ // aggregate's own topic — if so, that queue needs the SendMessage grant
397
+ // too, once, below the loop.
398
+ let anyDcbFallback = ref(false)
361
399
 
362
400
  bundledInfos->Dict.forEachWithKey((info, _name) => {
363
401
  info.queryDbResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
402
+ // The queue this slice publishes its command to. An Aggregate registers
403
+ // its CommandTopic while plugin construction is still synchronous —
404
+ // before DCB construction registers the slices targeting it — so a
405
+ // named target resolves here; anything else keeps the DCB fallback.
406
+ let target = info.commandTargetName->Option.flatMap(CommandTopicRegistry.get)
407
+ switch target {
408
+ | Some({resource}) =>
409
+ // The target CommandTopic joins the channel's resources so
410
+ // `connectLambda`'s existing `sqs:SendMessage` grant covers it.
411
+ allQueryDbResources->Array.push(resource)->ignore
412
+ | None => anyDcbFallback := true
413
+ }
364
414
  let pkg = Util_Bundle.extractPackageName(info.specModulePath)
365
415
  packageDirs->Dict.set(pkg, Util_Bundle.resolvePackageRoot(pkg))
366
416
  // Usually the same package as the spec, but bundle it explicitly —
@@ -380,8 +430,16 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
380
430
  | None => ""
381
431
  }
382
432
 
433
+ // Per slice, not plugin-wide: an outbound slice targeting an Aggregate
434
+ // publishes to that aggregate's CommandTopic. Everything else keeps the
435
+ // DCB topic, which is what the field has always meant — and with it the
436
+ // DCB queue's own flavor.
437
+ let (commandQueueUrl, commandQueueIsFifo) = switch target {
438
+ | Some({queueUrl, isFifo}) => (queueUrl, isFifo)
439
+ | None => (dcbQueueUrl, dcbQueueIsFifo.contents)
440
+ }
383
441
  let handlerJson =
384
- Pulumi.Output.all3((info.queryDbTableName, dcbQueueUrl, sourceUrnsFor(info)))
442
+ Pulumi.Output.all3((info.queryDbTableName, commandQueueUrl, sourceUrnsFor(info)))
385
443
  ->Pulumi.Output.apply(((tableName, queueUrl, urns)) => {
386
444
  let urnsJson =
387
445
  urns->Array.map(JSON.Encode.string)->JSON.Encode.array->JSON.stringify
@@ -389,11 +447,20 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
389
447
  // an entry point that predates multi-source still routes its first
390
448
  // stream rather than none.
391
449
  let firstUrn = urns->Array.get(0)->Option.getOr("")
392
- `{"specModule":${specModule},"bodyModule":${bodyModule},"callbackType":${callbackType},"queryDbTableName":"${tableName}","dcbQueueUrl":"${queueUrl}","sourceUrn":"${firstUrn}","sourceUrns":${urnsJson}${contextFragment}}`
450
+ let fifoJson = commandQueueIsFifo ? "true" : "false"
451
+ `{"specModule":${specModule},"bodyModule":${bodyModule},"callbackType":${callbackType},"queryDbTableName":"${tableName}","dcbQueueUrl":"${queueUrl}","commandQueueIsFifo":${fifoJson},"sourceUrn":"${firstUrn}","sourceUrns":${urnsJson}${contextFragment}}`
393
452
  })
394
453
  let _ = handlerOutputs->Array.push(handlerJson)
395
454
  })
396
455
 
456
+ // The fallback publishers' grant. Every slice that resolved no target
457
+ // publishes to the plugin's DCB command topic, whose queue was captured
458
+ // by `setDcbQueueUrl` — without this the role has no `sqs:SendMessage`
459
+ // there and every such publish is rejected.
460
+ if anyDcbFallback.contents {
461
+ dcbQueueResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
462
+ }
463
+
397
464
  let handlerConfigOutput =
398
465
  Pulumi.Output.all(handlerOutputs)
399
466
  ->Pulumi.Output.apply(handlers => `{"handlers":[${handlers->Array.join(",")}]}`)
@@ -8,6 +8,7 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
8
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
9
9
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
10
10
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
11
+ import * as CommandTopicRegistry$ReventlessAws from "../CommandTopic/CommandTopicRegistry.res.mjs";
11
12
  import * as PluginRuntime_Builder$ReventlessAws from "../../plugin/runtime/PluginRuntime_Builder.res.mjs";
12
13
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
13
14
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
@@ -20,15 +21,28 @@ let dcbQueueUrlRef = {
20
21
  contents: undefined
21
22
  };
22
23
 
23
- function setDcbQueueUrl(url) {
24
- dcbQueueUrlRef.contents = url;
24
+ let dcbQueueIsFifo = {
25
+ contents: false
26
+ };
27
+
28
+ let dcbQueueResources = [];
29
+
30
+ function setDcbQueueUrl(isFifoOpt, resource, url) {
31
+ let isFifo = isFifoOpt !== undefined ? isFifoOpt : false;
32
+ if (Stdlib_Option.isNone(dcbQueueUrlRef.contents)) {
33
+ dcbQueueUrlRef.contents = url;
34
+ dcbQueueIsFifo.contents = isFifo;
35
+ }
36
+ Stdlib_Option.forEach(resource, r => {
37
+ dcbQueueResources.push(r);
38
+ });
25
39
  }
26
40
 
27
41
  function getDcbQueueUrl() {
28
42
  return dcbQueueUrlRef.contents;
29
43
  }
30
44
 
31
- function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt) {
45
+ function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt, commandTargetName) {
32
46
  let callbackType = callbackTypeOpt !== undefined ? callbackTypeOpt : "automation";
33
47
  let queryDbResources = queryDbResourcesOpt !== undefined ? queryDbResourcesOpt : [];
34
48
  let sourceTopics = sourceTopicsOpt !== undefined ? sourceTopicsOpt : ({});
@@ -41,7 +55,8 @@ function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackT
41
55
  queryDbResources: queryDbResources,
42
56
  context: context,
43
57
  sourceTopics: sourceTopics,
44
- consumesDcbLog: consumesDcbLog
58
+ consumesDcbLog: consumesDcbLog,
59
+ commandTargetName: commandTargetName
45
60
  };
46
61
  }
47
62
 
@@ -195,10 +210,19 @@ function finishWithDcbEventLog(dcbEventLog) {
195
210
  let handlerOutputs = [];
196
211
  let packageDirs = {};
197
212
  let allQueryDbResources = [];
213
+ let anyDcbFallback = {
214
+ contents: false
215
+ };
198
216
  Stdlib_Dict.forEachWithKey(bundledInfos, (info, _name) => {
199
217
  info.queryDbResources.forEach(r => {
200
218
  allQueryDbResources.push(r);
201
219
  });
220
+ let target = Stdlib_Option.flatMap(info.commandTargetName, CommandTopicRegistry$ReventlessAws.get);
221
+ if (target !== undefined) {
222
+ allQueryDbResources.push(target.resource);
223
+ } else {
224
+ anyDcbFallback.contents = true;
225
+ }
202
226
  let pkg = Util_Bundle$ReventlessAws.extractPackageName(info.specModulePath);
203
227
  packageDirs[pkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, pkg);
204
228
  let bodyPkg = Util_Bundle$ReventlessAws.extractPackageName(info.bodyModulePath);
@@ -208,18 +232,32 @@ function finishWithDcbEventLog(dcbEventLog) {
208
232
  let callbackType = Stdlib_Option.getOr(JSON.stringify(info.callbackType), `""`);
209
233
  let context = info.context;
210
234
  let contextFragment = context !== undefined ? Stdlib_Option.getOr(Stdlib_Option.map(JSON.stringify(context), json => `,"context":` + json), "") : "";
235
+ let match = target !== undefined ? [
236
+ target.queueUrl,
237
+ target.isFifo
238
+ ] : [
239
+ dcbQueueUrl,
240
+ dcbQueueIsFifo.contents
241
+ ];
242
+ let commandQueueIsFifo = match[1];
211
243
  let handlerJson = Pulumi.all([
212
244
  info.queryDbTableName,
213
- dcbQueueUrl,
245
+ match[0],
214
246
  sourceUrnsFor(info)
215
247
  ]).apply(param => {
216
248
  let urns = param[2];
217
249
  let urnsJson = JSON.stringify(urns.map(prim => prim));
218
250
  let firstUrn = Stdlib_Option.getOr(urns[0], "");
219
- return `{"specModule":` + specModule + `,"bodyModule":` + bodyModule + `,"callbackType":` + callbackType + `,"queryDbTableName":"` + param[0] + `","dcbQueueUrl":"` + param[1] + `","sourceUrn":"` + firstUrn + `","sourceUrns":` + urnsJson + contextFragment + `}`;
251
+ let fifoJson = commandQueueIsFifo ? "true" : "false";
252
+ return `{"specModule":` + specModule + `,"bodyModule":` + bodyModule + `,"callbackType":` + callbackType + `,"queryDbTableName":"` + param[0] + `","dcbQueueUrl":"` + param[1] + `","commandQueueIsFifo":` + fifoJson + `,"sourceUrn":"` + firstUrn + `","sourceUrns":` + urnsJson + contextFragment + `}`;
220
253
  });
221
254
  handlerOutputs.push(handlerJson);
222
255
  });
256
+ if (anyDcbFallback.contents) {
257
+ dcbQueueResources.forEach(r => {
258
+ allQueryDbResources.push(r);
259
+ });
260
+ }
223
261
  let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
224
262
  let envVars = {};
225
263
  envVars["HANDLER_CONFIG"] = handlerConfigOutput;
@@ -250,6 +288,8 @@ export {
250
288
  log,
251
289
  bundledInfos,
252
290
  dcbQueueUrlRef,
291
+ dcbQueueIsFifo,
292
+ dcbQueueResources,
253
293
  setDcbQueueUrl,
254
294
  getDcbQueueUrl,
255
295
  registerAutomationSlice,
@@ -46,7 +46,7 @@ function Make(Api) {
46
46
  let queryDbTableName = tableResource.name;
47
47
  let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(sourceNames));
48
48
  let consumesDcbLog = sourceNames.length === 0 || sourceNames.some(name => !(name in allEventTopics));
49
- AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Automation.moduleUrl), "automation", queryDbTableName, queryDbOutputs.resources, context, sourceTopics, consumesDcbLog);
49
+ AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Automation.moduleUrl), "automation", queryDbTableName, queryDbOutputs.resources, context, sourceTopics, consumesDcbLog, undefined);
50
50
  return as_;
51
51
  };
52
52
  return {
@@ -65,9 +65,14 @@ module Make = (Api: {
65
65
  ~bodyModulePath=Util_Bundle.getModuleSpecifier(Translation.moduleUrl),
66
66
  ~callbackType="outbound",
67
67
  ~queryDbTableName,
68
+ // The target queue rides along so `connectLambda`'s existing
69
+ // `sqs:SendMessage` grant covers it. Without it the role can publish
70
+ // nowhere, and the command is lost after the geocoder has been paid for.
68
71
  ~queryDbResources=queryDbOutputs.resources,
69
72
  ~sourceTopics,
70
73
  ~consumesDcbLog,
74
+ // Only the name: the target Aggregate's CommandTopic does not exist yet.
75
+ ~commandTargetName=?Spec.targetName,
71
76
  )
72
77
 
73
78
  ots
@@ -54,7 +54,7 @@ function Make(Api) {
54
54
  let declaredSources = Spec.sourceNames;
55
55
  let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(declaredSources));
56
56
  let consumesDcbLog = declaredSources.length === 0 || declaredSources.some(name => !(name in allEventTopics));
57
- AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl), "outbound", queryDbTableName, queryDbOutputs.resources, undefined, sourceTopics, consumesDcbLog);
57
+ AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl), "outbound", queryDbTableName, queryDbOutputs.resources, undefined, sourceTopics, consumesDcbLog, Spec.targetName);
58
58
  return ots;
59
59
  };
60
60
  return {