@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.
- package/CHANGELOG.md +23 -0
- package/package.json +7 -7
- package/src/adapter/Api/AppSyncEventsSigner_Ops.res +137 -0
- package/src/adapter/Api/AppSyncEventsSigner_Ops.res.mjs +121 -0
- package/src/adapter/Api/Platform_AdminScan_Ops.res +136 -0
- package/src/adapter/Api/Platform_AdminScan_Ops.res.mjs +98 -0
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +12 -90
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +11 -84
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +86 -0
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +79 -0
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +9 -79
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +7 -143
- package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res +48 -0
- package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res.mjs +63 -0
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +21 -80
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +23 -68
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res +70 -0
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res.mjs +68 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -1
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +1 -1
- package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
- package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -1
- package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +42 -126
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +246 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +145 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +27 -2
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +10 -3
- package/src/adapter/Runtime/CounterEntryPoint.mjs +19 -73
- package/src/adapter/Runtime/CounterEntryPoint_Ops.res +145 -0
- package/src/adapter/Runtime/CounterEntryPoint_Ops.res.mjs +129 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +16 -3
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +11 -6
- package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +52 -510
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +816 -0
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +651 -0
- package/src/adapter/Runtime/EventMapperEntryPoint.mjs +23 -67
- package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +126 -0
- package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res.mjs +83 -0
- package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +16 -63
- package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +150 -0
- package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res.mjs +111 -0
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res +96 -0
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res.mjs +70 -0
- package/src/adapter/Runtime/PgMigrationEntryPoint.res +54 -0
- package/src/adapter/Runtime/PgMigrationEntryPoint.res.mjs +37 -0
- package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +20 -55
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +155 -0
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +94 -0
- package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +80 -0
- package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +39 -0
- package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +4 -4
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +34 -153
- package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +114 -0
- package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res.mjs +83 -0
- package/src/adapter/Runtime/SideEffectEntryPoint.mjs +23 -64
- package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res +71 -0
- package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res.mjs +57 -0
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +34 -165
- package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +206 -0
- package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs +105 -0
- package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +156 -0
- package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res.mjs +123 -0
- package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +18 -138
- package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +198 -0
- package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res.mjs +155 -0
- package/src/adapter/StateTopic/StateTopic_AppSync.res +12 -170
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +7 -167
- package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +208 -0
- package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +183 -0
- package/src/components/AutomationSlice_Builder.res +2 -0
- package/src/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/OutboundTranslationSlice_Builder.res +1 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
- package/tests/AppSyncEventsSigner_OpsTest.res +75 -0
- package/tests/AppSyncEventsSigner_OpsTest.res.mjs +44 -0
- package/tests/AppSyncEventsSigner_ParityFixture.mjs +36 -0
- package/tests/AutomationSliceEntryPoint_OpsTest.res +165 -0
- package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +280 -0
- package/tests/CounterEntryPoint_OpsTest.res +115 -0
- package/tests/CounterEntryPoint_OpsTest.res.mjs +150 -0
- package/tests/EventCollectorEntryPoint_OpsTest.res +216 -0
- package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +339 -0
- package/tests/EventMapperEntryPoint_OpsTest.res +103 -0
- package/tests/EventMapperEntryPoint_OpsTest.res.mjs +120 -0
- package/tests/ExtensionPointEntryPoint_OpsTest.res +57 -0
- package/tests/ExtensionPointEntryPoint_OpsTest.res.mjs +54 -0
- package/tests/PgMigrationEntryPoint_IntegrationTest.res +22 -31
- package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +20 -23
- package/tests/PgQueryResolverEntryPoint_OpsTest.res +38 -0
- package/tests/PgQueryResolverEntryPoint_OpsTest.res.mjs +31 -0
- package/tests/Platform_AdminScan_OpsTest.res +80 -0
- package/tests/Platform_AdminScan_OpsTest.res.mjs +127 -0
- package/tests/ReadModelEntryPoint_OpsTest.res +141 -0
- package/tests/ReadModelEntryPoint_OpsTest.res.mjs +267 -0
- package/tests/SideEffectEntryPoint_OpsTest.res +79 -0
- package/tests/SideEffectEntryPoint_OpsTest.res.mjs +105 -0
- package/tests/StateViewSliceEntryPoint_OpsTest.res +167 -0
- package/tests/StateViewSliceEntryPoint_OpsTest.res.mjs +296 -0
- package/tests/TaskBucketEntryPoint_OpsTest.res +117 -0
- package/tests/TaskBucketEntryPoint_OpsTest.res.mjs +165 -0
- package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res +42 -13
- package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res.mjs +29 -6
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +0 -57
- package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +0 -40
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
// per-plugin EventCollector Lambda. This module is plugin-agnostic: behaviour is
|
|
3
3
|
// driven entirely by HANDLER_CONFIG.
|
|
4
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 EP/extension/spec
|
|
8
|
+
// modules named in HANDLER_CONFIG and the functor applications consuming those
|
|
9
|
+
// runtime-loaded modules. Everything else (config parse/validation, the Plugin-RM
|
|
10
|
+
// projection, the cross-plugin SNS subscription manager, publish/query/scheduler
|
|
11
|
+
// operations, handler-dict assembly, Plugin_Callback wiring, dispatch boundary)
|
|
12
|
+
// lives type-checked in EventCollectorEntryPoint_Ops.res.
|
|
13
|
+
//
|
|
5
14
|
// HANDLER_CONFIG schema (JSON in env var "HANDLER_CONFIG"):
|
|
6
15
|
//
|
|
7
16
|
// {
|
|
@@ -9,16 +18,10 @@
|
|
|
9
18
|
// "pluginExtensionPointCmdTopicUrl":string // SQS URL of PlatformPluginExtPoint cmd topic (admin's Plugin EP)
|
|
10
19
|
// "eventTopicArn": string // SNS ARN the EP's outgoing events publish to ("NOT_AVAILABLE" if none)
|
|
11
20
|
// "pluginReadModelTableName": string // DynamoDB table for Plugin RM (used by QueryEngine.scan)
|
|
12
|
-
// "pluginSchemaPersistenceTableName":string // DynamoDB table holding deploy-time SDL fragments
|
|
13
|
-
// // (rows keyed "deploy-schema:<name>"). Durable source
|
|
14
|
-
// // for the runtime schema stitch; "NOT_AVAILABLE" →
|
|
15
|
-
// // fall back to Plugin RM Connected-row scan.
|
|
16
21
|
// "appSyncApiId": string // "NOT_AVAILABLE" disables schema stitching
|
|
17
|
-
// "clonerEnabled": boolean
|
|
18
22
|
// "schedulerRoleArn": string // IAM role for CloudWatch Events schedule targets
|
|
19
23
|
// "schedulerQueueArn": string // ARN of the cmd topic the scheduler publishes to ("" → no commandTopicResources)
|
|
20
24
|
// "schedulerQueueName": string
|
|
21
|
-
// "pluginDefinition": Plugin.pluginDefinition // serialized; admin uses a fake one (id="Admin@INTERNAL")
|
|
22
25
|
// "extensionPoints": [ // EPs whose OUTGOING events this Lambda processes; admin: 1 entry, plugins: []
|
|
23
26
|
// {
|
|
24
27
|
// "specModule": string, // dynamic import specifier for the EP spec module
|
|
@@ -44,8 +47,7 @@
|
|
|
44
47
|
// }
|
|
45
48
|
// ],
|
|
46
49
|
// "publishToAggregates": { // aggregateName → env-var name holding the aggregate's cmd-topic SQS URL
|
|
47
|
-
// "Plugin": "PTA_Plugin_QUEUE_URL"
|
|
48
|
-
// "RecordProductDemand": "PTA_RecordProductDemand_QUEUE_URL"
|
|
50
|
+
// "Plugin": "PTA_Plugin_QUEUE_URL"
|
|
49
51
|
// },
|
|
50
52
|
// "readModelQueueUrls": { // rmName → env-var name holding the RM's EventCollector SQS URL
|
|
51
53
|
// "ProductDemands": "PRM_ProductDemands_QUEUE_URL"
|
|
@@ -60,353 +62,16 @@
|
|
|
60
62
|
// asset (this file). The runtime differences are entirely data — which EPs to
|
|
61
63
|
// process outgoing events for (admin only) and which Connect extension to wire
|
|
62
64
|
// (plugin only). Plugin_Callback.Make routes incoming SQS events to the right
|
|
63
|
-
// service-keyed handler dict;
|
|
65
|
+
// service-keyed handler dict; Ops reconstructs those dicts from what we wire here.
|
|
64
66
|
|
|
65
|
-
import { readFileSync } from "node:fs";
|
|
66
67
|
import { createRequire } from "node:module";
|
|
67
68
|
import { pathToFileURL } from "node:url";
|
|
68
|
-
import {
|
|
69
|
-
patchSpecId,
|
|
70
|
-
makeQueueRef,
|
|
71
|
-
scanByTableName,
|
|
72
|
-
log,
|
|
73
|
-
runEffect,
|
|
74
|
-
setRequestId,
|
|
75
|
-
extractMetaField,
|
|
76
|
-
extractSentTimestamp,
|
|
77
|
-
extractRetryCount,
|
|
78
|
-
} from "./HandlerFactoryHelpers.mjs";
|
|
79
|
-
import { subscribeQueueToTopic, unsubscribeQueueFromTopic } from "@reventlessdev/rescript-aws-sdk/src/SNS_Helpers.res.mjs";
|
|
80
|
-
import { sendMessage } from "@reventlessdev/reventless-aws/src/plugin/runtime/Util_PluginMessage_Runtime.res.mjs";
|
|
81
|
-
import { publish as snsPublish } from "@reventlessdev/reventless-aws/src/adapter/EventTopic/EventTopicPublisher_SNS_Runtime.res.mjs";
|
|
82
|
-
import { publishJsons as sqsPublishJsons } from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
69
|
+
import { patchSpecId } from "./HandlerFactoryHelpers.mjs";
|
|
83
70
|
import { Make as extensionPointOperationsMake } from "@reventlessdev/reventless-core/src/components/ExtensionPoint/ExtensionPoint_Operations.res.mjs";
|
|
84
71
|
import { Make as extensionOperationsMake } from "@reventlessdev/reventless-core/src/components/Extension/Extension_Operations.res.mjs";
|
|
85
72
|
import { Make as extensionMappingMake } from "@reventlessdev/reventless-infra/src/types/ExtensionMapping.res.mjs";
|
|
86
73
|
import { Make as extensionPointMappingMake } from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
|
|
87
|
-
import
|
|
88
|
-
import { handleDynamoDbOrSqsEvent } from "@reventlessdev/reventless-aws/src/adapter/EventCollector/EventCollectorChannel_SQS_Runtime.res.mjs";
|
|
89
|
-
import { createSchedule as cwCreateSchedule, deleteSchedule as cwDeleteSchedule } from "@reventlessdev/reventless-aws/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents_Runtime.res.mjs";
|
|
90
|
-
import {
|
|
91
|
-
decode as decodeFragment,
|
|
92
|
-
} from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
93
|
-
|
|
94
|
-
// The Plugin RM state schema uses `@s.matches(_jsNullable …)` for many
|
|
95
|
-
// optional fields — top-level AND nested. DDB writes drop undefined
|
|
96
|
-
// attributes, so the unmarshalled row arrives with the relevant keys
|
|
97
|
-
// *missing* (not set to null). Sury's strict parser then rejects them as
|
|
98
|
-
// "received undefined". Tracking and patching every nullable path by hand is
|
|
99
|
-
// whack-a-mole; instead, manageSubscriptions and reconcileSubscriptions only
|
|
100
|
-
// need a tiny subset of the state (id / status / extensions / extensionPoints
|
|
101
|
-
// / eventCollector), so they sidestep sury entirely and read the DDB row
|
|
102
|
-
// directly via this small projection.
|
|
103
|
-
function projectPluginRow(row) {
|
|
104
|
-
if (!row || typeof row !== "object") return null;
|
|
105
|
-
if (typeof row.id !== "string" || typeof row.status !== "string") return null;
|
|
106
|
-
const dcbEventLog =
|
|
107
|
-
row.dcbEventLog &&
|
|
108
|
-
typeof row.dcbEventLog === "object" &&
|
|
109
|
-
typeof row.dcbEventLog.name === "string" &&
|
|
110
|
-
typeof row.dcbEventLog.eventTopicArn === "string"
|
|
111
|
-
? { name: row.dcbEventLog.name, eventTopicArn: row.dcbEventLog.eventTopicArn }
|
|
112
|
-
: null;
|
|
113
|
-
return {
|
|
114
|
-
id: row.id,
|
|
115
|
-
status: row.status,
|
|
116
|
-
eventCollector: typeof row.eventCollector === "string" ? row.eventCollector : "",
|
|
117
|
-
extensions: Array.isArray(row.extensions)
|
|
118
|
-
? row.extensions
|
|
119
|
-
.filter((e) => e && typeof e.extensionPointName === "string")
|
|
120
|
-
.map((e) => ({
|
|
121
|
-
name: typeof e.name === "string" ? e.name : "",
|
|
122
|
-
extensionPointName: e.extensionPointName,
|
|
123
|
-
dcbSources: Array.isArray(e.dcbSources)
|
|
124
|
-
? e.dcbSources.filter((s) => typeof s === "string")
|
|
125
|
-
: [],
|
|
126
|
-
}))
|
|
127
|
-
: [],
|
|
128
|
-
extensionPoints: Array.isArray(row.extensionPoints)
|
|
129
|
-
? row.extensionPoints
|
|
130
|
-
.filter((ep) => ep && typeof ep.name === "string" && typeof ep.eventTopic === "string")
|
|
131
|
-
.map((ep) => ({
|
|
132
|
-
name: ep.name,
|
|
133
|
-
commandTopic: typeof ep.commandTopic === "string" ? ep.commandTopic : "",
|
|
134
|
-
eventTopic: ep.eventTopic,
|
|
135
|
-
}))
|
|
136
|
-
: [],
|
|
137
|
-
dcbEventLog,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function parseHandlerConfig(rawJson) {
|
|
142
|
-
if (!rawJson) throw new Error("HANDLER_CONFIG env var is empty");
|
|
143
|
-
let config;
|
|
144
|
-
try {
|
|
145
|
-
config = JSON.parse(rawJson);
|
|
146
|
-
} catch (e) {
|
|
147
|
-
throw new Error("HANDLER_CONFIG JSON parse error: " + (e && e.message));
|
|
148
|
-
}
|
|
149
|
-
const required = [
|
|
150
|
-
"queueUrl",
|
|
151
|
-
"pluginExtensionPointCmdTopicUrl",
|
|
152
|
-
"eventTopicArn",
|
|
153
|
-
"pluginReadModelTableName",
|
|
154
|
-
"appSyncApiId",
|
|
155
|
-
"schedulerRoleArn",
|
|
156
|
-
"schedulerQueueArn",
|
|
157
|
-
"schedulerQueueName",
|
|
158
|
-
"extensionPoints",
|
|
159
|
-
"extensions",
|
|
160
|
-
"publishToAggregates",
|
|
161
|
-
];
|
|
162
|
-
for (const k of required) {
|
|
163
|
-
if (config[k] === undefined) {
|
|
164
|
-
throw new Error("HANDLER_CONFIG missing required field: " + k);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
if (!Array.isArray(config.extensionPoints)) throw new Error("HANDLER_CONFIG.extensionPoints must be an array");
|
|
168
|
-
if (!Array.isArray(config.extensions)) throw new Error("HANDLER_CONFIG.extensions must be an array");
|
|
169
|
-
// connectExtension is allowed to be null or absent (admin case)
|
|
170
|
-
if (config.connectExtension === undefined) config.connectExtension = null;
|
|
171
|
-
// Optional fields with empty defaults — admin context omits them entirely.
|
|
172
|
-
if (config.readModelQueueUrls === undefined) config.readModelQueueUrls = {};
|
|
173
|
-
if (config.readModelNamesForSourceName === undefined) config.readModelNamesForSourceName = {};
|
|
174
|
-
// Per-extension defaults so older serialisers (or partial admin configs) keep working.
|
|
175
|
-
for (const ext of config.extensions) {
|
|
176
|
-
if (!Array.isArray(ext.aggregateNames)) ext.aggregateNames = [];
|
|
177
|
-
if (!Array.isArray(ext.readModelNames)) ext.readModelNames = [];
|
|
178
|
-
if (typeof ext.name !== "string") ext.name = ext.extensionPointName || "unknown";
|
|
179
|
-
if (typeof ext.delegateModule !== "string") ext.delegateModule = "";
|
|
180
|
-
}
|
|
181
|
-
return config;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const invalidNameChars = /[^.\-_a-zA-Z0-9]/g;
|
|
185
|
-
const resourceNaming = {
|
|
186
|
-
validateName: (n) => n.replace(invalidNameChars, "_"),
|
|
187
|
-
urnName: (arn) => {
|
|
188
|
-
const parts = arn.split(":");
|
|
189
|
-
return parts[5] || "unknown";
|
|
190
|
-
},
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
// Admin-mediated cross-plugin SNS subscription manager (Plan Phase 3, Step 1).
|
|
194
|
-
// When DoConnectPlugin / DoDisconnectPlugin fires on the admin EP mapping, we
|
|
195
|
-
// scan the Plugin RM for currently-Connected peers and wire (or tear down) SNS
|
|
196
|
-
// subscriptions between this plugin's EventCollector queue and peer plugins'
|
|
197
|
-
// EP event topics — in both directions:
|
|
198
|
-
// - peer EP topic → this plugin's EC queue, for each of this plugin's
|
|
199
|
-
// extensions whose `extensionPointName` matches a peer EP
|
|
200
|
-
// - this plugin's EP topics → peer EC queues, for each peer extension whose
|
|
201
|
-
// `extensionPointName` matches one of this plugin's EPs
|
|
202
|
-
// subscribeQueueToTopic is idempotent (lists existing subscriptions first);
|
|
203
|
-
// unsubscribeQueueFromTopic is best-effort and swallows the "no subscription"
|
|
204
|
-
// case. Failures are logged but do not throw — admin's RM projection still
|
|
205
|
-
// proceeds and cold-start reconciliation will re-attempt missed work.
|
|
206
|
-
function mkManageSubscriptions(tableName) {
|
|
207
|
-
if (!tableName || tableName === "NOT_AVAILABLE") return undefined;
|
|
208
|
-
const trySubscribe = async (queueArn, topicArn, label) => {
|
|
209
|
-
if (!queueArn || !topicArn) return;
|
|
210
|
-
try {
|
|
211
|
-
await subscribeQueueToTopic(queueArn, topicArn);
|
|
212
|
-
log.info(`subscribed ${label}: ${topicArn} -> ${queueArn}`, { comp: "manageSubscriptions" });
|
|
213
|
-
} catch (e) {
|
|
214
|
-
const msg = (e && e.message) || String(e);
|
|
215
|
-
log.error(`subscribe failed ${label}: ${topicArn} -> ${queueArn}`, { comp: "manageSubscriptions", detail: msg });
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
const tryUnsubscribe = async (queueArn, topicArn, label) => {
|
|
219
|
-
if (!queueArn || !topicArn) return;
|
|
220
|
-
try {
|
|
221
|
-
await unsubscribeQueueFromTopic(queueArn, topicArn);
|
|
222
|
-
log.info(`unsubscribed ${label}: ${topicArn} -> ${queueArn}`, { comp: "manageSubscriptions" });
|
|
223
|
-
} catch (e) {
|
|
224
|
-
const msg = (e && e.message) || String(e);
|
|
225
|
-
log.error(`unsubscribe failed ${label}: ${topicArn} -> ${queueArn}`, { comp: "manageSubscriptions", detail: msg });
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
const scanConnectedPeers = async (excludeId) => {
|
|
229
|
-
const rows = await scanByTableName(
|
|
230
|
-
tableName,
|
|
231
|
-
[["status", { TAG: "Contains" }, { TAG: "String", _0: "Connected" }]],
|
|
232
|
-
1000
|
|
233
|
-
);
|
|
234
|
-
const peers = [];
|
|
235
|
-
for (const row of rows) {
|
|
236
|
-
const state = projectPluginRow(row);
|
|
237
|
-
if (state && state.id !== excludeId) peers.push(state);
|
|
238
|
-
}
|
|
239
|
-
return peers;
|
|
240
|
-
};
|
|
241
|
-
// Extract the plugin display name from a versioned id like "Catalog@1.0.0-alpha.65".
|
|
242
|
-
const pluginNameOfId = (id) => (typeof id === "string" ? id.split("@")[0] : "");
|
|
243
|
-
return async (pluginDef, action) => {
|
|
244
|
-
const isConnect = action === "connect";
|
|
245
|
-
const op = isConnect ? trySubscribe : tryUnsubscribe;
|
|
246
|
-
const peers = await scanConnectedPeers(pluginDef.id);
|
|
247
|
-
// Subscriptions are wired between SNS topics and SQS queues that are owned
|
|
248
|
-
// per PLUGIN NAME, not per version (the EC queue, EP event topics, and DCB
|
|
249
|
-
// event topic ARNs all live in the plugin's stack and are stable across
|
|
250
|
-
// version-to-version upgrades). When a superseded version is Retired and
|
|
251
|
-
// its row transitions to Disconnected, this hook fires with action="disconnect"
|
|
252
|
-
// — but if a newer version of the same plugin is still Connected, the
|
|
253
|
-
// subscriptions are still in use. Tearing them down here would silently
|
|
254
|
-
// break cross-plugin event flow for the live version (and only cold-start
|
|
255
|
-
// reconciliation on the surviving plugin would heal it — which happens on
|
|
256
|
-
// its next Lambda init, not immediately). Skip the tear-down in that case.
|
|
257
|
-
if (!isConnect) {
|
|
258
|
-
const myName = pluginNameOfId(pluginDef.id);
|
|
259
|
-
const liveSibling = peers.find((p) => pluginNameOfId(p.id) === myName);
|
|
260
|
-
if (liveSibling) {
|
|
261
|
-
log.info(`disconnect skipped for ${pluginDef.id}: sibling version ${liveSibling.id} still Connected and shares the same EC queue / EP topics`, { comp: "manageSubscriptions" });
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// Subscriptions for this plugin's extensions: peer EP topic → this EC queue
|
|
266
|
-
for (const ext of pluginDef.extensions || []) {
|
|
267
|
-
for (const peer of peers) {
|
|
268
|
-
const peerEp = (peer.extensionPoints || []).find(
|
|
269
|
-
(ep) => ep.name === ext.extensionPointName
|
|
270
|
-
);
|
|
271
|
-
if (peerEp) {
|
|
272
|
-
await op(pluginDef.eventCollector, peerEp.eventTopic, `${ext.extensionPointName} -> ${pluginDef.id}`);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
// Subscriptions for this plugin's EPs: this EP topic → peer EC queues
|
|
277
|
-
for (const ep of pluginDef.extensionPoints || []) {
|
|
278
|
-
for (const peer of peers) {
|
|
279
|
-
const matchingExt = (peer.extensions || []).find(
|
|
280
|
-
(e) => e.extensionPointName === ep.name
|
|
281
|
-
);
|
|
282
|
-
if (matchingExt) {
|
|
283
|
-
await op(peer.eventCollector, ep.eventTopic, `${ep.name} -> ${peer.id}`);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
// ── Phase 4: DCB topic subscriptions ──────────────────────────────────
|
|
288
|
-
// For each DCB source this plugin's extensions reference, subscribe this
|
|
289
|
-
// plugin's EventCollector to the owning peer plugin's DCB EventTopic.
|
|
290
|
-
for (const ext of pluginDef.extensions || []) {
|
|
291
|
-
for (const dcbSourceName of ext.dcbSources || []) {
|
|
292
|
-
const peer = peers.find(
|
|
293
|
-
(p) => p.dcbEventLog && p.dcbEventLog.name === dcbSourceName
|
|
294
|
-
);
|
|
295
|
-
if (peer) {
|
|
296
|
-
await op(
|
|
297
|
-
pluginDef.eventCollector,
|
|
298
|
-
peer.dcbEventLog.eventTopicArn,
|
|
299
|
-
`dcb ${dcbSourceName} -> ${pluginDef.id}`
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
// If THIS plugin has a DCB EventLog, subscribe each peer plugin's
|
|
305
|
-
// EventCollector that references it via dcbSources.
|
|
306
|
-
if (pluginDef.dcbEventLog) {
|
|
307
|
-
const myDcbName = pluginDef.dcbEventLog.name;
|
|
308
|
-
for (const peer of peers) {
|
|
309
|
-
const consuming = (peer.extensions || []).some((e) =>
|
|
310
|
-
(e.dcbSources || []).includes(myDcbName)
|
|
311
|
-
);
|
|
312
|
-
if (consuming) {
|
|
313
|
-
await op(
|
|
314
|
-
peer.eventCollector,
|
|
315
|
-
pluginDef.dcbEventLog.eventTopicArn,
|
|
316
|
-
`dcb ${myDcbName} -> ${peer.id}`
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Cold-start reconciliation — scan the Plugin RM for every Connected plugin
|
|
325
|
-
// and rerun manageSubscriptions(p, "connect") for each. Idempotent at the SNS
|
|
326
|
-
// level (subscribeQueueToTopic dedupes), so this is safe to run repeatedly;
|
|
327
|
-
// it catches subscriptions lost out-of-band (topic deleted+recreated, plugin
|
|
328
|
-
// restarted with a new EC ARN, etc.). Fire-and-forget — admin still serves
|
|
329
|
-
// incoming events while reconciliation completes.
|
|
330
|
-
async function reconcileSubscriptionsOnce(tableName, manageSubscriptions) {
|
|
331
|
-
if (!tableName || tableName === "NOT_AVAILABLE" || !manageSubscriptions) return;
|
|
332
|
-
try {
|
|
333
|
-
const rows = await scanByTableName(
|
|
334
|
-
tableName,
|
|
335
|
-
[["status", { TAG: "Contains" }, { TAG: "String", _0: "Connected" }]],
|
|
336
|
-
1000
|
|
337
|
-
);
|
|
338
|
-
for (const row of rows) {
|
|
339
|
-
const state = projectPluginRow(row);
|
|
340
|
-
if (state) {
|
|
341
|
-
try {
|
|
342
|
-
await manageSubscriptions(state, "connect");
|
|
343
|
-
} catch (e) {
|
|
344
|
-
log.warn("manageSubscriptions failed", { comp: "reconcileSubscriptions", detail: (e && e.message) || String(e) });
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
} catch (e) {
|
|
349
|
-
log.warn("scan failed", { comp: "reconcileSubscriptions", detail: (e && e.message) || String(e) });
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function buildPublishToAggregates(map) {
|
|
354
|
-
const out = {};
|
|
355
|
-
for (const [aggName, envVarName] of Object.entries(map || {})) {
|
|
356
|
-
const queueUrl = process.env[envVarName] || "";
|
|
357
|
-
out[aggName] = sqsPublishJsons(makeQueueRef(queueUrl), "SQS_FIFO");
|
|
358
|
-
}
|
|
359
|
-
return out;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function dictByServiceKeys(serviceKeys, handler) {
|
|
363
|
-
const dict = {};
|
|
364
|
-
for (const k of serviceKeys || []) {
|
|
365
|
-
if (!dict[k]) dict[k] = [];
|
|
366
|
-
dict[k].push(handler);
|
|
367
|
-
}
|
|
368
|
-
return dict;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function mergeDicts(...dicts) {
|
|
372
|
-
const out = {};
|
|
373
|
-
for (const d of dicts) {
|
|
374
|
-
for (const [k, v] of Object.entries(d)) {
|
|
375
|
-
out[k] = (out[k] || []).concat(v);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return out;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// pluginDefinition is shipped as a file in the asset zip rather than packed
|
|
382
|
-
// into HANDLER_CONFIG (AWS Lambda's UpdateFunctionConfiguration request has a
|
|
383
|
-
// 5120-byte limit; pluginStructure alone — schemas inline per component —
|
|
384
|
-
// blew past that). The Lambda runtime extracts the zip to /var/task, and the
|
|
385
|
-
// re-export index.mjs lives at /var/task/index.mjs alongside
|
|
386
|
-
// pluginDefinition.json. process.cwd() is /var/task in Lambda, so a relative
|
|
387
|
-
// path resolves correctly. The file is parsed once at cold start.
|
|
388
|
-
function loadPluginDefinition() {
|
|
389
|
-
try {
|
|
390
|
-
const raw = readFileSync(new URL("./pluginDefinition.json", `file://${process.cwd()}/`), "utf-8");
|
|
391
|
-
return JSON.parse(raw);
|
|
392
|
-
} catch (e) {
|
|
393
|
-
const msg = (e && e.message) || String(e);
|
|
394
|
-
throw new Error("Failed to load pluginDefinition.json from asset zip: " + msg);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// The plugin's UI-fragment manifest — shipped as its own asset since the
|
|
399
|
-
// manifest no longer rides pluginDefinition (the UiFragmentRegistry slice owns
|
|
400
|
-
// fragment state). Contains JSON `null` for plugins without a UI; tolerate a
|
|
401
|
-
// missing file (archives built before the asset existed) the same way.
|
|
402
|
-
function loadUiFragments() {
|
|
403
|
-
try {
|
|
404
|
-
const raw = readFileSync(new URL("./uiFragments.json", `file://${process.cwd()}/`), "utf-8");
|
|
405
|
-
return JSON.parse(raw);
|
|
406
|
-
} catch (e) {
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
74
|
+
import * as Ops from "./EventCollectorEntryPoint_Ops.res.mjs";
|
|
410
75
|
|
|
411
76
|
// Cross-plugin spec packages (e.g. "@reventlessdev/online-shop-hybrid-catalog-spec")
|
|
412
77
|
// are bundled into the function asset under /var/task/node_modules/ by
|
|
@@ -428,52 +93,13 @@ const importFromAsset = (() => {
|
|
|
428
93
|
})();
|
|
429
94
|
|
|
430
95
|
async function buildHandler() {
|
|
431
|
-
const config = parseHandlerConfig(process.env["HANDLER_CONFIG"]);
|
|
432
|
-
const pluginDefinition = loadPluginDefinition();
|
|
433
|
-
// Stack name, not the function name — see PluginExtensionPointEntryPoint for
|
|
434
|
-
// why the disconnect schedule's rule prefix must not carry a content hash.
|
|
435
|
-
// Both entry points instantiate the same admin Plugin EP module, so they have
|
|
436
|
-
// to derive this identically.
|
|
437
|
-
const environment = process.env["Environment"] || "unknown";
|
|
96
|
+
const config = Ops.parseHandlerConfig(process.env["HANDLER_CONFIG"] || "");
|
|
97
|
+
const pluginDefinition = Ops.loadPluginDefinition();
|
|
438
98
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// used to call no longer exist on either side.
|
|
444
|
-
const runtimeOps = {
|
|
445
|
-
messagePublish: { sendMessageToChannel: sendMessage },
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
const queryEngine = {
|
|
449
|
-
scan: (readModelName, filterConfigs, limit) =>
|
|
450
|
-
scanByTableName(config.pluginReadModelTableName, filterConfigs, limit),
|
|
451
|
-
query: async () => {
|
|
452
|
-
throw new Error("QueryEngine.query not available in bundled Plugin EventCollector");
|
|
453
|
-
},
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
const scheduler = {
|
|
457
|
-
createSchedule: cwCreateSchedule(config.schedulerRoleArn),
|
|
458
|
-
deleteSchedule: cwDeleteSchedule,
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
const commandTopicResources = config.schedulerQueueArn !== ""
|
|
462
|
-
? [{
|
|
463
|
-
name: config.schedulerQueueName,
|
|
464
|
-
id: config.schedulerQueueName,
|
|
465
|
-
urn: config.schedulerQueueArn,
|
|
466
|
-
service: "unknown",
|
|
467
|
-
resourceInfo: "NoInfo",
|
|
468
|
-
role: "",
|
|
469
|
-
region: "",
|
|
470
|
-
resourceType: "",
|
|
471
|
-
configuration: {},
|
|
472
|
-
tags: {},
|
|
473
|
-
}]
|
|
474
|
-
: [];
|
|
475
|
-
|
|
476
|
-
const manageSubscriptionsFn = mkManageSubscriptions(config.pluginReadModelTableName);
|
|
99
|
+
const queryEngine = Ops.makeQueryEngine(config.pluginReadModelTableName);
|
|
100
|
+
const scheduler = Ops.makeScheduler(config.schedulerRoleArn);
|
|
101
|
+
const commandTopicResources = Ops.makeCommandTopicResources(config);
|
|
102
|
+
const manageSubscriptionsFn = Ops.makeManageSubscriptions(config.pluginReadModelTableName);
|
|
477
103
|
|
|
478
104
|
// EP operations — admin has 1 entry (Plugin EP), plugins have N user EPs.
|
|
479
105
|
//
|
|
@@ -489,8 +115,7 @@ async function buildHandler() {
|
|
|
489
115
|
// that declares an ExtensionPoint. The ExtensionPoint spec module is
|
|
490
116
|
// erased at compile time and must be re-attached at runtime.
|
|
491
117
|
//
|
|
492
|
-
// Dispatch on `typeof Make === function` to handle both.
|
|
493
|
-
// user-extensions branch below which already does this for ExtensionMappings.
|
|
118
|
+
// Dispatch on `typeof Make === function` to handle both.
|
|
494
119
|
const epHandlers = await Promise.all(config.extensionPoints.map(async (ep) => {
|
|
495
120
|
const specMod = patchSpecId(await importFromAsset(ep.specModule));
|
|
496
121
|
const mappingsMod = await importFromAsset(ep.mappingsModule);
|
|
@@ -499,12 +124,12 @@ async function buildHandler() {
|
|
|
499
124
|
if (typeof mappingsMod.Make === "function") {
|
|
500
125
|
// Admin's Plugin EP: invoke the Make functor with runtime ops.
|
|
501
126
|
const epModule = mappingsMod.Make({
|
|
502
|
-
runtimeOps,
|
|
503
|
-
environment,
|
|
504
|
-
// Connect-driven schema self-heal retired
|
|
127
|
+
runtimeOps: Ops.runtimeOps,
|
|
128
|
+
environment: Ops.environment,
|
|
129
|
+
// Connect-driven schema self-heal retired — the ApiSchemaPush
|
|
505
130
|
// SideEffect on ApiFragmentRegistry events is the single schema writer.
|
|
506
131
|
updateApiSchema: undefined,
|
|
507
|
-
manageSubscriptions: manageSubscriptionsFn,
|
|
132
|
+
manageSubscriptions: Ops.manageForDefinition(manageSubscriptionsFn),
|
|
508
133
|
});
|
|
509
134
|
mappingsModule = { mappings: [epModule.Mapping] };
|
|
510
135
|
} else {
|
|
@@ -522,55 +147,40 @@ async function buildHandler() {
|
|
|
522
147
|
Delegate: patchSpecId(mappingsMod.Delegate),
|
|
523
148
|
moduleUrl: ep.mappingsModule,
|
|
524
149
|
};
|
|
525
|
-
|
|
526
|
-
mappingsModule = { mappings: [transformedMapping] };
|
|
150
|
+
mappingsModule = { mappings: [extensionPointMappingMake(mappingImpl)] };
|
|
527
151
|
}
|
|
528
152
|
|
|
529
|
-
const resolvedTopic = {
|
|
530
|
-
name: ep.eventTopicArn,
|
|
531
|
-
id: ep.eventTopicArn,
|
|
532
|
-
arn: ep.eventTopicArn,
|
|
533
|
-
};
|
|
534
153
|
const ops = extensionPointOperationsMake(specMod)(mappingsModule)({
|
|
535
|
-
publishToEventTopic: (
|
|
154
|
+
publishToEventTopic: Ops.makePublishToEventTopic(ep.eventTopicArn),
|
|
536
155
|
commandTopicResources,
|
|
537
156
|
scheduler,
|
|
538
157
|
queryEngine,
|
|
539
|
-
resourceNaming,
|
|
158
|
+
resourceNaming: Ops.resourceNaming,
|
|
540
159
|
});
|
|
541
|
-
return {
|
|
542
|
-
aggregateNames: ep.aggregateNames || [],
|
|
543
|
-
outgoingHandler: ops.outgoingJsonEventsHandler,
|
|
544
|
-
};
|
|
160
|
+
return { aggregateNames: ep.aggregateNames, outgoingHandler: ops.outgoingJsonEventsHandler };
|
|
545
161
|
}));
|
|
546
162
|
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
const publishToPluginExtensionPoint = sqsPublishJsons(
|
|
550
|
-
makeQueueRef(config.pluginExtensionPointCmdTopicUrl),
|
|
551
|
-
"SQS_FIFO",
|
|
552
|
-
);
|
|
163
|
+
const publishToAggregates = Ops.buildPublishToAggregates(config.publishToAggregates);
|
|
164
|
+
const publishToPluginExtensionPoint = Ops.makePublishJsons(config.pluginExtensionPointCmdTopicUrl);
|
|
553
165
|
|
|
554
|
-
|
|
166
|
+
// Connect extension — exactly one entry per plugin Lambda; null for admin.
|
|
167
|
+
let connectExtensionHandler = undefined;
|
|
555
168
|
if (config.connectExtension) {
|
|
556
169
|
const ext = config.connectExtension;
|
|
557
170
|
const specMod = patchSpecId(await importFromAsset(ext.specModule));
|
|
558
171
|
const mappingsMod = await importFromAsset(ext.mappingsModule);
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
// directives moved to admin's manageSubscriptions hook. ReScript None
|
|
563
|
-
// compiles to `undefined`, so map the asset's JSON null accordingly.
|
|
172
|
+
// The Connect extension Spec carries pluginDefinition plus the plugin's
|
|
173
|
+
// UI-fragment manifest (its own asset). loadUiFragments returns a ReScript
|
|
174
|
+
// option — `undefined` (None) for plugins without a UI.
|
|
564
175
|
const extBuilder = mappingsMod.Make({
|
|
565
176
|
pluginDefinition,
|
|
566
|
-
uiFragments: loadUiFragments()
|
|
177
|
+
uiFragments: Ops.loadUiFragments(),
|
|
567
178
|
});
|
|
568
179
|
// PluginConnectExtension_Builder.Make returns a module exposing:
|
|
569
180
|
// - ConnectPluginMapping (single mapping)
|
|
570
181
|
// - ConnectPluginMappings (Mappings module: {Spec, name, mappings, moduleUrl})
|
|
571
182
|
// Use ConnectPluginMappings as the Mappings arg to Extension_Operations.Make.
|
|
572
|
-
const
|
|
573
|
-
const ops = extensionOperationsMake(specMod)(mappingsModule)({
|
|
183
|
+
const ops = extensionOperationsMake(specMod)(extBuilder.ConnectPluginMappings)({
|
|
574
184
|
publishToAggregates,
|
|
575
185
|
publishToPluginExtensionPoint,
|
|
576
186
|
readModelNamesForSourceName: {},
|
|
@@ -589,7 +199,7 @@ async function buildHandler() {
|
|
|
589
199
|
// it through ExtensionMapping.Make, and build per-extension Extension_Operations.
|
|
590
200
|
const userExtensionHandlers = await Promise.all(config.extensions.map(async (ext) => {
|
|
591
201
|
if (!ext.specModule || !ext.mappingsModule || !ext.delegateModule) {
|
|
592
|
-
|
|
202
|
+
Ops.warnSkippedExtension(ext);
|
|
593
203
|
return null;
|
|
594
204
|
}
|
|
595
205
|
const epSpec = patchSpecId(await importFromAsset(ext.specModule));
|
|
@@ -615,42 +225,12 @@ async function buildHandler() {
|
|
|
615
225
|
mappings: [transformedMapping],
|
|
616
226
|
};
|
|
617
227
|
|
|
618
|
-
|
|
619
|
-
// each aggregate's cmd-topic SQS URL is looked up via the
|
|
620
|
-
// PTA_<aggName>_QUEUE_URL env var name carried in HANDLER_CONFIG.
|
|
621
|
-
const extPublishToAggregates = {};
|
|
622
|
-
for (const aggName of ext.aggregateNames) {
|
|
623
|
-
const envVarName = config.publishToAggregates[aggName];
|
|
624
|
-
if (!envVarName) continue;
|
|
625
|
-
const queueUrl = process.env[envVarName] || "";
|
|
626
|
-
if (!queueUrl) continue;
|
|
627
|
-
extPublishToAggregates[aggName] = sqsPublishJsons(makeQueueRef(queueUrl), "SQS_FIFO");
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Per-extension publishToReadModels — each RM's EventCollector SQS URL
|
|
631
|
-
// is looked up via the PRM_<rmName>_QUEUE_URL env var name carried in
|
|
632
|
-
// HANDLER_CONFIG.readModelQueueUrls.
|
|
633
|
-
const extPublishToReadModels = {};
|
|
634
|
-
for (const rmName of ext.readModelNames) {
|
|
635
|
-
const envVarName = config.readModelQueueUrls[rmName];
|
|
636
|
-
if (!envVarName) continue;
|
|
637
|
-
const queueUrl = process.env[envVarName] || "";
|
|
638
|
-
if (!queueUrl) continue;
|
|
639
|
-
extPublishToReadModels[rmName] = sqsPublishJsons(makeQueueRef(queueUrl), "SQS_FIFO");
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// readModelNamesForSourceName scoped to this extension's EP — Extension_Operations
|
|
643
|
-
// uses this dict to decide which RMs receive each incoming event.
|
|
644
|
-
const extReadModelNamesForSourceName = {
|
|
645
|
-
[ext.extensionPointName]:
|
|
646
|
-
config.readModelNamesForSourceName[ext.extensionPointName] || [],
|
|
647
|
-
};
|
|
648
|
-
|
|
228
|
+
const dicts = Ops.extensionPublishDicts(config, ext);
|
|
649
229
|
const ops = extensionOperationsMake(epSpec)(mappingsModule)({
|
|
650
|
-
publishToAggregates:
|
|
230
|
+
publishToAggregates: dicts.publishToAggregates,
|
|
651
231
|
publishToPluginExtensionPoint,
|
|
652
|
-
readModelNamesForSourceName:
|
|
653
|
-
publishToReadModels:
|
|
232
|
+
readModelNamesForSourceName: dicts.readModelNamesForSourceName,
|
|
233
|
+
publishToReadModels: dicts.publishToReadModels,
|
|
654
234
|
queryEngine,
|
|
655
235
|
});
|
|
656
236
|
|
|
@@ -661,57 +241,19 @@ async function buildHandler() {
|
|
|
661
241
|
outgoingHandler: ops.outgoingJsonEventsHandler,
|
|
662
242
|
};
|
|
663
243
|
}));
|
|
664
|
-
const extensionHandlers = userExtensionHandlers.filter(Boolean);
|
|
665
244
|
|
|
666
|
-
//
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const incomingConnectExtensionEventHandlers = connectExtensionHandler
|
|
671
|
-
? { [connectExtensionHandler.extensionPointName]: [connectExtensionHandler.incomingHandler] }
|
|
672
|
-
: {};
|
|
673
|
-
const outgoingExtensionEventHandlers = extensionHandlers
|
|
674
|
-
.map((eh) => dictByServiceKeys(eh.aggregateNames, eh.outgoingHandler))
|
|
675
|
-
.reduce(mergeDicts, {});
|
|
676
|
-
const incomingExtensionEventHandlers = extensionHandlers
|
|
677
|
-
.map((eh) => ({ [eh.extensionPointName]: [eh.incomingHandler] }))
|
|
678
|
-
.reduce(mergeDicts, {});
|
|
245
|
+
// Cold-start reconciliation — awaited so it finishes inside the same
|
|
246
|
+
// invocation that initialised the Lambda (a floating promise pauses when the
|
|
247
|
+
// runtime freezes between invocations). Idempotent at the SNS level.
|
|
248
|
+
await Ops.reconcileSubscriptionsOnce(config.pluginReadModelTableName, manageSubscriptionsFn);
|
|
679
249
|
|
|
680
|
-
|
|
250
|
+
return Ops.makeSqsHandlerBundle({
|
|
681
251
|
pluginDefinition,
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
252
|
+
queueUrl: config.queueUrl,
|
|
253
|
+
epHandlers,
|
|
254
|
+
connectExtensionHandler,
|
|
255
|
+
extensionHandlers: userExtensionHandlers.filter(Boolean),
|
|
686
256
|
});
|
|
687
|
-
|
|
688
|
-
// Cold-start reconciliation — awaited so it finishes inside the same
|
|
689
|
-
// invocation that initialised the Lambda. Fire-and-forget doesn't work on
|
|
690
|
-
// Lambda: any unfinished promise pauses when the runtime freezes between
|
|
691
|
-
// invocations and rarely gets a chance to resume. Idempotent at the SNS
|
|
692
|
-
// level, so the modest extra init latency is paid only on true cold starts.
|
|
693
|
-
await reconcileSubscriptionsOnce(config.pluginReadModelTableName, manageSubscriptionsFn);
|
|
694
|
-
|
|
695
|
-
const sqsHandler = handleDynamoDbOrSqsEvent(makeQueueRef(config.queueUrl), callback.handleJsonEvents);
|
|
696
|
-
// This module backs both the admin and every per-plugin EventCollector
|
|
697
|
-
// Lambda, so the comp names the plugin whose events it drains — matching the
|
|
698
|
-
// `Plugin(<id>)` shape Plugin_Callback logs under.
|
|
699
|
-
return { sqsHandler, comp: `Plugin(${pluginDefinition.id})` };
|
|
700
257
|
}
|
|
701
258
|
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
export async function handler(event, context) {
|
|
705
|
-
setRequestId(context?.awsRequestId);
|
|
706
|
-
const records = event.Records || [];
|
|
707
|
-
log.debug("processing " + records.length.toString() + " record(s)", { comp: "PluginEventCollectorRuntime" });
|
|
708
|
-
const { sqsHandler, comp } = await handlerBundlePromise;
|
|
709
|
-
await runEffect(sqsHandler(event, context), {
|
|
710
|
-
correlationId: extractMetaField(records, "correlationId"),
|
|
711
|
-
causationId: extractMetaField(records, "causationId"),
|
|
712
|
-
comp,
|
|
713
|
-
timestamp: extractSentTimestamp(records),
|
|
714
|
-
retryCount: extractRetryCount(records),
|
|
715
|
-
});
|
|
716
|
-
return "";
|
|
717
|
-
}
|
|
259
|
+
export const handler = Ops.makeHandler(buildHandler());
|