@reventlessdev/reventless-aws 3.0.0-alpha.232 → 3.0.0-alpha.233

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 (104) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +8 -8
  3. package/src/adapter/Api/AppSyncEventsSigner_Ops.res +137 -0
  4. package/src/adapter/Api/AppSyncEventsSigner_Ops.res.mjs +121 -0
  5. package/src/adapter/Api/Platform_AdminScan_Ops.res +136 -0
  6. package/src/adapter/Api/Platform_AdminScan_Ops.res.mjs +98 -0
  7. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +12 -90
  8. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +11 -84
  9. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +86 -0
  10. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +79 -0
  11. package/src/adapter/Api/Platform_UIFragments_Lambda.res +9 -79
  12. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +7 -143
  13. package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res +48 -0
  14. package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res.mjs +63 -0
  15. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +21 -80
  16. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +23 -68
  17. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res +70 -0
  18. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res.mjs +68 -0
  19. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -1
  20. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +1 -1
  21. package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
  22. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -1
  23. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +42 -126
  24. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +246 -0
  25. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +145 -0
  26. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +27 -2
  27. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +10 -3
  28. package/src/adapter/Runtime/CounterEntryPoint.mjs +19 -73
  29. package/src/adapter/Runtime/CounterEntryPoint_Ops.res +145 -0
  30. package/src/adapter/Runtime/CounterEntryPoint_Ops.res.mjs +129 -0
  31. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +11 -1
  32. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +9 -3
  33. package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +52 -510
  34. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +816 -0
  35. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +651 -0
  36. package/src/adapter/Runtime/EventMapperEntryPoint.mjs +23 -67
  37. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +126 -0
  38. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res.mjs +83 -0
  39. package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +16 -63
  40. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +150 -0
  41. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res.mjs +111 -0
  42. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res +96 -0
  43. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res.mjs +70 -0
  44. package/src/adapter/Runtime/PgMigrationEntryPoint.res +54 -0
  45. package/src/adapter/Runtime/PgMigrationEntryPoint.res.mjs +37 -0
  46. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +20 -55
  47. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +155 -0
  48. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +94 -0
  49. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +80 -0
  50. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +39 -0
  51. package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +4 -4
  52. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +34 -153
  53. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +114 -0
  54. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res.mjs +83 -0
  55. package/src/adapter/Runtime/SideEffectEntryPoint.mjs +23 -64
  56. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res +71 -0
  57. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res.mjs +57 -0
  58. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +34 -165
  59. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +206 -0
  60. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs +105 -0
  61. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +156 -0
  62. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res.mjs +123 -0
  63. package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +18 -138
  64. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +198 -0
  65. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res.mjs +155 -0
  66. package/src/adapter/StateTopic/StateTopic_AppSync.res +12 -170
  67. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +7 -167
  68. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +208 -0
  69. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +183 -0
  70. package/src/components/AutomationSlice_Builder.res +2 -0
  71. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  72. package/src/components/OutboundTranslationSlice_Builder.res +1 -0
  73. package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
  74. package/tests/AppSyncEventsSigner_OpsTest.res +75 -0
  75. package/tests/AppSyncEventsSigner_OpsTest.res.mjs +44 -0
  76. package/tests/AppSyncEventsSigner_ParityFixture.mjs +36 -0
  77. package/tests/AutomationSliceEntryPoint_OpsTest.res +165 -0
  78. package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +280 -0
  79. package/tests/CounterEntryPoint_OpsTest.res +115 -0
  80. package/tests/CounterEntryPoint_OpsTest.res.mjs +150 -0
  81. package/tests/EventCollectorEntryPoint_OpsTest.res +216 -0
  82. package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +339 -0
  83. package/tests/EventMapperEntryPoint_OpsTest.res +103 -0
  84. package/tests/EventMapperEntryPoint_OpsTest.res.mjs +120 -0
  85. package/tests/ExtensionPointEntryPoint_OpsTest.res +57 -0
  86. package/tests/ExtensionPointEntryPoint_OpsTest.res.mjs +54 -0
  87. package/tests/PgMigrationEntryPoint_IntegrationTest.res +22 -31
  88. package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +20 -23
  89. package/tests/PgQueryResolverEntryPoint_OpsTest.res +38 -0
  90. package/tests/PgQueryResolverEntryPoint_OpsTest.res.mjs +31 -0
  91. package/tests/Platform_AdminScan_OpsTest.res +80 -0
  92. package/tests/Platform_AdminScan_OpsTest.res.mjs +127 -0
  93. package/tests/ReadModelEntryPoint_OpsTest.res +141 -0
  94. package/tests/ReadModelEntryPoint_OpsTest.res.mjs +267 -0
  95. package/tests/SideEffectEntryPoint_OpsTest.res +79 -0
  96. package/tests/SideEffectEntryPoint_OpsTest.res.mjs +105 -0
  97. package/tests/StateViewSliceEntryPoint_OpsTest.res +167 -0
  98. package/tests/StateViewSliceEntryPoint_OpsTest.res.mjs +296 -0
  99. package/tests/TaskBucketEntryPoint_OpsTest.res +117 -0
  100. package/tests/TaskBucketEntryPoint_OpsTest.res.mjs +165 -0
  101. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res +42 -13
  102. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res.mjs +29 -6
  103. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +0 -57
  104. package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +0 -40
@@ -1,138 +1,54 @@
1
- // AutomationSlice Lambda entry point.
2
- // At cold start: reads HANDLER_CONFIG, dynamically imports Spec modules,
3
- // supports both AutomationSlice and OutboundTranslationSlice callbacks.
4
- // Decodes events, runs phase1/phase2, then syncs to QueryDb.
1
+ // AutomationSlice / OutboundTranslationSlice Lambda entry point.
2
+ // At cold start: reads HANDLER_CONFIG, dynamically imports each slice's spec +
3
+ // body modules, applies the curried callback functor, and routes stream events
4
+ // by source URN.
5
+ //
6
+ // Thin untyped shell ("typed core, thin shell" —
7
+ // docs/plans/minimize-lambda-entrypoint-mjs-shell.md): this file owns ONLY the
8
+ // inherently-untyped seam — the dynamic `import()` of the spec + body modules
9
+ // named in HANDLER_CONFIG and the curried functor applications consuming them
10
+ // (AutomationSlice_Callback.Make(Spec)(Automation) /
11
+ // OutboundTranslationSlice_Callback.Make(Spec)(Translation)). Everything else
12
+ // (HANDLER_CONFIG parsing, the phase-1/phase-2 pipelines, the TODO-list
13
+ // QueryDb sync, the routed dispatch boundary) lives type-checked in
14
+ // AutomationSliceEntryPoint_Ops.res / StreamRoutedEntryPoint_Ops.res.
5
15
 
6
- import * as Effect from "effect/Effect";
7
- import * as Stream from "effect/Stream";
8
- import * as Chunk from "effect/Chunk";
9
- import { parseJsonOrThrow } from "sury/src/S.res.mjs";
10
- import {
11
- makeTableRef,
12
- makeQueueRef,
13
- log,
14
- runEffect,
15
- setRequestId,
16
- extractMetaField,
17
- extractSentTimestamp,
18
- extractRetryCount,
19
- } from "./HandlerFactoryHelpers.mjs";
20
- import { save as qdbSave } from "@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs";
21
- import { publishJsons as sqsPublishJsons } from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
22
16
  import { Make as automationSliceCallbackMake } from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
23
- import { handleStreamEvent } from "@reventlessdev/reventless-aws/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs";
24
17
  import { Make as outboundTranslationSliceCallbackMake } from "@reventlessdev/reventless-core/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs";
18
+ import * as StreamOps from "./StreamRoutedEntryPoint_Ops.res.mjs";
19
+ import * as Ops from "./AutomationSliceEntryPoint_Ops.res.mjs";
25
20
 
26
21
  const dynamicImport = (specifier) => import('/var/task/node_modules/' + specifier);
27
22
 
28
- // Same comp the slice's own callback logs under (AutomationSlice_Callback /
29
- // OutboundTranslationSlice_Callback), so a filter catches both the framework's
30
- // lines and the application handler's.
31
- const sliceComp = (callbackType, sliceName) =>
32
- callbackType === "outbound"
33
- ? `OutboundTranslationSlice(${sliceName})`
34
- : `AutomationSlice(${sliceName})`;
35
-
36
- function groupBySource(records) {
37
- const dict = {};
38
- records.forEach(record => {
39
- const arn = record.eventSourceARN;
40
- const existing = dict[arn] || [];
41
- dict[arn] = existing.concat([record]);
42
- });
43
- return dict;
44
- }
45
-
46
- function buildHandler(specModule, callbackMake, queryDbTableName, dcbQueueUrl) {
47
- const callback = callbackMake(specModule);
48
- const publishJsons = sqsPublishJsons(makeQueueRef(dcbQueueUrl), "SQS_FIFO");
49
- const table = makeTableRef(queryDbTableName);
50
- const rawSave = qdbSave(table);
51
-
52
- const syncToQueryDb = async () => {
53
- const items = Object.entries(callback.todoItems.contents);
54
- await Promise.all(items.map(async entry => await rawSave(entry[0], entry[1], "Overwrite", undefined)));
55
- };
56
-
57
- const eventSchema = specModule.consumedEventSchema;
58
-
59
- const jsonEventsHandler = stream => Effect.flatMap(
60
- Stream.runCollect(
61
- Stream.flatMap(
62
- Stream.mapEffect(stream, json => Effect.sync(() => {
63
- try {
64
- const eventJson = json.event != null ? json.event : json;
65
- return [parseJsonOrThrow(eventJson, eventSchema)];
66
- } catch (exn) {
67
- log.error("failed to decode event", { comp: "AutomationSliceRuntime", detail: exn && exn.message ? exn.message : String(exn) });
68
- return [];
69
- }
70
- })),
71
- events => Stream.fromIterable(events)
72
- )
73
- ),
74
- chunk => Effect.promise(async () => {
75
- const eventsArr = Chunk.toReadonlyArray(chunk);
76
- callback.phase1(eventsArr);
77
- await callback.phase2(publishJsons);
78
- return await syncToQueryDb();
79
- })
80
- );
81
-
82
- return (event, context) => handleStreamEvent(jsonEventsHandler, event, context);
83
- }
84
-
85
23
  async function buildAllHandlers() {
86
- const configStr = process.env["HANDLER_CONFIG"] || '{"handlers":[]}';
87
- const config = JSON.parse(configStr);
88
24
  const handlers = {};
89
- await Promise.all(config.handlers.map(async h => {
90
- const specModule = await dynamicImport(h.specModule);
91
- const callbackMake = h.callbackType === "outbound"
92
- ? prim => outboundTranslationSliceCallbackMake(prim)
93
- : prim => automationSliceCallbackMake(prim);
94
- const handler = buildHandler(specModule, callbackMake, h.queryDbTableName, h.dcbQueueUrl);
95
- // Multiple slices can share one source stream (e.g. several automation
96
- // slices reacting to the same DcbEventLog). Accumulate all handlers per
97
- // source ARN; a plain assignment collapses them to one (Promise.all race),
98
- // silently dropping the rest. Each keeps its own comp — that is what
99
- // separates co-hosted slices in the log stream.
100
- (handlers[h.sourceUrn] ||= []).push({
101
- handler,
102
- comp: sliceComp(h.callbackType, specModule.name),
103
- });
104
- }));
105
- return handlers;
106
- }
107
-
108
- const initPromise = buildAllHandlers();
25
+ const entries = Ops.parseHandlerConfig(process.env["HANDLER_CONFIG"] || "");
109
26
 
110
- export async function handler(event, context) {
111
- setRequestId(context?.awsRequestId);
112
- const handlers = await initPromise;
113
- const records = event.Records || [];
114
- const grouped = groupBySource(records);
115
-
116
- await Promise.all(Object.entries(grouped).map(async ([arn, subRecords]) => {
117
- const streamHandlers = handlers[arn];
118
- if (streamHandlers !== undefined && streamHandlers.length > 0) {
119
- log.debug("found " + streamHandlers.length + " handler(s) for " + arn, { comp: "AutomationSliceRuntime" });
120
- const correlationId = extractMetaField(subRecords, "correlationId");
121
- const causationId = extractMetaField(subRecords, "causationId");
122
- const timestamp = extractSentTimestamp(subRecords);
123
- const retryCount = extractRetryCount(subRecords);
124
- await Promise.all(streamHandlers.map(({ handler: h, comp }) =>
125
- runEffect(h({ Records: subRecords }, context), {
126
- correlationId,
127
- causationId,
128
- comp,
129
- timestamp,
130
- retryCount,
131
- })
132
- ));
133
- } else {
134
- log.warn("no handler found: " + arn, { comp: "AutomationSliceRuntime" });
27
+ await Promise.all(entries.map(async (entry) => {
28
+ if (!entry.bodyModule) {
29
+ Ops.warnMissingBodyModule(entry);
30
+ return;
135
31
  }
32
+ const [specModule, bodyModule] = await Promise.all([
33
+ dynamicImport(entry.specModule),
34
+ dynamicImport(entry.bodyModule),
35
+ ]);
36
+ const registered = entry.callbackType === "outbound"
37
+ ? Ops.makeOutboundRegisteredHandler(
38
+ entry,
39
+ specModule.name,
40
+ specModule.consumedEventSchema,
41
+ outboundTranslationSliceCallbackMake(specModule)(bodyModule)
42
+ )
43
+ : Ops.makeAutomationRegisteredHandler(
44
+ entry,
45
+ specModule.name,
46
+ automationSliceCallbackMake(specModule)(bodyModule)
47
+ );
48
+ StreamOps.addToRegistry(handlers, entry.sourceUrn, registered);
136
49
  }));
137
- return "";
50
+
51
+ return handlers;
138
52
  }
53
+
54
+ export const handler = StreamOps.makeRoutedHandler("AutomationSliceRuntime", buildAllHandlers());
@@ -0,0 +1,246 @@
1
+ // Typed cold-start core for the AutomationSlice / OutboundTranslationSlice
2
+ // Lambda entry point.
3
+ //
4
+ // The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md):
5
+ // AutomationSliceEntryPoint.mjs keeps only the inherently-untyped seam — the
6
+ // dynamic `import()` of the spec + body modules named in HANDLER_CONFIG and
7
+ // the curried callback-functor applications consuming them
8
+ // (AutomationSlice_Callback.Make(Spec)(Automation) /
9
+ // OutboundTranslationSlice_Callback.Make(Spec)(Translation)). HANDLER_CONFIG
10
+ // parsing, the two phase-1/phase-2 pipelines, the TODO-list QueryDb sync, and
11
+ // handler registration live here, fully type-checked; the routed dispatch
12
+ // boundary is shared with the ReadModel / StateViewSlice entry points in
13
+ // StreamRoutedEntryPoint_Ops.
14
+ //
15
+ // This conversion also REPAIRS the wiring: the former `.mjs` predated the
16
+ // mixed-source callback rework — it applied the (now curried) functor to the
17
+ // spec module alone, read `todoItems.contents` (no longer a ref), and passed
18
+ // schema-decoded events to `phase1` (the automation callback now takes raw
19
+ // envelope JSONs plus a context). Any event through that Lambda threw at
20
+ // `callback.phase1`. The `bodyModule` and `context` HANDLER_CONFIG fields
21
+ // exist for this wiring (AutomationSliceRuntime_Builder_Single).
22
+
23
+ @val @scope("process") external processEnv: dict<string> = "env"
24
+
25
+ type handlerEntry = {
26
+ specModule: string,
27
+ bodyModule: string,
28
+ callbackType: string,
29
+ queryDbTableName: string,
30
+ dcbQueueUrl: string,
31
+ sourceUrn: string,
32
+ context: option<Reventless.AutomationSlice.context>,
33
+ }
34
+
35
+ let strOf = (obj: dict<JSON.t>, key: string): option<string> =>
36
+ obj->Dict.get(key)->Option.flatMap(JSON.Decode.string)
37
+
38
+ let decodeContext = (json: JSON.t): option<Reventless.AutomationSlice.context> =>
39
+ json
40
+ ->JSON.Decode.object
41
+ ->Option.map(c => {
42
+ Reventless.AutomationSlice.environment: c->strOf("environment")->Option.getOr("unknown"),
43
+ platformName: c->strOf("platformName")->Option.getOr("unknown"),
44
+ pluginName: c->strOf("pluginName")->Option.getOr("unknown"),
45
+ sliceName: c->strOf("sliceName")->Option.getOr("unknown"),
46
+ })
47
+
48
+ let decodeEntry = (json: JSON.t): option<handlerEntry> =>
49
+ json
50
+ ->JSON.Decode.object
51
+ ->Option.map(h => {
52
+ specModule: h->strOf("specModule")->Option.getOr(""),
53
+ bodyModule: h->strOf("bodyModule")->Option.getOr(""),
54
+ callbackType: h->strOf("callbackType")->Option.getOr("automation"),
55
+ queryDbTableName: h->strOf("queryDbTableName")->Option.getOr(""),
56
+ dcbQueueUrl: h->strOf("dcbQueueUrl")->Option.getOr(""),
57
+ sourceUrn: h->strOf("sourceUrn")->Option.getOr(""),
58
+ context: h->Dict.get("context")->Option.flatMap(decodeContext),
59
+ })
60
+
61
+ let parseHandlerConfig = (rawJson: string): array<handlerEntry> =>
62
+ rawJson == ""
63
+ ? []
64
+ : rawJson
65
+ ->JSON.parseOrThrow
66
+ ->JSON.Decode.object
67
+ ->Option.flatMap(obj => obj->Dict.get("handlers"))
68
+ ->Option.flatMap(JSON.Decode.array)
69
+ ->Option.getOr([])
70
+ ->Array.filterMap(decodeEntry)
71
+
72
+ // A pre-`bodyModule` config entry cannot rebuild the callback (the spec module
73
+ // alone lacks the mappings/process). Warn and skip rather than crash the whole
74
+ // Lambda — co-hosted slices with complete entries still run.
75
+ let warnMissingBodyModule = (entry: handlerEntry) =>
76
+ StreamRoutedEntryPoint_Ops.logWarn(
77
+ `handler entry for spec '${entry.specModule}' carries no bodyModule (stale HANDLER_CONFIG?); skipping`,
78
+ {comp: "AutomationSliceRuntime"},
79
+ )
80
+
81
+ // Fallback for automation entries from a config without `context` — sliceName
82
+ // is the only field the runtime can still recover.
83
+ let defaultContext = (sliceName: string): Reventless.AutomationSlice.context => {
84
+ environment: processEnv->Dict.get("Environment")->Option.getOr("unknown"),
85
+ platformName: "unknown",
86
+ pluginName: "unknown",
87
+ sliceName,
88
+ }
89
+
90
+ // ── Shared plumbing ─────────────────────────────────────────────────────────
91
+
92
+ let makePublishJsons = (queueUrl: string): ReventlessCore.CommandTopic.publishJsons => {
93
+ let queue: Util_SQS_Runtime.resolvedQueue = {id: queueUrl, name: queueUrl, arn: ""}
94
+ queue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS_FIFO)
95
+ }
96
+
97
+ // Overwrite-sync the in-memory TODO list into the slice's view table. Rows are
98
+ // serialized through the callback's row schema — the same JSON shape the
99
+ // deploy-time QueryDb writes.
100
+ let makeSyncTodoItems = (
101
+ ~queryDbTableName: string,
102
+ ~todoItems: dict<'row>,
103
+ ~rowSchema: S.t<'row>,
104
+ ): (unit => promise<unit>) => {
105
+ let table: Util_DynamoDb_Runtime.resolvedTable = {
106
+ id: "",
107
+ name: queryDbTableName,
108
+ arn: "",
109
+ hashKey: "id",
110
+ }
111
+ let save = QueryDbStorage_DynamoDb_Runtime.save(table)
112
+ async () => {
113
+ let _ =
114
+ await todoItems
115
+ ->Dict.toArray
116
+ ->Array.map(((id, row)) =>
117
+ save(id, row->S.reverseConvertToJsonOrThrow(rowSchema), ReventlessCore.QueryDb.Overwrite, None)
118
+ )
119
+ ->Promise.all
120
+ }
121
+ }
122
+
123
+ // ── Phase-1/phase-2 pipelines ───────────────────────────────────────────────
124
+ // Both run: collect the batch → phase 1 → sync (so consumers observe Pending
125
+ // rows even if phase 2 fails) → phase 2 → sync. Unlike the in-process builders
126
+ // (which DETACH phase 2 to avoid a local-bus self-deadlock), the Lambda AWAITS
127
+ // it: SQS decouples the downstream fan-out, and a detached promise would
128
+ // freeze with the runtime between invocations.
129
+
130
+ // The callback shapes the shell's functor applications produce
131
+ // (AutomationSlice_Callback.T / OutboundTranslationSlice_Callback.T), typed at
132
+ // the seam.
133
+ type automationCallback = {
134
+ todoItems: dict<ReventlessCore.AutomationSlice_Callback.todoRow>,
135
+ phase1: (array<JSON.t>, Reventless.AutomationSlice.context) => unit,
136
+ phase2: ReventlessCore.CommandTopic.publishJsons => promise<unit>,
137
+ }
138
+ type outboundCallback<'event> = {
139
+ todoItems: dict<ReventlessCore.OutboundTranslationSlice_Callback.todoRow>,
140
+ phase1: array<'event> => unit,
141
+ phase2: ReventlessCore.CommandTopic.publishJsons => promise<unit>,
142
+ }
143
+
144
+ // Automation: phase 1 takes the RAW envelope JSONs — the callback routes by
145
+ // `meta.service` and unwraps `{id, meta, event}` itself — plus the context.
146
+ let makeAutomationJsonEventsHandler = (
147
+ ~context: Reventless.AutomationSlice.context,
148
+ ~callback: automationCallback,
149
+ ~publishJsons: ReventlessCore.CommandTopic.publishJsons,
150
+ ~syncTodoItems: unit => promise<unit>,
151
+ ): ReventlessCore.EventCollector.jsonEventsHandler =>
152
+ stream =>
153
+ stream
154
+ ->Stream.runCollect
155
+ ->Effect.flatMap(jsons =>
156
+ Effect.promise(async () => {
157
+ callback.phase1(jsons, context)
158
+ await syncTodoItems()
159
+ await callback.phase2(publishJsons)
160
+ await syncTodoItems()
161
+ })
162
+ )
163
+
164
+ // Outbound: phase 1 takes DECODED consumed events. Mirrors the in-process
165
+ // builder's decode — the tolerant DcbDecode on the inner `event` payload, so
166
+ // event types the slice does not consume are dropped silently instead of
167
+ // logging an error per event.
168
+ let makeOutboundJsonEventsHandler = (
169
+ ~consumedEventSchema: S.t<'event>,
170
+ ~callback: outboundCallback<'event>,
171
+ ~publishJsons: ReventlessCore.CommandTopic.publishJsons,
172
+ ~syncTodoItems: unit => promise<unit>,
173
+ ): ReventlessCore.EventCollector.jsonEventsHandler => {
174
+ let decoder = Reventless.DcbDecode.makeDecoder(consumedEventSchema)
175
+ stream =>
176
+ stream
177
+ ->Stream.mapEffect(json =>
178
+ Effect.sync(() => {
179
+ let rawEvent =
180
+ json
181
+ ->JSON.Decode.object
182
+ ->Option.flatMap(d => d->Dict.get("event"))
183
+ ->Option.getOr(json)
184
+ let (eventType, dataDict) = rawEvent->ReventlessCore.Message.splitMessage
185
+ switch decoder.decode(~eventType, ~data=dataDict) {
186
+ | Some(event) => [event]
187
+ | None => []
188
+ }
189
+ })
190
+ )
191
+ ->Stream.flatMap(events => Stream.fromIterable(events))
192
+ ->Stream.runCollect
193
+ ->Effect.flatMap(events =>
194
+ Effect.promise(async () => {
195
+ callback.phase1(events)
196
+ await syncTodoItems()
197
+ await callback.phase2(publishJsons)
198
+ await syncTodoItems()
199
+ })
200
+ )
201
+ }
202
+
203
+ // ── Handler assembly (called by the shell per HANDLER_CONFIG entry) ─────────
204
+ // Comp matches what the slice's own callback logs under, so a filter catches
205
+ // both the framework's lines and the application handler's.
206
+
207
+ let makeAutomationRegisteredHandler = (
208
+ entry: handlerEntry,
209
+ ~sliceName: string,
210
+ ~callback: automationCallback,
211
+ ): StreamRoutedEntryPoint_Ops.registeredHandler => {
212
+ handler: StreamRoutedEntryPoint_Ops.toStreamHandler(
213
+ makeAutomationJsonEventsHandler(
214
+ ~context=entry.context->Option.getOr(defaultContext(sliceName)),
215
+ ~callback,
216
+ ~publishJsons=makePublishJsons(entry.dcbQueueUrl),
217
+ ~syncTodoItems=makeSyncTodoItems(
218
+ ~queryDbTableName=entry.queryDbTableName,
219
+ ~todoItems=callback.todoItems,
220
+ ~rowSchema=ReventlessCore.AutomationSlice_Callback.todoRowSchema,
221
+ ),
222
+ ),
223
+ ),
224
+ comp: `AutomationSlice(${sliceName})`,
225
+ }
226
+
227
+ let makeOutboundRegisteredHandler = (
228
+ entry: handlerEntry,
229
+ ~sliceName: string,
230
+ ~consumedEventSchema: S.t<'event>,
231
+ ~callback: outboundCallback<'event>,
232
+ ): StreamRoutedEntryPoint_Ops.registeredHandler => {
233
+ handler: StreamRoutedEntryPoint_Ops.toStreamHandler(
234
+ makeOutboundJsonEventsHandler(
235
+ ~consumedEventSchema,
236
+ ~callback,
237
+ ~publishJsons=makePublishJsons(entry.dcbQueueUrl),
238
+ ~syncTodoItems=makeSyncTodoItems(
239
+ ~queryDbTableName=entry.queryDbTableName,
240
+ ~todoItems=callback.todoItems,
241
+ ~rowSchema=ReventlessCore.OutboundTranslationSlice_Callback.todoRowSchema,
242
+ ),
243
+ ),
244
+ ),
245
+ comp: `OutboundTranslationSlice(${sliceName})`,
246
+ }
@@ -0,0 +1,145 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
5
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Stream$1 from "effect/Stream";
10
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
+ import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/components/DcbDecode.res.mjs";
12
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
13
+ import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
14
+ import * as StreamRoutedEntryPoint_Ops$ReventlessAws from "./StreamRoutedEntryPoint_Ops.res.mjs";
15
+ import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
16
+ import * as QueryDbStorage_DynamoDb_Runtime$ReventlessAws from "../QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs";
17
+ import * as OutboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs";
18
+
19
+ function strOf(obj, key) {
20
+ return Stdlib_Option.flatMap(obj[key], Stdlib_JSON.Decode.string);
21
+ }
22
+
23
+ function decodeContext(json) {
24
+ return Stdlib_Option.map(Stdlib_JSON.Decode.object(json), c => ({
25
+ environment: Stdlib_Option.getOr(strOf(c, "environment"), "unknown"),
26
+ platformName: Stdlib_Option.getOr(strOf(c, "platformName"), "unknown"),
27
+ pluginName: Stdlib_Option.getOr(strOf(c, "pluginName"), "unknown"),
28
+ sliceName: Stdlib_Option.getOr(strOf(c, "sliceName"), "unknown")
29
+ }));
30
+ }
31
+
32
+ function decodeEntry(json) {
33
+ return Stdlib_Option.map(Stdlib_JSON.Decode.object(json), h => ({
34
+ specModule: Stdlib_Option.getOr(strOf(h, "specModule"), ""),
35
+ bodyModule: Stdlib_Option.getOr(strOf(h, "bodyModule"), ""),
36
+ callbackType: Stdlib_Option.getOr(strOf(h, "callbackType"), "automation"),
37
+ queryDbTableName: Stdlib_Option.getOr(strOf(h, "queryDbTableName"), ""),
38
+ dcbQueueUrl: Stdlib_Option.getOr(strOf(h, "dcbQueueUrl"), ""),
39
+ sourceUrn: Stdlib_Option.getOr(strOf(h, "sourceUrn"), ""),
40
+ context: Stdlib_Option.flatMap(h["context"], decodeContext)
41
+ }));
42
+ }
43
+
44
+ function parseHandlerConfig(rawJson) {
45
+ if (rawJson === "") {
46
+ return [];
47
+ } else {
48
+ return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(rawJson)), obj => obj["handlers"]), Stdlib_JSON.Decode.array), []), decodeEntry);
49
+ }
50
+ }
51
+
52
+ function warnMissingBodyModule(entry) {
53
+ StreamRoutedEntryPoint_Ops$ReventlessAws.logWarn(`handler entry for spec '` + entry.specModule + `' carries no bodyModule (stale HANDLER_CONFIG?); skipping`, {
54
+ comp: "AutomationSliceRuntime"
55
+ });
56
+ }
57
+
58
+ function defaultContext(sliceName) {
59
+ return {
60
+ environment: Stdlib_Option.getOr(process.env["Environment"], "unknown"),
61
+ platformName: "unknown",
62
+ pluginName: "unknown",
63
+ sliceName: sliceName
64
+ };
65
+ }
66
+
67
+ function makePublishJsons(queueUrl) {
68
+ let queue = {
69
+ id: queueUrl,
70
+ name: queueUrl,
71
+ arn: ""
72
+ };
73
+ return CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(queue, "SQS_FIFO");
74
+ }
75
+
76
+ function makeSyncTodoItems(queryDbTableName, todoItems, rowSchema) {
77
+ let table = {
78
+ id: "",
79
+ name: queryDbTableName,
80
+ arn: "",
81
+ hashKey: "id"
82
+ };
83
+ let save = QueryDbStorage_DynamoDb_Runtime$ReventlessAws.save(table);
84
+ return async () => {
85
+ await Promise.all(Object.entries(todoItems).map(param => save(param[0], S.reverseConvertToJsonOrThrow(param[1], rowSchema), "Overwrite", undefined)));
86
+ };
87
+ }
88
+
89
+ function makeAutomationJsonEventsHandler(context, callback, publishJsons, syncTodoItems) {
90
+ return stream => Effect.flatMap(Stream.runCollect(stream), jsons => Effect.promise(async () => {
91
+ callback.phase1(jsons, context);
92
+ await syncTodoItems();
93
+ await callback.phase2(publishJsons);
94
+ return await syncTodoItems();
95
+ }));
96
+ }
97
+
98
+ function makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJsons, syncTodoItems) {
99
+ let decoder = DcbDecode$Reventless.makeDecoder(consumedEventSchema);
100
+ return stream => Effect.flatMap(Stream.runCollect(Stream$1.flatMap(Stream$1.mapEffect(stream, json => Effect.sync(() => {
101
+ let rawEvent = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["event"]), json);
102
+ let match = Message$ReventlessCore.splitMessage(rawEvent);
103
+ let event = decoder.decode(match[0], match[1]);
104
+ if (event !== undefined) {
105
+ return [Primitive_option.valFromOption(event)];
106
+ } else {
107
+ return [];
108
+ }
109
+ })), events => Stream$1.fromIterable(events))), events => Effect.promise(async () => {
110
+ callback.phase1(events);
111
+ await syncTodoItems();
112
+ await callback.phase2(publishJsons);
113
+ return await syncTodoItems();
114
+ }));
115
+ }
116
+
117
+ function makeAutomationRegisteredHandler(entry, sliceName, callback) {
118
+ return {
119
+ 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))),
120
+ comp: `AutomationSlice(` + sliceName + `)`
121
+ };
122
+ }
123
+
124
+ function makeOutboundRegisteredHandler(entry, sliceName, consumedEventSchema, callback) {
125
+ return {
126
+ handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeOutboundJsonEventsHandler(consumedEventSchema, callback, makePublishJsons(entry.dcbQueueUrl), makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchema))),
127
+ comp: `OutboundTranslationSlice(` + sliceName + `)`
128
+ };
129
+ }
130
+
131
+ export {
132
+ strOf,
133
+ decodeContext,
134
+ decodeEntry,
135
+ parseHandlerConfig,
136
+ warnMissingBodyModule,
137
+ defaultContext,
138
+ makePublishJsons,
139
+ makeSyncTodoItems,
140
+ makeAutomationJsonEventsHandler,
141
+ makeOutboundJsonEventsHandler,
142
+ makeAutomationRegisteredHandler,
143
+ makeOutboundRegisteredHandler,
144
+ }
145
+ /* S Not a pure module */
@@ -8,8 +8,17 @@ type runtimeParts = Util.Lambda.runtimeParts
8
8
 
9
9
  type sliceInfo = {
10
10
  specModulePath: string,
11
+ // The slice's body module (`<Slice>_Automation.res` / `<Slice>_Translation.res`)
12
+ // — the second argument of the curried callback functor
13
+ // (AutomationSlice_Callback.Make(Spec)(Automation)). The entry point must
14
+ // import it alongside the spec; a spec module alone cannot rebuild the
15
+ // callback.
16
+ bodyModulePath: string,
11
17
  callbackType: string,
12
18
  queryDbTableName: Pulumi.Output.t<string>,
19
+ // Threaded to phase 1 collect/resolve on the automation path (mirrors the
20
+ // in-process builder's `~context`); outbound slices take no context.
21
+ context: option<Reventless.AutomationSlice.context>,
13
22
  }
14
23
 
15
24
  let bundledInfos: dict<sliceInfo> = Dict.make()
@@ -24,10 +33,12 @@ let getDcbQueueUrl = () => dcbQueueUrlRef.contents
24
33
  let registerAutomationSlice = (
25
34
  ~name,
26
35
  ~specModulePath,
36
+ ~bodyModulePath,
27
37
  ~callbackType="automation",
28
38
  ~queryDbTableName,
39
+ ~context=?,
29
40
  ) =>
30
- bundledInfos->Dict.set(name, {specModulePath, callbackType, queryDbTableName})
41
+ bundledInfos->Dict.set(name, {specModulePath, bodyModulePath, callbackType, queryDbTableName, context})
31
42
 
32
43
  type storedSpec = {
33
44
  componentName: string,
@@ -128,17 +139,31 @@ let finish = () =>
128
139
  | Some(info) =>
129
140
  let pkg = Util_Bundle.extractPackageName(info.specModulePath)
130
141
  packageDirs->Dict.set(pkg, Util_Bundle.resolvePackageRoot(pkg))
142
+ // Usually the same package as the spec, but bundle it explicitly —
143
+ // the entry point imports the body module by specifier too.
144
+ let bodyPkg = Util_Bundle.extractPackageName(info.bodyModulePath)
145
+ packageDirs->Dict.set(bodyPkg, Util_Bundle.resolvePackageRoot(bodyPkg))
131
146
 
132
147
  let specModule =
133
148
  info.specModulePath->JSON.stringifyAny->Option.getOr(`""`)
149
+ let bodyModule =
150
+ info.bodyModulePath->JSON.stringifyAny->Option.getOr(`""`)
134
151
  let callbackType =
135
152
  info.callbackType->JSON.stringifyAny->Option.getOr(`""`)
153
+ let contextFragment = switch info.context {
154
+ | Some(context) =>
155
+ context
156
+ ->JSON.stringifyAny
157
+ ->Option.map(json => `,"context":${json}`)
158
+ ->Option.getOr("")
159
+ | None => ""
160
+ }
136
161
 
137
162
  let handlerJson =
138
163
  Pulumi.Output.all3((info.queryDbTableName, dcbQueueUrl, spec.sourceUrns))
139
164
  ->Pulumi.Output.apply(((tableName, queueUrl, urns)) => {
140
165
  let sourceUrn = urns->Array.getUnsafe(0)
141
- `{"specModule":${specModule},"callbackType":${callbackType},"queryDbTableName":"${tableName}","dcbQueueUrl":"${queueUrl}","sourceUrn":"${sourceUrn}"}`
166
+ `{"specModule":${specModule},"bodyModule":${bodyModule},"callbackType":${callbackType},"queryDbTableName":"${tableName}","dcbQueueUrl":"${queueUrl}","sourceUrn":"${sourceUrn}"${contextFragment}}`
142
167
  })
143
168
  let _ = handlerOutputs->Array.push(handlerJson)
144
169
  | None =>
@@ -26,12 +26,14 @@ function getDcbQueueUrl() {
26
26
  return dcbQueueUrlRef.contents;
27
27
  }
28
28
 
29
- function registerAutomationSlice(name, specModulePath, callbackTypeOpt, queryDbTableName) {
29
+ function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, context) {
30
30
  let callbackType = callbackTypeOpt !== undefined ? callbackTypeOpt : "automation";
31
31
  bundledInfos[name] = {
32
32
  specModulePath: specModulePath,
33
+ bodyModulePath: bodyModulePath,
33
34
  callbackType: callbackType,
34
- queryDbTableName: queryDbTableName
35
+ queryDbTableName: queryDbTableName,
36
+ context: context
35
37
  };
36
38
  }
37
39
 
@@ -104,15 +106,20 @@ function finish() {
104
106
  }
105
107
  let pkg = Util_Bundle$ReventlessAws.extractPackageName(info.specModulePath);
106
108
  packageDirs[pkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(pkg);
109
+ let bodyPkg = Util_Bundle$ReventlessAws.extractPackageName(info.bodyModulePath);
110
+ packageDirs[bodyPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(bodyPkg);
107
111
  let specModule = Stdlib_Option.getOr(JSON.stringify(info.specModulePath), `""`);
112
+ let bodyModule = Stdlib_Option.getOr(JSON.stringify(info.bodyModulePath), `""`);
108
113
  let callbackType = Stdlib_Option.getOr(JSON.stringify(info.callbackType), `""`);
114
+ let context = info.context;
115
+ let contextFragment = context !== undefined ? Stdlib_Option.getOr(Stdlib_Option.map(JSON.stringify(context), json => `,"context":` + json), "") : "";
109
116
  let handlerJson = Pulumi.all([
110
117
  info.queryDbTableName,
111
118
  dcbQueueUrl,
112
119
  spec.sourceUrns
113
120
  ]).apply(param => {
114
121
  let sourceUrn = param[2][0];
115
- return `{"specModule":` + specModule + `,"callbackType":` + callbackType + `,"queryDbTableName":"` + param[0] + `","dcbQueueUrl":"` + param[1] + `","sourceUrn":"` + sourceUrn + `"}`;
122
+ return `{"specModule":` + specModule + `,"bodyModule":` + bodyModule + `,"callbackType":` + callbackType + `,"queryDbTableName":"` + param[0] + `","dcbQueueUrl":"` + param[1] + `","sourceUrn":"` + sourceUrn + `"` + contextFragment + `}`;
116
123
  });
117
124
  handlerOutputs.push(handlerJson);
118
125
  });