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

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 (110) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +7 -7
  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.mjs +14 -1
  32. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +22 -2
  33. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +15 -4
  34. package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +52 -510
  35. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +816 -0
  36. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +651 -0
  37. package/src/adapter/Runtime/EventMapperEntryPoint.mjs +23 -67
  38. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +126 -0
  39. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res.mjs +83 -0
  40. package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +16 -63
  41. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +150 -0
  42. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res.mjs +111 -0
  43. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res +96 -0
  44. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res.mjs +70 -0
  45. package/src/adapter/Runtime/PgMigrationEntryPoint.res +54 -0
  46. package/src/adapter/Runtime/PgMigrationEntryPoint.res.mjs +37 -0
  47. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +20 -55
  48. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +155 -0
  49. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +94 -0
  50. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +80 -0
  51. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +39 -0
  52. package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +4 -4
  53. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +34 -153
  54. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +114 -0
  55. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res.mjs +83 -0
  56. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +26 -6
  57. package/src/adapter/Runtime/SideEffectEntryPoint.mjs +23 -64
  58. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res +71 -0
  59. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res.mjs +57 -0
  60. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +5 -2
  61. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +1 -1
  62. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +34 -165
  63. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +206 -0
  64. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs +105 -0
  65. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +156 -0
  66. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res.mjs +123 -0
  67. package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +18 -138
  68. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +198 -0
  69. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res.mjs +155 -0
  70. package/src/adapter/StateTopic/StateTopic_AppSync.res +12 -170
  71. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +7 -167
  72. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +208 -0
  73. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +183 -0
  74. package/src/components/AutomationSlice_Builder.res +2 -0
  75. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  76. package/src/components/OutboundTranslationSlice_Builder.res +1 -0
  77. package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
  78. package/src/plugin/runtime/PluginRuntime_Builder.res +13 -4
  79. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +1 -1
  80. package/tests/AppSyncEventsSigner_OpsTest.res +75 -0
  81. package/tests/AppSyncEventsSigner_OpsTest.res.mjs +44 -0
  82. package/tests/AppSyncEventsSigner_ParityFixture.mjs +36 -0
  83. package/tests/AutomationSliceEntryPoint_OpsTest.res +165 -0
  84. package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +280 -0
  85. package/tests/CounterEntryPoint_OpsTest.res +115 -0
  86. package/tests/CounterEntryPoint_OpsTest.res.mjs +150 -0
  87. package/tests/EventCollectorEntryPoint_OpsTest.res +216 -0
  88. package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +339 -0
  89. package/tests/EventMapperEntryPoint_OpsTest.res +103 -0
  90. package/tests/EventMapperEntryPoint_OpsTest.res.mjs +120 -0
  91. package/tests/ExtensionPointEntryPoint_OpsTest.res +57 -0
  92. package/tests/ExtensionPointEntryPoint_OpsTest.res.mjs +54 -0
  93. package/tests/PgMigrationEntryPoint_IntegrationTest.res +22 -31
  94. package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +20 -23
  95. package/tests/PgQueryResolverEntryPoint_OpsTest.res +38 -0
  96. package/tests/PgQueryResolverEntryPoint_OpsTest.res.mjs +31 -0
  97. package/tests/Platform_AdminScan_OpsTest.res +80 -0
  98. package/tests/Platform_AdminScan_OpsTest.res.mjs +127 -0
  99. package/tests/ReadModelEntryPoint_OpsTest.res +141 -0
  100. package/tests/ReadModelEntryPoint_OpsTest.res.mjs +267 -0
  101. package/tests/SideEffectEntryPoint_OpsTest.res +79 -0
  102. package/tests/SideEffectEntryPoint_OpsTest.res.mjs +105 -0
  103. package/tests/StateViewSliceEntryPoint_OpsTest.res +167 -0
  104. package/tests/StateViewSliceEntryPoint_OpsTest.res.mjs +296 -0
  105. package/tests/TaskBucketEntryPoint_OpsTest.res +117 -0
  106. package/tests/TaskBucketEntryPoint_OpsTest.res.mjs +165 -0
  107. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res +42 -13
  108. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res.mjs +29 -6
  109. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +0 -57
  110. package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +0 -40
@@ -13,9 +13,28 @@ type iamPolicy = {
13
13
  }
14
14
  let additionalIamPolicies: array<iamPolicy> = []
15
15
 
16
- // Legacy CallbackFunction path — retained for module type compatibility.
17
- // Not called at runtime; all Lambda deployments now use makeFromCodeAsset.
18
- // Can be removed once the module type no longer requires environmentMaker.
16
+ // CallbackFunction path — the last serialized-closure Lambda builder on AWS.
17
+ //
18
+ // REACHABLE, despite most deployments going through makeFromCodeAsset. Three
19
+ // component builders instantiate ReventlessCore.EventCollectorRuntime_Builder_
20
+ // PerEventCollector.Make over this module — ForeignReadModel_Builder,
21
+ // SideEffectHandler_PerSideEffectHandler and Task_Builder_PerBucket — and that
22
+ // functor's forEventCollector calls `make` below. The live route is a Task whose
23
+ // spec returns `Task.sideEffects`: Task_Builder then constructs a
24
+ // SideEffectHandler, which provisions its collector Lambda here.
25
+ //
26
+ // A Lambda built this way is a serialized closure, and Pulumi's closure walker
27
+ // fails on Effect-TS — silently, so the function is simply never created and the
28
+ // deploy still reports success (see docs/plans/done/complete-bundled-migration.md,
29
+ // where an ordering-aws deploy came up four Lambdas short). Where it does
30
+ // serialize, it then resolves @aws-sdk against whatever the layer and the runtime
31
+ // each supply, carrying the cold-start SDK-skew failure compiled entry points
32
+ // were introduced to avoid. Converting these three builders to a compiled entry
33
+ // point is the fix; until then a side-effect-bearing Task is the exposed surface.
34
+ //
35
+ // `make` cannot simply be dropped: Runtime.Environment requires environmentMaker
36
+ // and the in-memory platform implements it for real (LocalRuntimeEnvironment
37
+ // registers the handler in a Deferred).
19
38
  let make: ReventlessCore.Runtime.environmentMaker<'event, context, 'result, parts> = (
20
39
  ~name,
21
40
  ~handler,
@@ -27,8 +46,8 @@ let make: ReventlessCore.Runtime.environmentMaker<'event, context, 'result, part
27
46
  let opts =
28
47
  opts->Option.map(ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions)
29
48
 
30
- // Legacy path (see above): no component kind reaches here because nothing calls
31
- // it, so it is attributed to the Platform rather than to an invented domain kind.
49
+ // The environmentMaker signature carries no component kind, so callers cannot
50
+ // pass one through; attributed to the Platform rather than to an invented kind.
32
51
  let tags = AWS.Tags.make(~name, ~kind=ReventlessCore.ComponentType.Platform, ~role=Runtime, ~scope=Platform)
33
52
 
34
53
  let lambdaRole = IAM.Role.makeWithDefaultPolicy(
@@ -58,7 +77,8 @@ let make: ReventlessCore.Runtime.environmentMaker<'event, context, 'result, part
58
77
  )
59
78
 
60
79
  // Coerce CallbackFunction.t → Function.t (structurally compatible: both have arn, id, name).
61
- // This legacy path is only retained for module type compatibility.
80
+ // The coercion goes away with the path itself, once the three PerEventCollector
81
+ // builders above move to a compiled entry point.
62
82
  let lambdaAsFunction: Pulumi.Output.t<PulumiAws.Lambda.Function.t> = lambda->Obj.magic
63
83
 
64
84
  {
@@ -1,82 +1,41 @@
1
1
  // SideEffectHandler Lambda entry point.
2
2
  // At cold start: reads HANDLER_CONFIG, dynamically imports SideEffect modules,
3
3
  // wires SideEffectHandler_Callback.Make, builds handler map keyed by source URN.
4
+ //
5
+ // Thin untyped shell ("typed core, thin shell" —
6
+ // docs/plans/minimize-lambda-entrypoint-mjs-shell.md): this file owns ONLY the
7
+ // seams that are inherently untyped — the dynamic `import()` of the side-effect
8
+ // modules named in HANDLER_CONFIG and the SideEffectHandler_Callback.Make
9
+ // functor application consuming them. HANDLER_CONFIG parsing and handler
10
+ // registration live type-checked in SideEffectEntryPoint_Ops.res; the routed
11
+ // dispatch boundary in StreamRoutedEntryPoint_Ops.res.
4
12
 
5
- import {
6
- log,
7
- runEffect,
8
- setRequestId,
9
- extractMetaField,
10
- extractSentTimestamp,
11
- extractRetryCount,
12
- } from "./HandlerFactoryHelpers.mjs";
13
13
  import { Make as sideEffectHandlerCallbackMake } from "@reventlessdev/reventless-core/src/components/SideEffectHandler/SideEffectHandler_Callback.res.mjs";
14
- import { handleStreamEvent } from "@reventlessdev/reventless-aws/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs";
14
+ import * as StreamOps from "./StreamRoutedEntryPoint_Ops.res.mjs";
15
+ import * as Ops from "./SideEffectEntryPoint_Ops.res.mjs";
15
16
 
16
17
  const dynamicImport = (specifier) => import('/var/task/node_modules/' + specifier);
17
18
 
18
- function groupBySource(records) {
19
- const dict = {};
20
- records.forEach(record => {
21
- const arn = record.eventSourceARN;
22
- const existing = dict[arn] || [];
23
- dict[arn] = existing.concat([record]);
24
- });
25
- return dict;
26
- }
27
-
28
19
  async function buildAllHandlers() {
29
- const configStr = process.env["HANDLER_CONFIG"] || '{"handlers":[]}';
30
- const config = JSON.parse(configStr);
31
20
  const handlers = {};
21
+ const entries = Ops.parseHandlerConfig(process.env["HANDLER_CONFIG"] || "");
32
22
 
33
- await Promise.all(config.handlers.map(async h => {
23
+ await Promise.all(entries.map(async (entry) => {
34
24
  const sideEffectModules = await Promise.all(
35
- h.sideEffectModules.map(async modPath => await dynamicImport(modPath))
25
+ entry.sideEffectModules.map(async modPath => await dynamicImport(modPath))
26
+ );
27
+ const callback = sideEffectHandlerCallbackMake({
28
+ sideEffects: sideEffectModules,
29
+ queryEngine: Ops.noopQueryEngine,
30
+ });
31
+ StreamOps.addToRegistry(
32
+ handlers,
33
+ entry.sourceUrn,
34
+ Ops.makeRegisteredHandler(entry, callback.handleJsonEvents)
36
35
  );
37
- const noOpQueryEngine = { scan: async () => [], query: async () => [] };
38
- const callback = sideEffectHandlerCallbackMake({ sideEffects: sideEffectModules, queryEngine: noOpQueryEngine });
39
- const handler = (event, context) => handleStreamEvent(callback.handleJsonEvents, event, context);
40
- // Multiple side-effect handlers can share one source stream. Accumulate all
41
- // per source ARN; a plain assignment collapses them to one (Promise.all
42
- // race), silently dropping the rest. `comp`/`plugin` come from
43
- // HANDLER_CONFIG — the shell has only module paths to identify a handler by.
44
- (handlers[h.sourceUrn] ||= []).push({ handler, comp: h.comp, plugin: h.plugin });
45
36
  }));
46
37
 
47
38
  return handlers;
48
39
  }
49
40
 
50
- const initPromise = buildAllHandlers();
51
-
52
- export async function handler(event, context) {
53
- setRequestId(context?.awsRequestId);
54
- const handlers = await initPromise;
55
- const records = event.Records || [];
56
- const grouped = groupBySource(records);
57
-
58
- await Promise.all(Object.entries(grouped).map(async ([arn, subRecords]) => {
59
- const streamHandlers = handlers[arn];
60
- if (streamHandlers !== undefined && streamHandlers.length > 0) {
61
- log.debug("found " + streamHandlers.length + " handler(s) for " + arn, { comp: "SideEffectRuntime" });
62
- const correlationId = extractMetaField(subRecords, "correlationId");
63
- const causationId = extractMetaField(subRecords, "causationId");
64
- const timestamp = extractSentTimestamp(subRecords);
65
- const retryCount = extractRetryCount(subRecords);
66
- await Promise.all(streamHandlers.map(({ handler: h, comp, plugin }) =>
67
- runEffect(h({ Records: subRecords }, context), {
68
- correlationId,
69
- causationId,
70
- comp,
71
- plugin,
72
- timestamp,
73
- retryCount,
74
- })
75
- ));
76
- } else {
77
- log.warn("no handler found: " + arn, { comp: "SideEffectRuntime" });
78
- }
79
- }));
80
-
81
- return "";
82
- }
41
+ export const handler = StreamOps.makeRoutedHandler("SideEffectRuntime", buildAllHandlers());
@@ -0,0 +1,71 @@
1
+ // Typed cold-start core for the SideEffectHandler Lambda entry point.
2
+ //
3
+ // The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md):
4
+ // SideEffectEntryPoint.mjs keeps only the untyped seam — the dynamic `import()`
5
+ // of the side-effect modules named in HANDLER_CONFIG and the
6
+ // SideEffectHandler_Callback.Make functor application consuming them.
7
+ // HANDLER_CONFIG parsing and handler registration live here, fully
8
+ // type-checked; the routed dispatch boundary is shared with the other
9
+ // stream-routed entry points in StreamRoutedEntryPoint_Ops.
10
+
11
+ type handlerEntry = {
12
+ sourceUrn: string,
13
+ sideEffectModules: array<string>,
14
+ // Resolved at deploy time and baked into HANDLER_CONFIG
15
+ // (SideEffectHandlerRuntime_Builder_Single) — the shell has only module paths
16
+ // to identify a handler by.
17
+ comp: option<string>,
18
+ plugin: option<string>,
19
+ }
20
+
21
+ let strOf = (obj: dict<JSON.t>, key: string): option<string> =>
22
+ obj->Dict.get(key)->Option.flatMap(JSON.Decode.string)
23
+
24
+ let decodeEntry = (json: JSON.t): option<handlerEntry> =>
25
+ json
26
+ ->JSON.Decode.object
27
+ ->Option.map(h => {
28
+ sourceUrn: h->strOf("sourceUrn")->Option.getOr(""),
29
+ sideEffectModules: h
30
+ ->Dict.get("sideEffectModules")
31
+ ->Option.flatMap(JSON.Decode.array)
32
+ ->Option.getOr([])
33
+ ->Array.filterMap(JSON.Decode.string),
34
+ comp: h->strOf("comp"),
35
+ plugin: h->strOf("plugin"),
36
+ })
37
+
38
+ let parseHandlerConfig = (rawJson: string): array<handlerEntry> =>
39
+ rawJson == ""
40
+ ? []
41
+ : rawJson
42
+ ->JSON.parseOrThrow
43
+ ->JSON.Decode.object
44
+ ->Option.flatMap(obj => obj->Dict.get("handlers"))
45
+ ->Option.flatMap(JSON.Decode.array)
46
+ ->Option.getOr([])
47
+ ->Array.filterMap(decodeEntry)
48
+
49
+ // Side effects are fire-and-forget consumers with no query access in the
50
+ // bundled handler — same restriction as the former shell's no-op stub.
51
+ let noopQueryEngine: Reventless.QueryEngine.operations = {
52
+ scan: async (~readModelName as _, ~filterConfigs as _, ~limit as _) => [],
53
+ query: async (
54
+ ~readModelName as _,
55
+ ~key as _=?,
56
+ ~id as _,
57
+ ~subIdConfig as _=?,
58
+ ~filterConfigs as _=?,
59
+ ~ascending as _=?,
60
+ ~limit as _=?,
61
+ ) => [],
62
+ }
63
+
64
+ let makeRegisteredHandler = (
65
+ entry: handlerEntry,
66
+ handleJsonEvents: ReventlessCore.EventCollector.jsonEventsHandler,
67
+ ): StreamRoutedEntryPoint_Ops.registeredHandler => {
68
+ handler: StreamRoutedEntryPoint_Ops.toStreamHandler(handleJsonEvents),
69
+ comp: ?entry.comp,
70
+ plugin: ?entry.plugin,
71
+ }
@@ -0,0 +1,57 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as StreamRoutedEntryPoint_Ops$ReventlessAws from "./StreamRoutedEntryPoint_Ops.res.mjs";
7
+
8
+ function strOf(obj, key) {
9
+ return Stdlib_Option.flatMap(obj[key], Stdlib_JSON.Decode.string);
10
+ }
11
+
12
+ function decodeEntry(json) {
13
+ return Stdlib_Option.map(Stdlib_JSON.Decode.object(json), h => ({
14
+ sourceUrn: Stdlib_Option.getOr(strOf(h, "sourceUrn"), ""),
15
+ sideEffectModules: Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(h["sideEffectModules"], Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.string),
16
+ comp: strOf(h, "comp"),
17
+ plugin: strOf(h, "plugin")
18
+ }));
19
+ }
20
+
21
+ function parseHandlerConfig(rawJson) {
22
+ if (rawJson === "") {
23
+ return [];
24
+ } else {
25
+ 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);
26
+ }
27
+ }
28
+
29
+ async function noopQueryEngine_scan(param, param$1, param$2) {
30
+ return [];
31
+ }
32
+
33
+ async function noopQueryEngine_query(param, param$1, param$2, param$3, param$4, param$5, param$6) {
34
+ return [];
35
+ }
36
+
37
+ let noopQueryEngine = {
38
+ scan: noopQueryEngine_scan,
39
+ query: noopQueryEngine_query
40
+ };
41
+
42
+ function makeRegisteredHandler(entry, handleJsonEvents) {
43
+ return {
44
+ handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(handleJsonEvents),
45
+ comp: entry.comp,
46
+ plugin: entry.plugin
47
+ };
48
+ }
49
+
50
+ export {
51
+ strOf,
52
+ decodeEntry,
53
+ parseHandlerConfig,
54
+ noopQueryEngine,
55
+ makeRegisteredHandler,
56
+ }
57
+ /* StreamRoutedEntryPoint_Ops-ReventlessAws Not a pure module */
@@ -21,7 +21,10 @@ let log = ReventlessCore.Logger.fromEnv()
21
21
  type inboundSlicePaths = {
22
22
  specPath: string,
23
23
  translationPath: string,
24
- auditTableName: option<Pulumi.Output.t<string>>,
24
+ // Plain Output, never `option<Pulumi.Output.t<string>>` — see the note on
25
+ // `PluginRuntime_Builder.inboundSliceReg.auditTableName`. Empty string = no
26
+ // audit table (serialized as `null`).
27
+ auditTableName: Pulumi.Output.t<string>,
25
28
  }
26
29
 
27
30
  // `slicePaths`: `(specPath, behaviorPath)` per StateChangeSlice (already merged
@@ -142,7 +145,7 @@ let forDcbCommandTopic = (
142
145
  | [] => Pulumi.Output.make("")
143
146
  | slices =>
144
147
  slices
145
- ->Array.map(s => s.auditTableName->Option.getOr(Pulumi.Output.make("")))
148
+ ->Array.map(s => s.auditTableName)
146
149
  ->Pulumi.Output.all
147
150
  ->Pulumi.Output.apply(tableNames => {
148
151
  let entries =
@@ -59,7 +59,7 @@ function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginNa
59
59
  return `{"spec":` + s + `,"behavior":` + b + `}`;
60
60
  }).join(",");
61
61
  let pgConnectionFragment = pgSelection !== undefined ? pgSelection.connectionConfig.apply(cc => `,"pgConnection":` + JSON.stringify(PgConnection$ReventlessAws.connectionConfigToJson(pgSelection.lockStrategy, cc))) : Pulumi.output("");
62
- let inboundFragment = inboundSlices.length !== 0 ? Pulumi.all(inboundSlices.map(s => Stdlib_Option.getOr(s.auditTableName, Pulumi.output("")))).apply(tableNames => {
62
+ let inboundFragment = inboundSlices.length !== 0 ? Pulumi.all(inboundSlices.map(s => s.auditTableName)).apply(tableNames => {
63
63
  let entries = inboundSlices.map((s, i) => {
64
64
  let spec = Stdlib_Option.getOr(JSON.stringify(s.specPath), `""`);
65
65
  let translation = Stdlib_Option.getOr(JSON.stringify(s.translationPath), `""`);
@@ -1,177 +1,46 @@
1
1
  // StateViewSlice Lambda entry point.
2
- // At cold start: reads HANDLER_CONFIG, dynamically imports Spec modules,
3
- // builds projection handler that decodes events and runs Projection.handleAction.
4
- // Routes DynamoDB Stream events by source URN.
5
-
6
- import * as Effect from "effect/Effect";
7
- import * as Stream from "effect/Stream";
8
- import { parseJsonOrThrow } from "sury/src/S.res.mjs";
9
- import {
10
- log,
11
- runEffect,
12
- setRequestId,
13
- extractMetaField,
14
- extractSentTimestamp,
15
- extractRetryCount,
16
- } from "./HandlerFactoryHelpers.mjs";
17
- import { handleAction } from "@reventlessdev/reventless-core/src/Projection.res.mjs";
18
- // Typed cold-start core — DynamoDB QueryDb ops, compiler-checked against the
19
- // framework signatures (see the module header and
20
- // docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
21
- import { makeDynamoQueryDbOps } from "./QueryDbEntryPoint_Ops.res.mjs";
22
- import { opsFor as pgQdbOpsFor } from "@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs";
23
- import { withLiveUpdates } from "./StateTopicPublish.mjs";
24
- import { handleStreamEvent } from "@reventlessdev/reventless-aws/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs";
2
+ // At cold start: reads HANDLER_CONFIG, dynamically imports Spec/Projection
3
+ // modules, builds a projection handler per slice, and routes stream events by
4
+ // 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 spec/projection modules
9
+ // named in HANDLER_CONFIG and the reads of their exports. Everything else
10
+ // (HANDLER_CONFIG parsing incl. the compact-v2 expansion, the QueryDb operation
11
+ // assembly incl. the Postgres / live-update branch, the projection stream
12
+ // pipeline, the routed dispatch boundary) lives type-checked in
13
+ // StateViewSliceEntryPoint_Ops.res / ProjectionEntryPoint_Ops.res /
14
+ // StreamRoutedEntryPoint_Ops.res.
15
+
16
+ import * as StreamOps from "./StreamRoutedEntryPoint_Ops.res.mjs";
17
+ import * as Ops from "./StateViewSliceEntryPoint_Ops.res.mjs";
25
18
 
26
19
  const dynamicImport = (specifier) => import('/var/task/node_modules/' + specifier);
27
20
 
28
- // One Lambda hosts every state-view slice of a plugin, so each handler carries
29
- // the comp of the slice it projects — matching the `StateViewSlice(<name>)`
30
- // shape the rest of the framework logs under.
31
- const stateViewComp = (sliceName) => `StateViewSlice(${sliceName})`;
32
-
33
- function groupBySource(records) {
34
- const dict = {};
35
- records.forEach(record => {
36
- const arn = record.eventSourceARN;
37
- const existing = dict[arn] || [];
38
- dict[arn] = existing.concat([record]);
39
- });
40
- return dict;
41
- }
42
-
43
- // `pgConnection`, when present, selects the Postgres QueryDb runtime for this
44
- // slice's view table (`queryDbTableName` is then the slice spec name, the shared
45
- // `qdb_<name>` discriminator). Absent → the DynamoDB path is byte-identical.
46
- export function buildJsonEventsHandler(specModule, projectionModule, queryDbTableName, pgConnection, stateTopicName) {
47
- let queryDbOps;
48
- if (pgConnection) {
49
- const indexes = (specModule.config && specModule.config.indexes) || [];
50
- const subIdField = specModule.subIdConfig ? specModule.subIdConfig.subIdField : undefined;
51
- const pgOps = pgQdbOpsFor(pgConnection, queryDbTableName, indexes, subIdField);
52
- // B3.3: subscription-enabled (Stream) Postgres view slice → publish a
53
- // live-update descriptor after save/delete. `stateTopicName` + APPSYNC_ENDPOINT
54
- // gate it; absent → withLiveUpdates passes pgOps through. The view slice's id
55
- // is the save's first arg (no id injection), so no wrap-ordering concern.
56
- queryDbOps = withLiveUpdates(pgOps, {
57
- endpoint: process.env.APPSYNC_ENDPOINT,
58
- region: process.env.AWS_REGION,
59
- topicName: stateTopicName,
60
- subIdField,
61
- });
62
- } else {
63
- queryDbOps = makeDynamoQueryDbOps(queryDbTableName);
64
- }
65
- // Spec module exports `consumedEventSchema`; the `project` function lives in
66
- // the sibling projection module (`<Name>_Projection.res`).
67
- const eventSchema = specModule.consumedEventSchema;
68
- const project = projectionModule.project;
69
-
70
- return stream => Stream.runForEach(
71
- Stream.flatMap(
72
- Stream.mapEffect(stream, json => Effect.sync(() => {
73
- try {
74
- // Events arrive as `{id, meta, recordedAt, event}` envelopes
75
- // (Util_DynamoDbStream_Runtime.buildJsonEvent' / PgChangeFeedRelay).
76
- // Surface `meta` + `recordedAt` to the projection as the `consumed`
77
- // envelope; `recordedAt` defaults to "" if a producer omitted it.
78
- const eventJson = json.event != null ? json.event : json;
79
- const meta = json.meta;
80
- const recordedAt = json.recordedAt != null ? json.recordedAt : "";
81
- return project({ event: parseJsonOrThrow(eventJson, eventSchema), meta, recordedAt });
82
- } catch (exn) {
83
- log.error("failed to decode event", { comp: "StateViewSliceRuntime", detail: exn && exn.message ? exn.message : String(exn) });
84
- return [];
85
- }
86
- })),
87
- actions => Stream.fromIterable(actions)
88
- ),
89
- action => Effect.map(
90
- // Thread the slice's `subIdConfig` (generated from `@subId`) so sub-id-dependent
91
- // actions like `UpdateMultiState` can resolve the sort key. Dropping it here made
92
- // every such action hit the `MissingSubIdConfig` guard and silently write nothing,
93
- // while the test-harness callback path (which threads it) stayed green. `undefined`
94
- // for slices without an `@subId` is correct — those never emit sub-id actions.
95
- // First positional arg is the compiled `~comp=` optional (attribution
96
- // for Projection.handleAction's debug-lazy action log) — the slice's
97
- // spec name. Compiled shape: (comp, action, operations, subIdConfig).
98
- Effect.promise(() => handleAction(specModule.name, action, queryDbOps, specModule.subIdConfig)),
99
- _ => {}
100
- )
101
- );
102
- }
103
-
104
- // HANDLER_CONFIG is emitted compact (StateViewSliceRuntime_Builder_Single) to
105
- // stay under AWS Lambda's 4KB env-var limit: a shared module-path `base` and a
106
- // shared `sourceUrn` are hoisted out, and per-handler keys are shortened to
107
- // s/p/q/u. Expand back to the full {specModule, projectionModule,
108
- // queryDbTableName, sourceUrn} shape. Legacy full-key entries pass through.
109
- function expandHandlers(config) {
110
- const base = config.base || "";
111
- const sharedUrn = config.sourceUrn || "";
112
- return (config.handlers || []).map(h => {
113
- if (h.specModule !== undefined) return h;
114
- return {
115
- specModule: base + h.s,
116
- projectionModule: base + h.p,
117
- queryDbTableName: h.q,
118
- sourceUrn: h.u !== undefined ? h.u : sharedUrn,
119
- // Shared across a plugin's slices (all follow the platform backend toggle);
120
- // hoisted to the top level of the compressed config like base/sourceUrn.
121
- pgConnection: config.pgConnection,
122
- // B3.3: per-slice AppSync Events channel root (list field name). Present
123
- // only for subscription-enabled (Stream) Postgres view slices.
124
- stateTopicName: h.t,
125
- };
126
- });
127
- }
128
-
129
21
  async function buildAllHandlers() {
130
- const configStr = process.env["HANDLER_CONFIG"] || '{"handlers":[]}';
131
- const config = JSON.parse(configStr);
132
22
  const handlers = {};
133
- await Promise.all(expandHandlers(config).map(async h => {
23
+ const entries = Ops.parseHandlerConfig(process.env["HANDLER_CONFIG"] || "");
24
+
25
+ await Promise.all(entries.map(async (entry) => {
134
26
  const [specModule, projectionModule] = await Promise.all([
135
- dynamicImport(h.specModule),
136
- dynamicImport(h.projectionModule),
27
+ dynamicImport(entry.specModule),
28
+ dynamicImport(entry.projectionModule),
137
29
  ]);
138
- const jsonEventsHandler = buildJsonEventsHandler(specModule, projectionModule, h.queryDbTableName, h.pgConnection, h.stateTopicName);
139
- const streamHandler = (event, context) => handleStreamEvent(jsonEventsHandler, event, context);
140
- const existing = handlers[h.sourceUrn] || [];
141
- existing.push({ handler: streamHandler, comp: stateViewComp(specModule.name) });
142
- handlers[h.sourceUrn] = existing;
30
+ StreamOps.addToRegistry(
31
+ handlers,
32
+ entry.sourceUrn,
33
+ Ops.makeRegisteredHandler(entry, {
34
+ name: specModule.name,
35
+ consumedEventSchema: specModule.consumedEventSchema,
36
+ project: projectionModule.project,
37
+ config: specModule.config,
38
+ subIdConfig: specModule.subIdConfig,
39
+ })
40
+ );
143
41
  }));
42
+
144
43
  return handlers;
145
44
  }
146
45
 
147
- const initPromise = buildAllHandlers();
148
-
149
- export async function handler(event, context) {
150
- setRequestId(context?.awsRequestId);
151
- const handlers = await initPromise;
152
- const records = event.Records || [];
153
- const grouped = groupBySource(records);
154
-
155
- await Promise.all(Object.entries(grouped).map(async ([arn, subRecords]) => {
156
- const streamHandlers = handlers[arn];
157
- if (streamHandlers !== undefined && streamHandlers.length > 0) {
158
- log.debug("found " + streamHandlers.length + " handler(s) for " + arn, { comp: "StateViewSliceRuntime" });
159
- const correlationId = extractMetaField(subRecords, "correlationId");
160
- const causationId = extractMetaField(subRecords, "causationId");
161
- const timestamp = extractSentTimestamp(subRecords);
162
- const retryCount = extractRetryCount(subRecords);
163
- await Promise.all(streamHandlers.map(({ handler: h, comp }) =>
164
- runEffect(h({ Records: subRecords }, context), {
165
- correlationId,
166
- causationId,
167
- comp,
168
- timestamp,
169
- retryCount,
170
- })
171
- ));
172
- } else {
173
- log.warn("no handler found: " + arn, { comp: "StateViewSliceRuntime" });
174
- }
175
- }));
176
- return "";
177
- }
46
+ export const handler = StreamOps.makeRoutedHandler("StateViewSliceRuntime", buildAllHandlers());