@reventlessdev/reventless-aws 3.0.0-alpha.231 → 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 +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_Ops.res +16 -3
  32. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +11 -6
  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,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 */
@@ -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());
@@ -0,0 +1,206 @@
1
+ // Typed cold-start core for the StateViewSlice Lambda entry point.
2
+ //
3
+ // The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md):
4
+ // StateViewSliceEntryPoint.mjs keeps only the inherently-untyped seam — the
5
+ // dynamic `import()` of spec/projection modules named in HANDLER_CONFIG and the
6
+ // reads of their exports. HANDLER_CONFIG parsing (including the compact-v2
7
+ // expansion), the QueryDb operation assembly, and the whole projection stream
8
+ // pipeline (envelope decode → sury event parse → project → handleAction) live
9
+ // here, fully type-checked; the QueryDb backend branch is shared with the
10
+ // ReadModel entry point in ProjectionEntryPoint_Ops and the routed dispatch
11
+ // boundary lives in StreamRoutedEntryPoint_Ops.
12
+
13
+ type handlerEntry = {
14
+ specModule: string,
15
+ projectionModule: string,
16
+ queryDbTableName: string,
17
+ sourceUrn: string,
18
+ // B3.3: per-slice AppSync Events channel root (list field name). Present only
19
+ // for subscription-enabled (Stream) Postgres view slices.
20
+ stateTopicName: option<string>,
21
+ pgConnection: option<PgConnection.connectionConfig>,
22
+ }
23
+
24
+ // The builder writes `pgConnection` via PgConnection.connectionConfigToJson, so
25
+ // the parsed object IS the record shape — cast at the documented JSON contract
26
+ // (the AggregateEntryPoint_Ops.asConnectionConfig pattern).
27
+ external asPgConnection: JSON.t => PgConnection.connectionConfig = "%identity"
28
+
29
+ let strOf = (obj: dict<JSON.t>, key: string): option<string> =>
30
+ obj->Dict.get(key)->Option.flatMap(JSON.Decode.string)
31
+
32
+ let pgConnectionOf = (obj: dict<JSON.t>): option<PgConnection.connectionConfig> =>
33
+ obj
34
+ ->Dict.get("pgConnection")
35
+ ->Option.filter(j => j != JSON.Null)
36
+ ->Option.map(asPgConnection)
37
+
38
+ // HANDLER_CONFIG is emitted compact (StateViewSliceRuntime_Builder_Single) to
39
+ // stay under AWS Lambda's 4KB env-var limit: a shared module-path `base`, a
40
+ // shared `sourceUrn`, and a shared `pgConnection` are hoisted out (all of a
41
+ // plugin's slices follow the platform backend toggle), and per-handler keys are
42
+ // shortened to s/p/q/u/t. Expand back to the full handlerEntry shape. Legacy
43
+ // full-key entries pass through with their own per-entry fields.
44
+ let parseHandlerConfig = (rawJson: string): array<handlerEntry> =>
45
+ rawJson == ""
46
+ ? []
47
+ : {
48
+ let obj =
49
+ rawJson
50
+ ->JSON.parseOrThrow
51
+ ->JSON.Decode.object
52
+ ->Option.getOr(Dict.make())
53
+ let base = obj->strOf("base")->Option.getOr("")
54
+ let sharedUrn = obj->strOf("sourceUrn")->Option.getOr("")
55
+ let sharedPgConnection = obj->pgConnectionOf
56
+ obj
57
+ ->Dict.get("handlers")
58
+ ->Option.flatMap(JSON.Decode.array)
59
+ ->Option.getOr([])
60
+ ->Array.filterMap(item =>
61
+ item
62
+ ->JSON.Decode.object
63
+ ->Option.map(h =>
64
+ switch h->strOf("specModule") {
65
+ | Some(specModule) => {
66
+ specModule,
67
+ projectionModule: h->strOf("projectionModule")->Option.getOr(""),
68
+ queryDbTableName: h->strOf("queryDbTableName")->Option.getOr(""),
69
+ sourceUrn: h->strOf("sourceUrn")->Option.getOr(""),
70
+ stateTopicName: h->strOf("stateTopicName"),
71
+ pgConnection: h->pgConnectionOf,
72
+ }
73
+ | None => {
74
+ specModule: base ++ h->strOf("s")->Option.getOr(""),
75
+ projectionModule: base ++ h->strOf("p")->Option.getOr(""),
76
+ queryDbTableName: h->strOf("q")->Option.getOr(""),
77
+ sourceUrn: h->strOf("u")->Option.getOr(sharedUrn),
78
+ stateTopicName: h->strOf("t"),
79
+ pgConnection: sharedPgConnection,
80
+ }
81
+ }
82
+ )
83
+ )
84
+ }
85
+
86
+ // ── Projection stream pipeline ──────────────────────────────────────────────
87
+
88
+ // Events arrive as `{id, meta, recordedAt, event}` envelopes
89
+ // (Util_DynamoDbStream_Runtime.buildJsonEvent' / PgChangeFeedRelay). Surface
90
+ // `meta` + `recordedAt` to the projection as the `consumed` envelope;
91
+ // `recordedAt` defaults to "" if a producer omitted it. `meta` is already
92
+ // schema-shaped JSON written by the producer — cast at the documented contract
93
+ // (absent maps to undefined, exactly what the former JS passed through).
94
+ external asMeta: option<JSON.t> => Reventless.Message.meta = "%identity"
95
+
96
+ type envelope<'event> = {
97
+ event: JSON.t,
98
+ meta: Reventless.Message.meta,
99
+ recordedAt: string,
100
+ }
101
+
102
+ let decodeEnvelope = (json: JSON.t): envelope<'event> => {
103
+ let fields = json->JSON.Decode.object
104
+ {
105
+ event: switch fields->Option.flatMap(f => f->Dict.get("event")) {
106
+ | Some(JSON.Null) | None => json
107
+ | Some(event) => event
108
+ },
109
+ meta: fields->Option.flatMap(f => f->Dict.get("meta"))->asMeta,
110
+ recordedAt: fields
111
+ ->Option.flatMap(f => f->Dict.get("recordedAt"))
112
+ ->Option.flatMap(JSON.Decode.string)
113
+ ->Option.getOr(""),
114
+ }
115
+ }
116
+
117
+ let exnMessage = StreamRoutedEntryPoint_Ops.exnMessage
118
+
119
+ // The typed core of buildJsonEventsHandler: decode each envelope, parse the
120
+ // event against the slice's consumed-event schema, project, and run every
121
+ // resulting action against the view table. A decode failure logs and yields no
122
+ // actions (the batch continues — at-least-once delivery would otherwise wedge
123
+ // the whole source on one malformed record).
124
+ let makeJsonEventsHandler = (
125
+ ~sliceName: string,
126
+ ~eventSchema: S.t<'event>,
127
+ ~project: Reventless.StateViewSlice.consumed<'event> => array<
128
+ Reventless.Projection.action<string, JSON.t>,
129
+ >,
130
+ ~queryDbOps: ReventlessCore.QueryDb_Adapter.operations,
131
+ ~subIdConfig: option<Reventless.ReadModel.subIdConfig<JSON.t>>,
132
+ ): ReventlessCore.EventCollector.jsonEventsHandler =>
133
+ stream =>
134
+ stream
135
+ ->Stream.mapEffect(json =>
136
+ Effect.sync(() =>
137
+ try {
138
+ let {event, meta, recordedAt} = decodeEnvelope(json)
139
+ project({event: event->S.parseJsonOrThrow(eventSchema), meta, recordedAt})
140
+ } catch {
141
+ | exn =>
142
+ StreamRoutedEntryPoint_Ops.logError(
143
+ "failed to decode event",
144
+ {comp: "StateViewSliceRuntime", detail: exnMessage(exn)},
145
+ )
146
+ []
147
+ }
148
+ )
149
+ )
150
+ ->Stream.flatMap(actions => Stream.fromIterable(actions))
151
+ ->Stream.runForEach(action =>
152
+ // Thread the slice's `subIdConfig` (generated from `@subId`) so
153
+ // sub-id-dependent actions like `UpdateMultiState` can resolve the sort
154
+ // key. Dropping it here made every such action hit the
155
+ // `MissingSubIdConfig` guard and silently write nothing, while the
156
+ // test-harness callback path (which threads it) stayed green. `None` for
157
+ // slices without an `@subId` is correct — those never emit sub-id
158
+ // actions. `~comp` is the slice's spec name — attribution for
159
+ // Projection.handleAction's debug-lazy action log.
160
+ Effect.promise(() =>
161
+ ReventlessCore.Projection.handleAction(~comp=sliceName, action, queryDbOps, subIdConfig)
162
+ )->Effect.map(_ => ())
163
+ )
164
+
165
+ // ── Handler assembly (called by the shell per HANDLER_CONFIG entry) ─────────
166
+ // `sliceModules` types the shell's reads off the two dynamically imported
167
+ // modules: the spec module exports `name` / `consumedEventSchema` / `config` /
168
+ // `subIdConfig`; the `project` function lives in the sibling projection module
169
+ // (`<Name>_Projection.res`).
170
+
171
+ type sliceModules<'event> = {
172
+ name: string,
173
+ consumedEventSchema: S.t<'event>,
174
+ project: Reventless.StateViewSlice.consumed<'event> => array<
175
+ Reventless.Projection.action<string, JSON.t>,
176
+ >,
177
+ config: option<ProjectionEntryPoint_Ops.specConfig>,
178
+ subIdConfig: option<Reventless.ReadModel.subIdConfig<JSON.t>>,
179
+ }
180
+
181
+ let makeRegisteredHandler = (
182
+ entry: handlerEntry,
183
+ modules: sliceModules<'event>,
184
+ ): StreamRoutedEntryPoint_Ops.registeredHandler => {
185
+ let queryDbOps = ProjectionEntryPoint_Ops.makeQueryDbOps(
186
+ ~queryDbTableName=entry.queryDbTableName,
187
+ ~pgConnection=entry.pgConnection,
188
+ ~stateTopicName=entry.stateTopicName,
189
+ ~indexes=ProjectionEntryPoint_Ops.indexesOf(modules.config),
190
+ ~subIdField=ProjectionEntryPoint_Ops.subIdFieldOf(modules.subIdConfig),
191
+ )
192
+ {
193
+ handler: StreamRoutedEntryPoint_Ops.toStreamHandler(
194
+ makeJsonEventsHandler(
195
+ ~sliceName=modules.name,
196
+ ~eventSchema=modules.consumedEventSchema,
197
+ ~project=modules.project,
198
+ ~queryDbOps,
199
+ ~subIdConfig=modules.subIdConfig,
200
+ ),
201
+ ),
202
+ // Matches the `StateViewSlice(<name>)` shape the rest of the framework
203
+ // logs under.
204
+ comp: `StateViewSlice(${modules.name})`,
205
+ }
206
+ }