@reventlessdev/reventless-aws 3.0.0-alpha.220 → 3.0.0-alpha.221

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 (160) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res.mjs +4 -0
  4. package/src/Platform_Stack.res +6 -0
  5. package/src/Platform_Stack.res.mjs +3 -1
  6. package/src/adapter/AWS_Tags.res +127 -12
  7. package/src/adapter/AWS_Tags.res.mjs +58 -17
  8. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +13 -1
  9. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +3 -4
  10. package/src/adapter/Api/Platform_UIFragments_Lambda.res +13 -1
  11. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +3 -4
  12. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +12 -0
  13. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res.mjs +4 -2
  14. package/src/adapter/CommandGenerator/InboundTranslationResolvers_AppSync.res +6 -0
  15. package/src/adapter/CommandGenerator/InboundTranslationResolvers_AppSync.res.mjs +2 -1
  16. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +2 -2
  17. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +2 -2
  18. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +2 -2
  19. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +2 -2
  20. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +2 -2
  21. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +2 -2
  22. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +2 -2
  23. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +2 -2
  24. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +1 -0
  25. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
  26. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res +1 -1
  27. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res.mjs +1 -2
  28. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +1 -1
  29. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs +1 -1
  30. package/src/adapter/EventCollector/EventCollectorChannel_SQS.res +2 -2
  31. package/src/adapter/EventCollector/EventCollectorChannel_SQS.res.mjs +2 -2
  32. package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res +2 -2
  33. package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res.mjs +2 -2
  34. package/src/adapter/EventLog/EventLogStorage.res +3 -3
  35. package/src/adapter/EventLog/EventLogStorage.res.mjs +3 -3
  36. package/src/adapter/EventLog/EventLogStorage_DynamoDb.res +2 -2
  37. package/src/adapter/EventLog/EventLogStorage_DynamoDb.res.mjs +2 -2
  38. package/src/adapter/EventLog/EventLogStorage_DynamoDbStream.res +2 -2
  39. package/src/adapter/EventLog/EventLogStorage_DynamoDbStream.res.mjs +2 -2
  40. package/src/adapter/EventLog/EventLogStorage_Postgres.res +1 -1
  41. package/src/adapter/EventLog/EventLogStorage_Postgres.res.mjs +1 -1
  42. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +7 -1
  43. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +2 -2
  44. package/src/adapter/EventTopic/EventTopicPublisher_DynamoDbStream.res +1 -1
  45. package/src/adapter/EventTopic/EventTopicPublisher_DynamoDbStream.res.mjs +1 -1
  46. package/src/adapter/EventTopic/EventTopicPublisher_SNS.res +2 -2
  47. package/src/adapter/EventTopic/EventTopicPublisher_SNS.res.mjs +2 -2
  48. package/src/adapter/EventTopic/EventTopicPublisher_SNS_FIFO.res +2 -2
  49. package/src/adapter/EventTopic/EventTopicPublisher_SNS_FIFO.res.mjs +2 -2
  50. package/src/adapter/Mcp/MCP_Lambda.res.mjs +1 -1
  51. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +7 -0
  52. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +4 -2
  53. package/src/adapter/Postgres/PgMigration_Builder.res +1 -0
  54. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -1
  55. package/src/adapter/QueryDb/PgQueryResolver_Builder.res +7 -0
  56. package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +4 -2
  57. package/src/adapter/QueryDb/QueryDbStorage.res +6 -6
  58. package/src/adapter/QueryDb/QueryDbStorage.res.mjs +6 -6
  59. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res +2 -2
  60. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res.mjs +2 -2
  61. package/src/adapter/QueryDb/QueryDbStorage_DynamoDbStream.res +2 -2
  62. package/src/adapter/QueryDb/QueryDbStorage_DynamoDbStream.res.mjs +2 -2
  63. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +1 -1
  64. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +1 -1
  65. package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +24 -41
  66. package/src/adapter/Runtime/AggregateEntryPoint.mjs +52 -45
  67. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res +19 -2
  68. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +8 -5
  69. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res +19 -2
  70. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +8 -5
  71. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res +9 -1
  72. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +4 -2
  73. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res +9 -1
  74. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +4 -2
  75. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +9 -1
  76. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +4 -2
  77. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +9 -1
  78. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +4 -2
  79. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +36 -22
  80. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +1 -0
  81. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +1 -1
  82. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +39 -42
  83. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res +7 -1
  84. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +4 -2
  85. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +19 -1
  86. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +5 -2
  87. package/src/adapter/Runtime/EventMapperEntryPoint.mjs +25 -23
  88. package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +25 -41
  89. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res +1 -0
  90. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
  91. package/src/adapter/Runtime/HandlerFactoryHelpers.mjs +126 -0
  92. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs +24 -41
  93. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +33 -24
  94. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +52 -2
  95. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +25 -7
  96. package/src/adapter/Runtime/SideEffectEntryPoint.mjs +25 -24
  97. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +9 -1
  98. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +4 -2
  99. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +1 -0
  100. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +1 -1
  101. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +26 -21
  102. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +2 -1
  103. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +2 -2
  104. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res +1 -0
  105. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
  106. package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents.res +12 -0
  107. package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents.res.mjs +4 -2
  108. package/src/adapter/StateTopic/StateTopic_AppSync.res +7 -1
  109. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +2 -2
  110. package/src/adapter/Task/TaskBucket_S3.res +1 -0
  111. package/src/adapter/Task/TaskBucket_S3.res.mjs +7 -1
  112. package/src/components/Aggregate_Builder_Micro.res.mjs +2 -0
  113. package/src/components/Aggregate_Builder_NoResolver.res.mjs +2 -0
  114. package/src/components/Aggregate_Builder_PerAggregate.res.mjs +2 -0
  115. package/src/components/Aggregate_Builder_Single.res.mjs +2 -0
  116. package/src/components/Aggregate_Builder_Single_Async.res.mjs +2 -0
  117. package/src/components/Api/AppSync_Adapter.res +12 -0
  118. package/src/components/Api/AppSync_Adapter.res.mjs +6 -2
  119. package/src/components/Api/AppSync_MergedApi.res +12 -0
  120. package/src/components/Api/AppSync_MergedApi.res.mjs +5 -2
  121. package/src/components/AutomationSlice_Builder.res.mjs +2 -0
  122. package/src/components/ExtensionPoint_Builder.res.mjs +2 -0
  123. package/src/components/ForeignReadModel_Builder.res.mjs +4 -0
  124. package/src/components/OutboundTranslationSlice_Builder.res.mjs +2 -0
  125. package/src/components/Plugin.res.mjs +2 -0
  126. package/src/components/ReadModel_Builder_NoResolver.res.mjs +2 -0
  127. package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +2 -0
  128. package/src/components/ReadModel_Builder_PerReadModel.res.mjs +2 -0
  129. package/src/components/ReadModel_Builder_Single.res.mjs +2 -0
  130. package/src/components/ReadModel_Builder_Single_Stream.res.mjs +2 -0
  131. package/src/components/SideEffectHandler_PerSideEffectHandler.res.mjs +6 -0
  132. package/src/components/SideEffectHandler_Single.res.mjs +4 -0
  133. package/src/components/StateViewSlice_Builder.res.mjs +2 -0
  134. package/src/components/StateViewSlice_Builder_Stream.res.mjs +2 -0
  135. package/src/components/Task_Builder_PerBucket.res.mjs +4 -0
  136. package/src/components/Vpc.res +10 -1
  137. package/src/components/Vpc.res.mjs +3 -1
  138. package/src/plugin/cloner/ClonerRunner_Fargate.res +26 -1
  139. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +9 -6
  140. package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res +6 -0
  141. package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res.mjs +4 -1
  142. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +1 -0
  143. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
  144. package/src/plugin/runtime/PluginRuntime_Builder.res +2 -0
  145. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +2 -2
  146. package/src/plugin/stack/Plugin_ExtensionPoint_Builder.res.mjs +4 -0
  147. package/src/plugin/stack/Plugin_Stack.res +26 -1
  148. package/src/plugin/stack/Plugin_Stack.res.mjs +8 -3
  149. package/src/util/Util_CognitoGroupUser.res +6 -0
  150. package/src/util/Util_CognitoGroupUser.res.mjs +3 -1
  151. package/src/util/Util_DeadLetterQueue.res +8 -2
  152. package/src/util/Util_DeadLetterQueue.res.mjs +3 -4
  153. package/src/util/Util_LogAttribution.res +31 -0
  154. package/src/util/Util_LogAttribution.res.mjs +27 -0
  155. package/tests/AWS_TagsTest.res +236 -0
  156. package/tests/AWS_TagsTest.res.mjs +153 -0
  157. package/tests/HandlerFactoryDispatchTest.res +190 -0
  158. package/tests/HandlerFactoryDispatchTest.res.mjs +135 -0
  159. package/tests/integration/AggregateEntryPoint_IntegrationTest.res +5 -3
  160. package/tests/integration/AggregateEntryPoint_IntegrationTest.res.mjs +5 -3
@@ -3,48 +3,22 @@
3
3
  // wires ExtensionPoint_Callback.Make + CommandTopic_Callback.Make,
4
4
  // routes SQS events through handleQueueEvent.
5
5
 
6
- import * as Effect from "effect/Effect";
7
- import { patchSpecId, makeQueueRef, log, pluginName } from "./HandlerFactoryHelpers.mjs";
8
- import { tag as requestContextTag } from "@reventlessdev/reventless-core/src/RequestContext.res.mjs";
6
+ import {
7
+ patchSpecId,
8
+ makeQueueRef,
9
+ log,
10
+ runEffect,
11
+ setRequestId,
12
+ extractMetaField,
13
+ extractSentTimestamp,
14
+ extractRetryCount,
15
+ } from "./HandlerFactoryHelpers.mjs";
9
16
  import { Make as extensionPointCallbackMake } from "@reventlessdev/reventless-core/src/components/ExtensionPoint/ExtensionPoint_Callback.res.mjs";
10
17
  import { Make as commandTopicCallbackMake } from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Callback.res.mjs";
11
18
  import { handleQueueEvent, publishJsons as sqsPublishJsons } from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
12
19
 
13
20
  const dynamicImport = (specifier) => import('/var/task/node_modules/' + specifier);
14
21
 
15
- // Set at handler entry; read by runEffect to tag logs with the Lambda request id.
16
- let _currentRequestId = "unknown";
17
-
18
- function runEffect(correlationId, effect) {
19
- let e = effect
20
- // Promote correlationId/requestId/plugin to top-level JSON log fields —
21
- // decoded by EffectLogger.install() from Effect log annotations. Harmless
22
- // no-op if the unified logger isn't installed.
23
- .pipe(Effect.annotateLogs("correlationId", correlationId || "unknown"))
24
- .pipe(Effect.annotateLogs("requestId", _currentRequestId));
25
- if (pluginName) e = e.pipe(Effect.annotateLogs("plugin", pluginName));
26
- return e
27
- .pipe(Effect.provideService(requestContextTag, { correlationId: correlationId || "unknown" }))
28
- .pipe(Effect.runPromise);
29
- }
30
-
31
- function extractCorrelationId(records) {
32
- const first = records[0];
33
- if (!first) return undefined;
34
- const body = first.body;
35
- if (!body) return undefined;
36
- try {
37
- const parsed = JSON.parse(body);
38
- if (typeof parsed === "object" && parsed !== null) {
39
- const meta = parsed.meta;
40
- if (meta && typeof meta === "object" && typeof meta.correlationId === "string") {
41
- return meta.correlationId;
42
- }
43
- }
44
- } catch (_) {}
45
- return undefined;
46
- }
47
-
48
22
  async function buildHandler() {
49
23
  const configStr = process.env["HANDLER_CONFIG"] || "{}";
50
24
  const config = JSON.parse(configStr);
@@ -84,18 +58,28 @@ async function buildHandler() {
84
58
  });
85
59
 
86
60
  const resolvedQueue = makeQueueRef(config.queueUrl);
87
- return handleQueueEvent(resolvedQueue, commandTopicCallback.handleJsonCommands);
61
+ // Pair the handler with the extension point it serves so the invocation's log
62
+ // lines name the component, not just this Lambda.
63
+ return {
64
+ sqsHandler: handleQueueEvent(resolvedQueue, commandTopicCallback.handleJsonCommands),
65
+ comp: `ExtensionPoint(${patchedSpec.name})`,
66
+ };
88
67
  }
89
68
 
90
69
  const initPromise = buildHandler();
91
70
 
92
71
  export async function handler(event, context) {
93
- _currentRequestId = context?.awsRequestId || "unknown";
94
- const sqsHandler = await initPromise;
72
+ setRequestId(context?.awsRequestId);
73
+ const { sqsHandler, comp } = await initPromise;
95
74
  const records = event.Records || [];
96
- const correlationId = extractCorrelationId(records);
97
75
 
98
76
  log.debug("processing " + records.length.toString() + " record(s)", { comp: "ExtensionPointRuntime" });
99
- await runEffect(correlationId, sqsHandler(event, context));
77
+ await runEffect(sqsHandler(event, context), {
78
+ correlationId: extractMetaField(records, "correlationId"),
79
+ causationId: extractMetaField(records, "causationId"),
80
+ comp,
81
+ timestamp: extractSentTimestamp(records),
82
+ retryCount: extractRetryCount(records),
83
+ });
100
84
  return "";
101
85
  }
@@ -71,6 +71,7 @@ let forCommandTopic = (
71
71
  let runtime = RuntimeEnvironment_Lambda.makeFromCodeAsset(
72
72
  ~name,
73
73
  ~unitKind=ReventlessCore.Monitoring.CommandHandler,
74
+ ~componentKind=ReventlessCore.ComponentType.ExtensionPoint,
74
75
  ~code,
75
76
  ~sourceCodeHash,
76
77
  ~envVars,
@@ -39,7 +39,7 @@ function forCommandTopic(param, connect, memorySizeOpt, timeoutOpt, specModuleUr
39
39
  packageDirs[specPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(specPkg);
40
40
  packageDirs[mappingsPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(mappingsPkg);
41
41
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/ExtensionPointEntryPoint.mjs", packageDirs, undefined);
42
- return connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, "CommandHandler", match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts));
42
+ return connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, "CommandHandler", "ExtensionPoint", match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts));
43
43
  }
44
44
 
45
45
  function finish() {
@@ -1,6 +1,11 @@
1
1
  // Shared runtime utilities for compiled Lambda entry points.
2
2
 
3
+ import * as Effect from "effect/Effect";
3
4
  import { $$String as IdString } from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
5
+ import {
6
+ tag as requestContextTag,
7
+ fromOptions as makeRequestContext,
8
+ } from "@reventlessdev/reventless-core/src/RequestContext.res.mjs";
4
9
  import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
5
10
  import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
6
11
 
@@ -66,6 +71,127 @@ export const log = {
66
71
  // Lambdas). Export so shim runEffect wrappers can annotate Effect logs with it.
67
72
  export const pluginName = PLUGIN_NAME;
68
73
 
74
+ // ─── Dispatch boundary for the entry-point shells ───────────────────────────
75
+ // The deployed Lambda is an archive whose index.handler is one of the .mjs entry
76
+ // points here — the AWS runtime builders take `~handler as _` and discard the
77
+ // ReScript closure, so ReventlessCore.Runtime.annotateInvocation (the ReScript
78
+ // dispatch boundary, used by reventless-local) never runs in a Lambda. This is
79
+ // its counterpart: the single place where every deployed invocation gets its log
80
+ // annotations and its RequestContext, so `comp` / `causationId` attribution is
81
+ // the same on both platforms.
82
+ //
83
+ // Options object rather than positional arguments: the shells call this from
84
+ // twelve sites and the field set grows, so a new field must not be able to
85
+ // shift an existing one. Same reason RequestContext exposes `fromOptions` —
86
+ // ReScript labelled arguments compile to positional ones.
87
+ //
88
+ // See docs/plans/entrypoint-dispatch-parity-and-latency-fields.md.
89
+
90
+ // Set at handler entry via setRequestId; read on each dispatch to tag logs with
91
+ // the Lambda request id.
92
+ let _currentRequestId = "unknown";
93
+
94
+ export function setRequestId(id) {
95
+ _currentRequestId = id || "unknown";
96
+ }
97
+
98
+ export function runEffect(effect, opts = {}) {
99
+ const { correlationId, causationId, comp, timestamp, retryCount, plugin } = opts;
100
+ const cid = correlationId || "unknown";
101
+ // `plugin` per dispatch (from HANDLER_CONFIG, resolved at deploy time) beats
102
+ // the Lambda-name-derived fallback: a shared Lambda (AllReadModels,
103
+ // AllAggregatesCmdHandler) hosts components from several plugins, so its name
104
+ // carries no single plugin identity.
105
+ const resolvedPlugin = plugin || pluginName;
106
+ let e = effect
107
+ // Promote correlationId/requestId/plugin/comp/causationId to top-level JSON
108
+ // log fields — decoded by EffectLogger.install() from Effect log
109
+ // annotations. Harmless no-op if the unified logger isn't installed.
110
+ .pipe(Effect.annotateLogs("correlationId", cid))
111
+ .pipe(Effect.annotateLogs("requestId", _currentRequestId));
112
+ if (resolvedPlugin) e = e.pipe(Effect.annotateLogs("plugin", resolvedPlugin));
113
+ if (comp) e = e.pipe(Effect.annotateLogs("comp", comp));
114
+ if (causationId) e = e.pipe(Effect.annotateLogs("causationId", causationId));
115
+ // Only annotate a redelivery. A constant "1" on every line is noise, and its
116
+ // absence is what makes `filter retryCount > 1` a usable query.
117
+ if (retryCount > 1) e = e.pipe(Effect.annotateLogs("retryCount", String(retryCount)));
118
+ return e
119
+ .pipe(
120
+ Effect.provideService(
121
+ requestContextTag,
122
+ makeRequestContext({
123
+ correlationId: cid,
124
+ causationId,
125
+ component: comp,
126
+ pluginName: resolvedPlugin || undefined,
127
+ timestamp,
128
+ retryCount,
129
+ }),
130
+ ),
131
+ )
132
+ .pipe(Effect.runPromise);
133
+ }
134
+
135
+ // ─── Message-envelope + transport-attribute extraction ──────────────────────
136
+ // Mirrors RuntimeEnvironment_Lambda.extractMetaField (ReScript side, same
137
+ // position in the batch) so both dispatch boundaries read the same fields.
138
+
139
+ // Read one envelope meta field (correlationId, causationId, …) off the first
140
+ // record. Two transports, two shapes:
141
+ // SQS — the envelope is a JSON `body` string with a nested `meta`.
142
+ // DynamoDB stream — the EventLog row stores meta keys *flat* as top-level
143
+ // attributes (Message.composeMeta rebuilds `meta` from them),
144
+ // so read NewImage.<field>.S.
145
+ // Undefined when neither shape yields a string.
146
+ export function extractMetaField(records, field) {
147
+ const first = (records || [])[0];
148
+ if (!first) return undefined;
149
+ if (typeof first.body === "string") {
150
+ let parsed;
151
+ try {
152
+ parsed = JSON.parse(first.body);
153
+ } catch (_) {
154
+ return undefined;
155
+ }
156
+ const value = parsed?.meta?.[field];
157
+ return typeof value === "string" ? value : undefined;
158
+ }
159
+ const attr = first.dynamodb?.NewImage?.[field];
160
+ return typeof attr?.S === "string" ? attr.S : undefined;
161
+ }
162
+
163
+ // Send time of the triggering message, ms since epoch. SQS stamps
164
+ // `SentTimestamp` (ms, server-side clock — not the producer's); a DynamoDB
165
+ // stream record carries `ApproximateCreationDateTime` in *seconds*. Undefined
166
+ // when neither is present, so a handler can tell "no send time" from "zero
167
+ // latency".
168
+ export function extractSentTimestamp(records) {
169
+ const first = (records || [])[0];
170
+ if (!first) return undefined;
171
+ const sqsSent = first.attributes?.SentTimestamp;
172
+ if (sqsSent !== undefined) {
173
+ const ms = Number(sqsSent);
174
+ return Number.isFinite(ms) ? ms : undefined;
175
+ }
176
+ const streamCreated = first.dynamodb?.ApproximateCreationDateTime;
177
+ if (streamCreated !== undefined) {
178
+ const secs = Number(streamCreated);
179
+ return Number.isFinite(secs) ? secs * 1000 : undefined;
180
+ }
181
+ return undefined;
182
+ }
183
+
184
+ // Delivery attempt: 1 on first delivery. SQS counts redeliveries in
185
+ // `ApproximateReceiveCount`; DynamoDB streams have no equivalent, so a stream
186
+ // record reports 1.
187
+ export function extractRetryCount(records) {
188
+ const first = (records || [])[0];
189
+ const raw = first?.attributes?.ApproximateReceiveCount;
190
+ if (raw === undefined) return 1;
191
+ const count = parseInt(raw, 10);
192
+ return Number.isFinite(count) && count > 0 ? count : 1;
193
+ }
194
+
69
195
  // Patch a spec module's Id field to use IdString if undefined.
70
196
  // Workaround for `module Id = Id.String` not producing an ESM export.
71
197
  export const patchSpecId = (specModule) => ({ ...specModule, Id: specModule.Id || IdString });
@@ -2,9 +2,17 @@
2
2
  // All imports are framework packages — no user modules needed.
3
3
  // Handles Heartbeat, Cloner, and other plugin-level extension point commands.
4
4
 
5
- import * as Effect from "effect/Effect";
6
- import { patchSpecId, makeQueueRef, scanByTableName, log, pluginName } from "./HandlerFactoryHelpers.mjs";
7
- import { tag as requestContextTag } from "@reventlessdev/reventless-core/src/RequestContext.res.mjs";
5
+ import {
6
+ patchSpecId,
7
+ makeQueueRef,
8
+ scanByTableName,
9
+ log,
10
+ runEffect,
11
+ setRequestId,
12
+ extractMetaField,
13
+ extractSentTimestamp,
14
+ extractRetryCount,
15
+ } from "./HandlerFactoryHelpers.mjs";
8
16
  import { sendMessage } from "@reventlessdev/reventless-aws/src/plugin/runtime/Util_PluginMessage_Runtime.res.mjs";
9
17
  import * as PluginExtensionPointSpec from "@reventlessdev/reventless-infra/src/types/PluginExtensionPointSpec.res.mjs";
10
18
  import { Make as pluginEPPluginMake } from "@reventlessdev/reventless-core/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs";
@@ -13,39 +21,6 @@ import { Make as commandTopicCallbackMake } from "@reventlessdev/reventless-core
13
21
  import { handleQueueEvent, publishJsons as sqsPublishJsons } from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
14
22
  import { createSchedule as cwCreateSchedule, deleteSchedule as cwDeleteSchedule } from "@reventlessdev/reventless-aws/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents_Runtime.res.mjs";
15
23
 
16
- // Set at handler entry; read by runEffect to tag logs with the Lambda request id.
17
- let _currentRequestId = "unknown";
18
-
19
- function runEffect(correlationId, effect) {
20
- let e = effect
21
- // Promote correlationId/requestId/plugin to top-level JSON log fields —
22
- // decoded by EffectLogger.install() from Effect log annotations. Harmless
23
- // no-op if the unified logger isn't installed.
24
- .pipe(Effect.annotateLogs("correlationId", correlationId || "unknown"))
25
- .pipe(Effect.annotateLogs("requestId", _currentRequestId));
26
- if (pluginName) e = e.pipe(Effect.annotateLogs("plugin", pluginName));
27
- return e
28
- .pipe(Effect.provideService(requestContextTag, { correlationId: correlationId || "unknown" }))
29
- .pipe(Effect.runPromise);
30
- }
31
-
32
- function extractCorrelationId(records) {
33
- const first = records[0];
34
- if (!first) return undefined;
35
- const body = first.body;
36
- if (!body) return undefined;
37
- try {
38
- const parsed = JSON.parse(body);
39
- if (typeof parsed === "object" && parsed !== null) {
40
- const meta = parsed.meta;
41
- if (meta && typeof meta === "object" && typeof meta.correlationId === "string") {
42
- return meta.correlationId;
43
- }
44
- }
45
- } catch (_) {}
46
- return undefined;
47
- }
48
-
49
24
  function buildHandler() {
50
25
  const configStr = process.env["HANDLER_CONFIG"] || "{}";
51
26
  const config = JSON.parse(configStr);
@@ -121,17 +96,25 @@ function buildHandler() {
121
96
  commandsHandler: callback.handleIncomingCommands,
122
97
  });
123
98
 
124
- return handleQueueEvent(makeQueueRef(config.queueUrl), commandTopicCallback.handleJsonCommands);
99
+ return {
100
+ sqsHandler: handleQueueEvent(makeQueueRef(config.queueUrl), commandTopicCallback.handleJsonCommands),
101
+ comp: `ExtensionPoint(${patchedSpec.name})`,
102
+ };
125
103
  }
126
104
 
127
- const sqsHandler = buildHandler();
105
+ const { sqsHandler, comp } = buildHandler();
128
106
 
129
107
  export async function handler(event, context) {
130
- _currentRequestId = context?.awsRequestId || "unknown";
108
+ setRequestId(context?.awsRequestId);
131
109
  const records = event.Records || [];
132
- const correlationId = extractCorrelationId(records);
133
110
 
134
111
  log.debug("processing " + records.length.toString() + " record(s)", { comp: "PluginExtensionPointRuntime" });
135
- await runEffect(correlationId, sqsHandler(event, context));
112
+ await runEffect(sqsHandler(event, context), {
113
+ correlationId: extractMetaField(records, "correlationId"),
114
+ causationId: extractMetaField(records, "causationId"),
115
+ comp,
116
+ timestamp: extractSentTimestamp(records),
117
+ retryCount: extractRetryCount(records),
118
+ });
136
119
  return "";
137
120
  }
@@ -2,9 +2,15 @@
2
2
  // At cold start: reads HANDLER_CONFIG, dynamically imports Spec/Mappings modules,
3
3
  // wires ReadModel_Callback.Make, builds handler map keyed by source URN.
4
4
 
5
- import * as Effect from "effect/Effect";
6
- import { patchSpecId, log, pluginName } from "./HandlerFactoryHelpers.mjs";
7
- import { tag as requestContextTag } from "@reventlessdev/reventless-core/src/RequestContext.res.mjs";
5
+ import {
6
+ patchSpecId,
7
+ log,
8
+ runEffect,
9
+ setRequestId,
10
+ extractMetaField,
11
+ extractSentTimestamp,
12
+ extractRetryCount,
13
+ } from "./HandlerFactoryHelpers.mjs";
8
14
  import { Make as readModelCallbackMake } from "@reventlessdev/reventless-core/src/components/ReadModel/ReadModel_Callback.res.mjs";
9
15
  // Typed cold-start core — DynamoDB QueryDb ops, compiler-checked against the
10
16
  // framework signatures (see the module header and
@@ -48,22 +54,6 @@ function fixMappingsModule(mod) {
48
54
  return mod;
49
55
  }
50
56
 
51
- // Set at handler entry; read by runEffect to tag logs with the Lambda request id.
52
- let _currentRequestId = "unknown";
53
-
54
- function runEffect(correlationId, effect) {
55
- let e = effect
56
- // Promote correlationId/requestId/plugin to top-level JSON log fields —
57
- // decoded by EffectLogger.install() from Effect log annotations. Harmless
58
- // no-op if the unified logger isn't installed.
59
- .pipe(Effect.annotateLogs("correlationId", correlationId || "unknown"))
60
- .pipe(Effect.annotateLogs("requestId", _currentRequestId));
61
- if (pluginName) e = e.pipe(Effect.annotateLogs("plugin", pluginName));
62
- return e
63
- .pipe(Effect.provideService(requestContextTag, { correlationId: correlationId || "unknown" }))
64
- .pipe(Effect.runPromise);
65
- }
66
-
67
57
  function groupBySource(records) {
68
58
  const dict = {};
69
59
  records.forEach(record => {
@@ -130,13 +120,20 @@ async function buildAllHandlers() {
130
120
  const specModule = await dynamicImport(h.specModule);
131
121
  const mappingsModule = await dynamicImport(h.mappingsModule);
132
122
  const handler = buildReadModelHandler(specModule, mappingsModule, h.queryDbTableName, h.pgConnection, h.stateTopicName);
123
+ // `comp` / `plugin` are resolved at deploy time and baked into
124
+ // HANDLER_CONFIG (EventCollectorRuntime_Builder_Single) so the shell doesn't
125
+ // re-derive component identity from module paths, and so the string matches
126
+ // the ReScript dispatch boundary's `EventCollector(<Name>)` byte for byte.
127
+ // Kept next to the handler: this Lambda hosts every read model, so the comp
128
+ // is what separates their log lines.
129
+ const registered = { handler, comp: h.comp, plugin: h.plugin };
133
130
  // Multiple read models can share one source stream — e.g. every admin read
134
131
  // model (Plugins, PluginHistory, PlatformEventGraph, UIFragmentRegistry)
135
132
  // projects the Plugin aggregate's EventLog stream. Accumulate ALL handlers
136
133
  // per source ARN; a plain `handlers[urn] = handler` collapses them to one
137
134
  // (whichever async builder wins the Promise.all race), silently dropping the
138
135
  // rest — which leaves their QueryDbs empty.
139
- (handlers[h.sourceUrn] ||= []).push(handler);
136
+ (handlers[h.sourceUrn] ||= []).push(registered);
140
137
  }));
141
138
 
142
139
  return handlers;
@@ -145,7 +142,7 @@ async function buildAllHandlers() {
145
142
  const initPromise = buildAllHandlers();
146
143
 
147
144
  export async function handler(event, context) {
148
- _currentRequestId = context?.awsRequestId || "unknown";
145
+ setRequestId(context?.awsRequestId);
149
146
  const handlers = await initPromise;
150
147
  const records = event.Records || [];
151
148
  const grouped = groupBySource(records);
@@ -154,11 +151,23 @@ export async function handler(event, context) {
154
151
  const streamHandlers = handlers[arn];
155
152
  if (streamHandlers !== undefined && streamHandlers.length > 0) {
156
153
  log.debug("found " + streamHandlers.length + " handler(s) for " + arn, { comp: "ReadModelRuntime" });
154
+ const correlationId = extractMetaField(subRecords, "correlationId");
155
+ const causationId = extractMetaField(subRecords, "causationId");
156
+ const timestamp = extractSentTimestamp(subRecords);
157
+ const retryCount = extractRetryCount(subRecords);
157
158
  // Run every read model registered for this source stream (independent
158
- // QueryDbs, so concurrent is safe).
159
+ // QueryDbs, so concurrent is safe). Each carries its own comp so the
160
+ // shared Lambda's log lines stay separable per read model.
159
161
  await Promise.all(
160
- streamHandlers.map(streamHandler =>
161
- runEffect(undefined, streamHandler({ Records: subRecords }, context))
162
+ streamHandlers.map(({ handler: streamHandler, comp, plugin }) =>
163
+ runEffect(streamHandler({ Records: subRecords }, context), {
164
+ correlationId,
165
+ causationId,
166
+ comp,
167
+ plugin,
168
+ timestamp,
169
+ retryCount,
170
+ })
162
171
  )
163
172
  );
164
173
  } else {
@@ -33,7 +33,9 @@ let make: ReventlessCore.Runtime.environmentMaker<'event, context, 'result, part
33
33
  ~opts?,
34
34
  )
35
35
 
36
- let tags = AWS.Tags.make(~name, ReventlessCore.CommandTopic.componentType)
36
+ // Legacy path (see above): no component kind reaches here because nothing calls
37
+ // it, so it is attributed to the Platform rather than to an invented domain kind.
38
+ let tags = AWS.Tags.make(~name, ~kind=ReventlessCore.ComponentType.Platform, ~role=Runtime, ~scope=Platform)
37
39
  let lambda =
38
40
  handler->Pulumi.Output.apply(handler =>
39
41
  Lambda.CallbackFunction.make(
@@ -70,6 +72,13 @@ let makeFromCodeAsset: (
70
72
  kind; `makeFromCodeAsset` announces the provisioned Lambda to the
71
73
  `Monitoring` registry (no-op unless an extension registered a backend). */
72
74
  ~unitKind: ReventlessCore.Monitoring.unitKind,
75
+ /** The modelling kind of the component this Lambda runs, for resource
76
+ attribution (`reventless:kind`). Distinct from `~unitKind`, which grades
77
+ the unit by what its failure means — `CommandHandler` covers aggregates,
78
+ state-change slices and extension points alike, so it cannot identify the
79
+ owning component. Shared Lambdas that host several components of one kind
80
+ (e.g. AllAggregates) pass that kind. */
81
+ ~componentKind: ReventlessCore.ComponentType.t,
73
82
  ~code: Pulumi.Archive.t,
74
83
  ~sourceCodeHash: string,
75
84
  ~envVars: dict<Pulumi.Input.t<string>>=?,
@@ -91,6 +100,7 @@ let makeFromCodeAsset: (
91
100
  ) => ReventlessCore.Runtime.environment<parts> = (
92
101
  ~name,
93
102
  ~unitKind,
103
+ ~componentKind,
94
104
  ~code,
95
105
  ~sourceCodeHash,
96
106
  ~envVars=Dict.make(),
@@ -107,9 +117,17 @@ let makeFromCodeAsset: (
107
117
  let opts =
108
118
  opts->Option.map(ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions)
109
119
 
120
+ // One attribution identity for every resource provisioned here: same owning
121
+ // component and kind throughout, distinct piece roles — the function itself,
122
+ // the execution identity it assumes, and its log group are three pieces of one
123
+ // component, and an inventory needs to roll all three up to it.
124
+ let tagsFor = (~resourceName, ~role) =>
125
+ AWS.Tags.make(~name=resourceName, ~kind=componentKind, ~role, ~component=name)
126
+
110
127
  let lambdaRole = IAM.Role.makeWithDefaultPolicy(
111
128
  ~name,
112
129
  ~servicePrincipal=AWS.Lambda.principal->Pulumi.Output.make,
130
+ ~tags=tagsFor(~resourceName=name, ~role=Identity),
113
131
  ~opts?,
114
132
  )
115
133
 
@@ -195,7 +213,7 @@ let makeFromCodeAsset: (
195
213
  variables->Dict.set("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make)
196
214
  variables->Dict.set("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make)
197
215
 
198
- let tags = AWS.Tags.make(~name, ReventlessCore.CommandTopic.componentType)
216
+ let tags = tagsFor(~resourceName=name, ~role=Runtime)
199
217
  let lambda = Lambda.Function.make(
200
218
  ~name,
201
219
  ~args={
@@ -227,6 +245,7 @@ let makeFromCodeAsset: (
227
245
  ~args={
228
246
  name: `/aws/lambda/${name}`->Pulumi.Input.make,
229
247
  retentionInDays: days->Pulumi.Input.make,
248
+ tags: tagsFor(~resourceName=`${name}LogGroup`, ~role=Logs),
230
249
  },
231
250
  ~opts?,
232
251
  )
@@ -305,6 +324,37 @@ let extractMetaField = (event: event, field) =>
305
324
  let extractCorrelationId = (event: event) => event->extractMetaField("correlationId")
306
325
  let extractCausationId = (event: event) => event->extractMetaField("causationId")
307
326
 
327
+ // SQS attributes ride on the record but are absent from the minimal record type
328
+ // (see the commented-out `attributes` field in SQS_Queue.res). Read them the same
329
+ // way `recordBody` reads `body`: as a typed getter over the untyped record.
330
+ type recordAttributes = {
331
+ @as("SentTimestamp") sentTimestamp?: string,
332
+ @as("ApproximateReceiveCount") approximateReceiveCount?: string,
333
+ }
334
+ @get
335
+ external recordAttributes: PulumiAws.Lambda.CallbackFunction.record => option<recordAttributes> =
336
+ "attributes"
337
+
338
+ let firstRecordAttributes = (event: event) =>
339
+ event.records->Array.get(0)->Option.flatMap(recordAttributes)
340
+
341
+ // Send time of the triggering message, ms since epoch. SQS stamps
342
+ // `SentTimestamp` server-side (not the producer's clock). DynamoDB-stream
343
+ // sources carry `ApproximateCreationDateTime` instead and are dispatched through
344
+ // the entry-point shells, so None here rather than a fabricated value.
345
+ let extractSentTimestamp = (event: event) =>
346
+ event
347
+ ->firstRecordAttributes
348
+ ->Option.flatMap(attrs => attrs.sentTimestamp)
349
+ ->Option.flatMap(Float.fromString)
350
+
351
+ // Delivery attempt, 1 on first delivery.
352
+ let extractRetryCount = (event: event) =>
353
+ event
354
+ ->firstRecordAttributes
355
+ ->Option.flatMap(attrs => attrs.approximateReceiveCount)
356
+ ->Option.flatMap(Int.fromString(_))
357
+
308
358
  external asEventHandler: 'a => ReventlessCore.Runtime.eventHandler<event, context, 'result> =
309
359
  "%identity"
310
360
  external asEffectHandler: 'a => ReventlessCore.Runtime.effectHandler<
@@ -1,8 +1,10 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
3
4
  import * as Aws from "@pulumi/aws";
4
5
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
6
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
7
+ import * as Stdlib_Float from "@rescript/runtime/lib/es6/Stdlib_Float.js";
6
8
  import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
7
9
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
10
  import * as Pulumi from "@pulumi/pulumi";
@@ -17,7 +19,6 @@ import * as Monitoring$ReventlessCore from "@reventlessdev/reventless-core/src/a
17
19
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
18
20
  import * as Util_Lambda$ReventlessAws from "../../util/Util_Lambda.res.mjs";
19
21
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
20
- import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
21
22
  import * as Util_IAM_Role$ReventlessAws from "../../util/Util_IAM_Role.res.mjs";
22
23
 
23
24
  let additionalEnvVars = {};
@@ -28,8 +29,8 @@ function make(name, handler, memorySizeOpt, timeoutOpt, opts) {
28
29
  let memorySize = memorySizeOpt !== undefined ? memorySizeOpt : 1024;
29
30
  let timeout = timeoutOpt !== undefined ? timeoutOpt : 30;
30
31
  let opts$1 = Stdlib_Option.map(opts, Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions);
31
- let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name, Pulumi.output(AWS$ReventlessAws.Lambda.principal), opts$1);
32
- let tags = AWS_Tags$ReventlessAws.make(name, CommandTopic$ReventlessCore.componentType);
32
+ let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name, Pulumi.output(AWS$ReventlessAws.Lambda.principal), undefined, opts$1);
33
+ let tags = AWS_Tags$ReventlessAws.make(name, "Platform", "Runtime", "Platform", undefined, undefined, undefined, undefined);
33
34
  let lambda = handler.apply(handler => new (Aws.lambda.CallbackFunction)(name, Lambda$PulumiAws.CallbackFunction.Args.make(handler, lambdaRole, undefined, undefined, undefined, memorySize, timeout, undefined, undefined, undefined, tags, undefined), opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined));
34
35
  return {
35
36
  parts: {
@@ -43,13 +44,14 @@ function make(name, handler, memorySizeOpt, timeoutOpt, opts) {
43
44
  };
44
45
  }
45
46
 
46
- function makeFromCodeAsset(name, unitKind, code, sourceCodeHash, envVarsOpt, memorySizeOpt, timeoutOpt, reservedConcurrency, ephemeralStorageMb, logRetentionDays, dcbMetricsOpt, vpcConfig, opts) {
47
+ function makeFromCodeAsset(name, unitKind, componentKind, code, sourceCodeHash, envVarsOpt, memorySizeOpt, timeoutOpt, reservedConcurrency, ephemeralStorageMb, logRetentionDays, dcbMetricsOpt, vpcConfig, opts) {
47
48
  let envVars = envVarsOpt !== undefined ? envVarsOpt : ({});
48
49
  let memorySize = memorySizeOpt !== undefined ? memorySizeOpt : Runtime$ReventlessCore.CommandHandlerDefaults.memorySize;
49
50
  let timeout = timeoutOpt !== undefined ? timeoutOpt : Runtime$ReventlessCore.CommandHandlerDefaults.timeout;
50
51
  let dcbMetrics = dcbMetricsOpt !== undefined ? dcbMetricsOpt : false;
51
52
  let opts$1 = Stdlib_Option.map(opts, Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions);
52
- let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name, Pulumi.output(AWS$ReventlessAws.Lambda.principal), opts$1);
53
+ let tagsFor = (resourceName, role) => AWS_Tags$ReventlessAws.make(resourceName, componentKind, role, undefined, name, undefined, undefined, undefined);
54
+ let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name, Pulumi.output(AWS$ReventlessAws.Lambda.principal), tagsFor(name, "Identity"), opts$1);
53
55
  Stdlib_Option.forEach(vpcConfig, param => {
54
56
  new (Aws.iam.RolePolicy)(name + `VpcAccess`, {
55
57
  policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `VpcAccessPolicy`, [{
@@ -95,7 +97,7 @@ function makeFromCodeAsset(name, unitKind, code, sourceCodeHash, envVarsOpt, mem
95
97
  });
96
98
  variables["NODE_OPTIONS"] = Util_Bundle$ReventlessAws.esmLoaderNodeOptions;
97
99
  variables["ESM_FALLBACK_DIRS"] = Util_Bundle$ReventlessAws.esmFallbackDirs;
98
- let tags = AWS_Tags$ReventlessAws.make(name, CommandTopic$ReventlessCore.componentType);
100
+ let tags = tagsFor(name, "Runtime");
99
101
  let lambda = new (Aws.lambda.Function)(name, {
100
102
  handler: "index.handler",
101
103
  runtime: "nodejs22.x",
@@ -118,7 +120,8 @@ function makeFromCodeAsset(name, unitKind, code, sourceCodeHash, envVarsOpt, mem
118
120
  Stdlib_Option.forEach(logRetentionDays, days => {
119
121
  let logGroup = new (Aws.cloudwatch.LogGroup)(name + `LogGroup`, {
120
122
  name: `/aws/lambda/` + name,
121
- retentionInDays: days
123
+ retentionInDays: days,
124
+ tags: tagsFor(name + `LogGroup`, "Logs")
122
125
  }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
123
126
  if (dcbMetrics) {
124
127
  [
@@ -190,6 +193,18 @@ function extractCausationId(event) {
190
193
  return extractMetaField(event, "causationId");
191
194
  }
192
195
 
196
+ function firstRecordAttributes(event) {
197
+ return Stdlib_Option.flatMap(event.Records[0], prim => prim.attributes);
198
+ }
199
+
200
+ function extractSentTimestamp(event) {
201
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(firstRecordAttributes(event), attrs => attrs.SentTimestamp), Stdlib_Float.fromString);
202
+ }
203
+
204
+ function extractRetryCount(event) {
205
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(firstRecordAttributes(event), attrs => attrs.ApproximateReceiveCount), __x => Stdlib_Int.fromString(__x, undefined));
206
+ }
207
+
193
208
  export {
194
209
  additionalEnvVars,
195
210
  additionalIamPolicies,
@@ -199,5 +214,8 @@ export {
199
214
  extractMetaField,
200
215
  extractCorrelationId,
201
216
  extractCausationId,
217
+ firstRecordAttributes,
218
+ extractSentTimestamp,
219
+ extractRetryCount,
202
220
  }
203
221
  /* @pulumi/aws Not a pure module */