@reventlessdev/reventless-aws 3.0.0-alpha.261 → 3.0.0-alpha.263
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 +14 -0
- package/package.json +7 -7
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +27 -16
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +7 -2
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +23 -2
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +9 -2
- package/tests/AutomationSliceEntryPoint_OpsTest.res +12 -2
- package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +15 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.263 (2026-08-03)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** make the slice callback protocol one declaration, not three ([d49e543](https://github.com/ReventlessDev/reventless-core/commit/d49e543f4715c955c75cec8bd691ee0927b7e2f4))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.262 (2026-08-03)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** key the shared automation channel's DCB topic by convention ([fe473d5](https://github.com/ReventlessDev/reventless-core/commit/fe473d561343f39bc1d05a2afaa902459142bbd0))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.261 (2026-08-03)
|
|
7
21
|
|
|
8
22
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.263",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"sury": "11.0.0-alpha.4",
|
|
13
13
|
"uuid": "^13.0.0",
|
|
14
14
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.3",
|
|
15
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
16
15
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
|
-
"@reventlessdev/rescript-
|
|
16
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
18
17
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.65",
|
|
18
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.1",
|
|
19
19
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
20
20
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
21
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
22
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.71",
|
|
23
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.207",
|
|
24
21
|
"@reventlessdev/reventless-infra": "3.0.0-alpha.124",
|
|
25
|
-
"@reventlessdev/reventless-
|
|
22
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.208",
|
|
23
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
24
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.99",
|
|
25
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.72"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"rescript": "12.3.0",
|
|
@@ -129,16 +129,21 @@ let makeSyncTodoItems = (
|
|
|
129
129
|
// The callback shapes the shell's functor applications produce
|
|
130
130
|
// (AutomationSlice_Callback.T / OutboundTranslationSlice_Callback.T), typed at
|
|
131
131
|
// the seam.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
// The entry point builds callbacks dynamically per HANDLER_CONFIG entry, so it
|
|
133
|
+
// cannot name `AutomationSlice_Callback.T` / `OutboundTranslationSlice_Callback.T`
|
|
134
|
+
// — the Spec is not known statically — and has to ascribe a structural type where
|
|
135
|
+
// the value crosses from the dynamically-imported JS module into ReScript.
|
|
136
|
+
//
|
|
137
|
+
// These are aliases of the protocol types each callback module owns, never
|
|
138
|
+
// restatements of them. That distinction is the whole point: a hand-written copy
|
|
139
|
+
// is a second source of truth the compiler cannot reconcile with the first, and
|
|
140
|
+
// it drifts silently. It did — when `phase1` gained its `sourceId` the copy here
|
|
141
|
+
// kept saying `array<'event>`, stayed internally consistent, compiled, and then
|
|
142
|
+
// destructured a bare event as a tuple at runtime, so every outbound slice on AWS
|
|
143
|
+
// died reading `.TAG` of `undefined` on its first record. As aliases, the same
|
|
144
|
+
// change is a compile error here instead.
|
|
145
|
+
type automationCallback = ReventlessCore.AutomationSlice_Callback.runtime
|
|
146
|
+
type outboundCallback<'event> = ReventlessCore.OutboundTranslationSlice_Callback.runtime<'event>
|
|
142
147
|
|
|
143
148
|
// Automation: phase 1 takes the RAW envelope JSONs — the callback routes by
|
|
144
149
|
// `meta.service` and unwraps `{id, meta, event}` itself — plus the context.
|
|
@@ -175,14 +180,20 @@ let makeOutboundJsonEventsHandler = (
|
|
|
175
180
|
stream
|
|
176
181
|
->Stream.mapEffect(json =>
|
|
177
182
|
Effect.sync(() => {
|
|
178
|
-
let
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
let envelope = json->JSON.Decode.object
|
|
184
|
+
let rawEvent = envelope->Option.flatMap(d => d->Dict.get("event"))->Option.getOr(json)
|
|
185
|
+
// The entity the event was published for, mirroring the in-process
|
|
186
|
+
// builder. An Aggregate's event payload does not repeat its own id, so
|
|
187
|
+
// without lifting this out of the envelope `collect` cannot name the
|
|
188
|
+
// subject of the outbound item it creates.
|
|
189
|
+
let sourceId =
|
|
190
|
+
envelope
|
|
191
|
+
->Option.flatMap(d => d->Dict.get("id"))
|
|
192
|
+
->Option.flatMap(JSON.Decode.string)
|
|
193
|
+
->Option.getOr("")
|
|
183
194
|
let (eventType, dataDict) = rawEvent->ReventlessCore.Message.splitMessage
|
|
184
195
|
switch decoder.decode(~eventType, ~data=dataDict) {
|
|
185
|
-
| Some(event) => [event]
|
|
196
|
+
| Some(event) => [(sourceId, event)]
|
|
186
197
|
| None => []
|
|
187
198
|
}
|
|
188
199
|
})
|
|
@@ -98,11 +98,16 @@ function makeAutomationJsonEventsHandler(context, callback, publishJsons, syncTo
|
|
|
98
98
|
function makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJsons, syncTodoItems) {
|
|
99
99
|
let decoder = DcbDecode$Reventless.makeDecoder(consumedEventSchema);
|
|
100
100
|
return stream => Effect.flatMap(Stream.runCollect(Stream$1.flatMap(Stream$1.mapEffect(stream, json => Effect.sync(() => {
|
|
101
|
-
let
|
|
101
|
+
let envelope = Stdlib_JSON.Decode.object(json);
|
|
102
|
+
let rawEvent = Stdlib_Option.getOr(Stdlib_Option.flatMap(envelope, d => d["event"]), json);
|
|
103
|
+
let sourceId = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(envelope, d => d["id"]), Stdlib_JSON.Decode.string), "");
|
|
102
104
|
let match = Message$ReventlessCore.splitMessage(rawEvent);
|
|
103
105
|
let event = decoder.decode(match[0], match[1]);
|
|
104
106
|
if (event !== undefined) {
|
|
105
|
-
return [
|
|
107
|
+
return [[
|
|
108
|
+
sourceId,
|
|
109
|
+
Primitive_option.valFromOption(event)
|
|
110
|
+
]];
|
|
106
111
|
} else {
|
|
107
112
|
return [];
|
|
108
113
|
}
|
|
@@ -292,8 +292,18 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
292
292
|
// into its own event-source mapping, and the entry point routes an
|
|
293
293
|
// incoming record to the handlers registered for its stream, so a slice
|
|
294
294
|
// only ever sees the sources it asked for.
|
|
295
|
+
//
|
|
296
|
+
// Keyed `<plugin>DcbEventLog`, the convention `Dcb_Builder` uses when it
|
|
297
|
+
// puts the log into `allEventTopics`. That matters because a slice naming
|
|
298
|
+
// the log explicitly (every DCB-sourced AutomationSlice does) contributes
|
|
299
|
+
// it back under that key: a different key here would leave the same topic
|
|
300
|
+
// in the dict twice, and since an event-source mapping is named after the
|
|
301
|
+
// *topic resource* rather than the key, both would render as
|
|
302
|
+
// `<plugin>DcbEventLog2AllAutomationSlices` and the deploy would fail on a
|
|
303
|
+
// duplicate URN.
|
|
304
|
+
let dcbTopicKey = dcbResource.name->Option.getOr("") ++ "DcbEventLog"
|
|
295
305
|
let eventTopics: ReventlessCore.EventTopic.allOutputs = Dict.fromArray([
|
|
296
|
-
(
|
|
306
|
+
(dcbTopicKey, dcbOutputs.eventTopic),
|
|
297
307
|
])
|
|
298
308
|
bundledInfos->Dict.forEach(info =>
|
|
299
309
|
info.sourceTopics->Dict.forEachWithKey((topic, key) => eventTopics->Dict.set(key, topic))
|
|
@@ -326,7 +336,18 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
326
336
|
->Array.forEach(topic =>
|
|
327
337
|
topic.resources->Array.forEach(resource => urns->Array.push(resource.urn)->ignore)
|
|
328
338
|
)
|
|
329
|
-
|
|
339
|
+
// Deduped after resolution, because the same stream can be reached two
|
|
340
|
+
// ways — `consumesDcbLog` and an explicit source naming the DCB log.
|
|
341
|
+
// The entry point registers the handler once per URN, so a repeat would
|
|
342
|
+
// run the slice twice for every event: no error, no log, just doubled
|
|
343
|
+
// work and doubled commands.
|
|
344
|
+
urns
|
|
345
|
+
->Pulumi.Output.all
|
|
346
|
+
->Pulumi.Output.apply(resolved =>
|
|
347
|
+
resolved->Array.reduce([], (acc, urn) =>
|
|
348
|
+
acc->Array.includes(urn) ? acc : acc->Array.concat([urn])
|
|
349
|
+
)
|
|
350
|
+
)
|
|
330
351
|
}
|
|
331
352
|
|
|
332
353
|
let dcbQueueUrl = switch dcbQueueUrlRef.contents {
|
|
@@ -161,8 +161,9 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
161
161
|
parent: opts_parent
|
|
162
162
|
};
|
|
163
163
|
let dcbOutputs = Component$ReventlessCore.outputs(dcbEventLog);
|
|
164
|
+
let dcbTopicKey = Stdlib_Option.getOr(dcbResource.__name, "") + "DcbEventLog";
|
|
164
165
|
let eventTopics = Object.fromEntries([[
|
|
165
|
-
|
|
166
|
+
dcbTopicKey,
|
|
166
167
|
dcbOutputs.eventTopic
|
|
167
168
|
]]);
|
|
168
169
|
Stdlib_Dict.forEach(bundledInfos, info => Stdlib_Dict.forEachWithKey(info.sourceTopics, (topic, key) => {
|
|
@@ -181,7 +182,13 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
181
182
|
urns.push(resource.urn);
|
|
182
183
|
});
|
|
183
184
|
});
|
|
184
|
-
return Pulumi.all(urns)
|
|
185
|
+
return Pulumi.all(urns).apply(resolved => Stdlib_Array.reduce(resolved, [], (acc, urn) => {
|
|
186
|
+
if (acc.includes(urn)) {
|
|
187
|
+
return acc;
|
|
188
|
+
} else {
|
|
189
|
+
return acc.concat([urn]);
|
|
190
|
+
}
|
|
191
|
+
}));
|
|
185
192
|
};
|
|
186
193
|
let url = dcbQueueUrlRef.contents;
|
|
187
194
|
let dcbQueueUrl = url !== undefined ? url : Pulumi.output("NOT_AVAILABLE");
|
|
@@ -150,16 +150,26 @@ describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler", () => {
|
|
|
150
150
|
~publishJsons=noopPublish,
|
|
151
151
|
~syncTodoItems=async () => steps->Array.push("sync"),
|
|
152
152
|
)
|
|
153
|
+
// `id` is the envelope's subject — the entity the event was published for.
|
|
154
|
+
// An Aggregate's event payload does not repeat it, so this is the only place
|
|
155
|
+
// `collect` can learn which customer/order an event belongs to.
|
|
153
156
|
let placed = obj([
|
|
157
|
+
("id", str("ord-1")),
|
|
154
158
|
("meta", obj([("service", str("Order"))])),
|
|
155
159
|
("event", obj([("TAG", str("OrderPlaced")), ("orderId", str("ord-1"))])),
|
|
156
160
|
])
|
|
157
161
|
// Not consumed by this slice — dropped without an error.
|
|
158
162
|
let ignored = obj([("event", obj([("TAG", str("OrderShipped")), ("orderId", str("ord-1"))]))])
|
|
159
|
-
// Bare payload-less variant, no envelope.
|
|
163
|
+
// Bare payload-less variant, no envelope and therefore no id.
|
|
160
164
|
let archived = str("OrderArchived")
|
|
161
165
|
await Stream.fromIterable([placed, ignored, archived])->handler->Effect.runPromise
|
|
162
|
-
|
|
166
|
+
// Pairs, not bare events. Asserting the bare shape is what let the AWS entry
|
|
167
|
+
// point ship a phase1 that destructured `undefined` out of a non-tuple: the
|
|
168
|
+
// test agreed with the drifted local type instead of with the callback.
|
|
169
|
+
expect(received.contents)->toEqual([
|
|
170
|
+
("ord-1", OrderPlaced({orderId: "ord-1"})),
|
|
171
|
+
("", OrderArchived),
|
|
172
|
+
])
|
|
163
173
|
expect(steps)->toEqual(["phase1", "sync", "phase2", "sync"])
|
|
164
174
|
})
|
|
165
175
|
})
|
|
@@ -216,6 +216,10 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler
|
|
|
216
216
|
steps.push("sync");
|
|
217
217
|
});
|
|
218
218
|
let placed = Object.fromEntries([
|
|
219
|
+
[
|
|
220
|
+
"id",
|
|
221
|
+
"ord-1"
|
|
222
|
+
],
|
|
219
223
|
[
|
|
220
224
|
"meta",
|
|
221
225
|
Object.fromEntries([[
|
|
@@ -256,11 +260,17 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler
|
|
|
256
260
|
"OrderArchived"
|
|
257
261
|
])));
|
|
258
262
|
globalThis.expect(received.contents).toEqual([
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
[
|
|
264
|
+
"ord-1",
|
|
265
|
+
{
|
|
266
|
+
TAG: "OrderPlaced",
|
|
267
|
+
orderId: "ord-1"
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
[
|
|
271
|
+
"",
|
|
272
|
+
"OrderArchived"
|
|
273
|
+
]
|
|
264
274
|
]);
|
|
265
275
|
globalThis.expect(steps).toEqual([
|
|
266
276
|
"phase1",
|