@reventlessdev/reventless-aws 3.0.0-alpha.232 → 3.0.0-alpha.234
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.mjs +14 -1
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +22 -2
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +15 -4
- 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/RuntimeEnvironment_Lambda.res +26 -6
- 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/StateChangeSliceRuntime_Builder_Single.res +5 -2
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +1 -1
- 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/src/plugin/runtime/PluginRuntime_Builder.res +13 -4
- package/src/plugin/runtime/PluginRuntime_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
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
// Typed cold-start core for the Plugin EventCollector Lambda entry point.
|
|
2
|
+
//
|
|
3
|
+
// The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md):
|
|
4
|
+
// EventCollectorEntryPoint.mjs owns ONLY the untyped seams — the dynamic
|
|
5
|
+
// `import()` of EP/extension/spec modules named in HANDLER_CONFIG and the
|
|
6
|
+
// functor applications consuming those runtime-loaded modules
|
|
7
|
+
// (ExtensionPoint_Operations.Make, Extension_Operations.Make, the mapping
|
|
8
|
+
// factories, patchSpecId). Everything else lives here, fully type-checked:
|
|
9
|
+
// HANDLER_CONFIG parsing/validation, the Plugin-RM row projection, the admin
|
|
10
|
+
// cross-plugin SNS subscription manager and its cold-start reconciliation, the
|
|
11
|
+
// publish/query/scheduler operations, the service-keyed handler-dict assembly,
|
|
12
|
+
// Plugin_Callback wiring, and the Lambda dispatch boundary.
|
|
13
|
+
//
|
|
14
|
+
// Runtime-pure: no `open PulumiAws` values — Pulumi appears in type positions
|
|
15
|
+
// only (erased). The two `%identity` casts below sit on documented JSON
|
|
16
|
+
// contracts (same pattern as AggregateEntryPoint_Ops.asConnectionConfig).
|
|
17
|
+
|
|
18
|
+
// ── Shim bindings (HandlerFactoryHelpers.mjs) ───────────────────────────────
|
|
19
|
+
// The structured-log + Effect dispatch boundary shared by every deployed entry
|
|
20
|
+
// point, and the DynamoDB scan backing the read-model query engine.
|
|
21
|
+
|
|
22
|
+
type dispatchOpts = {
|
|
23
|
+
correlationId?: string,
|
|
24
|
+
causationId?: string,
|
|
25
|
+
comp?: string,
|
|
26
|
+
timestamp?: float,
|
|
27
|
+
retryCount?: int,
|
|
28
|
+
detail?: string,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@module("./HandlerFactoryHelpers.mjs")
|
|
32
|
+
external setRequestId: string => unit = "setRequestId"
|
|
33
|
+
@module("./HandlerFactoryHelpers.mjs")
|
|
34
|
+
external runEffect: (Effect.t<'a, 'e, 'r>, dispatchOpts) => promise<unit> = "runEffect"
|
|
35
|
+
@module("./HandlerFactoryHelpers.mjs")
|
|
36
|
+
external extractMetaField: (
|
|
37
|
+
array<PulumiAws.Lambda.CallbackFunction.record>,
|
|
38
|
+
string,
|
|
39
|
+
) => option<string> = "extractMetaField"
|
|
40
|
+
@module("./HandlerFactoryHelpers.mjs")
|
|
41
|
+
external extractSentTimestamp: array<PulumiAws.Lambda.CallbackFunction.record> => option<float> =
|
|
42
|
+
"extractSentTimestamp"
|
|
43
|
+
@module("./HandlerFactoryHelpers.mjs")
|
|
44
|
+
external extractRetryCount: array<PulumiAws.Lambda.CallbackFunction.record> => int =
|
|
45
|
+
"extractRetryCount"
|
|
46
|
+
@module("./HandlerFactoryHelpers.mjs") @scope("log")
|
|
47
|
+
external logDebug: (string, dispatchOpts) => unit = "debug"
|
|
48
|
+
@module("./HandlerFactoryHelpers.mjs") @scope("log")
|
|
49
|
+
external logInfo: (string, dispatchOpts) => unit = "info"
|
|
50
|
+
@module("./HandlerFactoryHelpers.mjs") @scope("log")
|
|
51
|
+
external logWarn: (string, dispatchOpts) => unit = "warn"
|
|
52
|
+
@module("./HandlerFactoryHelpers.mjs") @scope("log")
|
|
53
|
+
external logError: (string, dispatchOpts) => unit = "error"
|
|
54
|
+
@module("./HandlerFactoryHelpers.mjs")
|
|
55
|
+
external scanByTableName: (
|
|
56
|
+
string,
|
|
57
|
+
array<Reventless.QueryEngine.Filter.config>,
|
|
58
|
+
int,
|
|
59
|
+
) => promise<array<JSON.t>> = "scanByTableName"
|
|
60
|
+
|
|
61
|
+
@val @scope("process") external processEnv: dict<string> = "env"
|
|
62
|
+
|
|
63
|
+
let exnMessage = (exn: exn): string =>
|
|
64
|
+
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown error")
|
|
65
|
+
|
|
66
|
+
// ── js_nullable normalization ───────────────────────────────────────────────
|
|
67
|
+
// pluginDefinition option fields use @s.matches(js_nullable): when the record
|
|
68
|
+
// comes from a plain JSON.parse (not a sury parse), None is represented as
|
|
69
|
+
// `null` — which ReScript's option (undefined-based) would misread as Some.
|
|
70
|
+
// Normalize through Nullable so pattern matches see a real option.
|
|
71
|
+
|
|
72
|
+
external optionAsNullable: option<'a> => Nullable.t<'a> = "%identity"
|
|
73
|
+
let jsOption = (v: option<'a>): option<'a> => v->optionAsNullable->Nullable.toOption
|
|
74
|
+
|
|
75
|
+
// ── HANDLER_CONFIG ──────────────────────────────────────────────────────────
|
|
76
|
+
// Written by PluginRuntime_Builder.forPluginEventCollector; the field-by-field
|
|
77
|
+
// schema is documented in EventCollectorEntryPoint.mjs. Explicitly decoded (no
|
|
78
|
+
// blanket cast) so defaults land exactly where the former JS normalizer put
|
|
79
|
+
// them and a malformed deploy fails with the same named errors.
|
|
80
|
+
|
|
81
|
+
type extensionPointConfig = {
|
|
82
|
+
specModule: string,
|
|
83
|
+
mappingsModule: string,
|
|
84
|
+
eventTopicArn: string,
|
|
85
|
+
aggregateNames: array<string>,
|
|
86
|
+
}
|
|
87
|
+
type connectExtensionConfig = {
|
|
88
|
+
specModule: string,
|
|
89
|
+
mappingsModule: string,
|
|
90
|
+
extensionPointName: string,
|
|
91
|
+
}
|
|
92
|
+
type extensionConfig = {
|
|
93
|
+
name: string,
|
|
94
|
+
specModule: string,
|
|
95
|
+
mappingsModule: string,
|
|
96
|
+
delegateModule: string,
|
|
97
|
+
extensionPointName: string,
|
|
98
|
+
aggregateNames: array<string>,
|
|
99
|
+
readModelNames: array<string>,
|
|
100
|
+
}
|
|
101
|
+
type handlerConfig = {
|
|
102
|
+
queueUrl: string,
|
|
103
|
+
pluginExtensionPointCmdTopicUrl: string,
|
|
104
|
+
eventTopicArn: string,
|
|
105
|
+
pluginReadModelTableName: string,
|
|
106
|
+
appSyncApiId: string,
|
|
107
|
+
schedulerRoleArn: string,
|
|
108
|
+
schedulerQueueArn: string,
|
|
109
|
+
schedulerQueueName: string,
|
|
110
|
+
extensionPoints: array<extensionPointConfig>,
|
|
111
|
+
connectExtension: option<connectExtensionConfig>,
|
|
112
|
+
extensions: array<extensionConfig>,
|
|
113
|
+
publishToAggregates: dict<string>,
|
|
114
|
+
readModelQueueUrls: dict<string>,
|
|
115
|
+
readModelNamesForSourceName: dict<array<string>>,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let strOf = (obj: dict<JSON.t>, key: string): option<string> =>
|
|
119
|
+
obj->Dict.get(key)->Option.flatMap(JSON.Decode.string)
|
|
120
|
+
let strArrOf = (obj: dict<JSON.t>, key: string): array<string> =>
|
|
121
|
+
obj->Dict.get(key)->Option.flatMap(JSON.Decode.array)->Option.getOr([])->Array.filterMap(JSON.Decode.string)
|
|
122
|
+
let strDictOf = (obj: dict<JSON.t>, key: string): dict<string> =>
|
|
123
|
+
obj
|
|
124
|
+
->Dict.get(key)
|
|
125
|
+
->Option.flatMap(JSON.Decode.object)
|
|
126
|
+
->Option.getOr(Dict.make())
|
|
127
|
+
->Dict.toArray
|
|
128
|
+
->Array.filterMap(((k, v)) => v->JSON.Decode.string->Option.map(s => (k, s)))
|
|
129
|
+
->Dict.fromArray
|
|
130
|
+
|
|
131
|
+
let parseHandlerConfig = (rawJson: string): handlerConfig => {
|
|
132
|
+
if rawJson == "" {
|
|
133
|
+
JsError.throwWithMessage("HANDLER_CONFIG env var is empty")
|
|
134
|
+
}
|
|
135
|
+
let json = try rawJson->JSON.parseOrThrow catch {
|
|
136
|
+
| exn => JsError.throwWithMessage("HANDLER_CONFIG JSON parse error: " ++ exnMessage(exn))
|
|
137
|
+
}
|
|
138
|
+
let obj = switch json->JSON.Decode.object {
|
|
139
|
+
| Some(o) => o
|
|
140
|
+
| None => JsError.throwWithMessage("HANDLER_CONFIG is not a JSON object")
|
|
141
|
+
}
|
|
142
|
+
[
|
|
143
|
+
"queueUrl",
|
|
144
|
+
"pluginExtensionPointCmdTopicUrl",
|
|
145
|
+
"eventTopicArn",
|
|
146
|
+
"pluginReadModelTableName",
|
|
147
|
+
"appSyncApiId",
|
|
148
|
+
"schedulerRoleArn",
|
|
149
|
+
"schedulerQueueArn",
|
|
150
|
+
"schedulerQueueName",
|
|
151
|
+
"extensionPoints",
|
|
152
|
+
"extensions",
|
|
153
|
+
"publishToAggregates",
|
|
154
|
+
]->Array.forEach(k =>
|
|
155
|
+
if obj->Dict.get(k)->Option.isNone {
|
|
156
|
+
JsError.throwWithMessage("HANDLER_CONFIG missing required field: " ++ k)
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
let extensionPoints = switch obj->Dict.get("extensionPoints")->Option.flatMap(JSON.Decode.array) {
|
|
160
|
+
| None => JsError.throwWithMessage("HANDLER_CONFIG.extensionPoints must be an array")
|
|
161
|
+
| Some(items) =>
|
|
162
|
+
items->Array.filterMap(item =>
|
|
163
|
+
item
|
|
164
|
+
->JSON.Decode.object
|
|
165
|
+
->Option.map(ep => {
|
|
166
|
+
specModule: ep->strOf("specModule")->Option.getOr(""),
|
|
167
|
+
mappingsModule: ep->strOf("mappingsModule")->Option.getOr(""),
|
|
168
|
+
eventTopicArn: ep->strOf("eventTopicArn")->Option.getOr(""),
|
|
169
|
+
aggregateNames: ep->strArrOf("aggregateNames"),
|
|
170
|
+
})
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
let extensions = switch obj->Dict.get("extensions")->Option.flatMap(JSON.Decode.array) {
|
|
174
|
+
| None => JsError.throwWithMessage("HANDLER_CONFIG.extensions must be an array")
|
|
175
|
+
| Some(items) =>
|
|
176
|
+
items->Array.filterMap(item =>
|
|
177
|
+
item
|
|
178
|
+
->JSON.Decode.object
|
|
179
|
+
->Option.map(ext => {
|
|
180
|
+
let extensionPointName = ext->strOf("extensionPointName")->Option.getOr("")
|
|
181
|
+
{
|
|
182
|
+
// Same fallback chain the former JS normalizer applied.
|
|
183
|
+
name: ext
|
|
184
|
+
->strOf("name")
|
|
185
|
+
->Option.getOr(extensionPointName == "" ? "unknown" : extensionPointName),
|
|
186
|
+
specModule: ext->strOf("specModule")->Option.getOr(""),
|
|
187
|
+
mappingsModule: ext->strOf("mappingsModule")->Option.getOr(""),
|
|
188
|
+
delegateModule: ext->strOf("delegateModule")->Option.getOr(""),
|
|
189
|
+
extensionPointName,
|
|
190
|
+
aggregateNames: ext->strArrOf("aggregateNames"),
|
|
191
|
+
readModelNames: ext->strArrOf("readModelNames"),
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
// connectExtension is null (admin) or absent in older configs — both → None.
|
|
197
|
+
let connectExtension =
|
|
198
|
+
obj
|
|
199
|
+
->Dict.get("connectExtension")
|
|
200
|
+
->Option.flatMap(JSON.Decode.object)
|
|
201
|
+
->Option.map(ce => {
|
|
202
|
+
specModule: ce->strOf("specModule")->Option.getOr(""),
|
|
203
|
+
mappingsModule: ce->strOf("mappingsModule")->Option.getOr(""),
|
|
204
|
+
extensionPointName: ce->strOf("extensionPointName")->Option.getOr(""),
|
|
205
|
+
})
|
|
206
|
+
let readModelNamesForSourceName =
|
|
207
|
+
obj
|
|
208
|
+
->Dict.get("readModelNamesForSourceName")
|
|
209
|
+
->Option.flatMap(JSON.Decode.object)
|
|
210
|
+
->Option.getOr(Dict.make())
|
|
211
|
+
->Dict.toArray
|
|
212
|
+
->Array.map(((k, v)) => (
|
|
213
|
+
k,
|
|
214
|
+
v->JSON.Decode.array->Option.getOr([])->Array.filterMap(JSON.Decode.string),
|
|
215
|
+
))
|
|
216
|
+
->Dict.fromArray
|
|
217
|
+
{
|
|
218
|
+
queueUrl: obj->strOf("queueUrl")->Option.getOr(""),
|
|
219
|
+
pluginExtensionPointCmdTopicUrl: obj->strOf("pluginExtensionPointCmdTopicUrl")->Option.getOr(""),
|
|
220
|
+
eventTopicArn: obj->strOf("eventTopicArn")->Option.getOr(""),
|
|
221
|
+
pluginReadModelTableName: obj->strOf("pluginReadModelTableName")->Option.getOr(""),
|
|
222
|
+
appSyncApiId: obj->strOf("appSyncApiId")->Option.getOr(""),
|
|
223
|
+
schedulerRoleArn: obj->strOf("schedulerRoleArn")->Option.getOr(""),
|
|
224
|
+
schedulerQueueArn: obj->strOf("schedulerQueueArn")->Option.getOr(""),
|
|
225
|
+
schedulerQueueName: obj->strOf("schedulerQueueName")->Option.getOr(""),
|
|
226
|
+
extensionPoints,
|
|
227
|
+
connectExtension,
|
|
228
|
+
extensions,
|
|
229
|
+
publishToAggregates: obj->strDictOf("publishToAggregates"),
|
|
230
|
+
readModelQueueUrls: obj->strDictOf("readModelQueueUrls"),
|
|
231
|
+
readModelNamesForSourceName,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Plugin-RM row projection ────────────────────────────────────────────────
|
|
236
|
+
// The subset of the Plugin read-model state the subscription manager needs.
|
|
237
|
+
// Deliberately sidesteps sury: the state schema marks many fields
|
|
238
|
+
// `@s.matches(js_nullable …)`, but DDB drops undefined attributes on write, so
|
|
239
|
+
// the unmarshalled row arrives with those keys *missing* and sury's strict
|
|
240
|
+
// parser rejects them. Field types reuse the spec's definition records — the
|
|
241
|
+
// projection is a strict subset of Reventless.Plugin.pluginDefinition.
|
|
242
|
+
|
|
243
|
+
type pluginProjection = {
|
|
244
|
+
id: string,
|
|
245
|
+
status: string,
|
|
246
|
+
eventCollector: string,
|
|
247
|
+
extensions: array<Reventless.Plugin.extensionDefinition>,
|
|
248
|
+
extensionPoints: array<Reventless.Plugin.extensionPointDefinition>,
|
|
249
|
+
dcbEventLog: option<Reventless.Plugin.dcbEventLogDefinition>,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let projectPluginRow = (row: JSON.t): option<pluginProjection> =>
|
|
253
|
+
switch row->JSON.Decode.object {
|
|
254
|
+
| None => None
|
|
255
|
+
| Some(obj) =>
|
|
256
|
+
switch (obj->strOf("id"), obj->strOf("status")) {
|
|
257
|
+
| (Some(id), Some(status)) =>
|
|
258
|
+
let dcbEventLog =
|
|
259
|
+
obj
|
|
260
|
+
->Dict.get("dcbEventLog")
|
|
261
|
+
->Option.flatMap(JSON.Decode.object)
|
|
262
|
+
->Option.flatMap(d =>
|
|
263
|
+
switch (d->strOf("name"), d->strOf("eventTopicArn")) {
|
|
264
|
+
| (Some(name), Some(eventTopicArn)) =>
|
|
265
|
+
Some(({name, eventTopicArn}: Reventless.Plugin.dcbEventLogDefinition))
|
|
266
|
+
| _ => None
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
let extensions =
|
|
270
|
+
obj
|
|
271
|
+
->Dict.get("extensions")
|
|
272
|
+
->Option.flatMap(JSON.Decode.array)
|
|
273
|
+
->Option.getOr([])
|
|
274
|
+
->Array.filterMap(e =>
|
|
275
|
+
e
|
|
276
|
+
->JSON.Decode.object
|
|
277
|
+
->Option.flatMap(eo =>
|
|
278
|
+
eo
|
|
279
|
+
->strOf("extensionPointName")
|
|
280
|
+
->Option.map((extensionPointName): Reventless.Plugin.extensionDefinition => {
|
|
281
|
+
name: eo->strOf("name")->Option.getOr(""),
|
|
282
|
+
extensionPointName,
|
|
283
|
+
dcbSources: eo->strArrOf("dcbSources"),
|
|
284
|
+
})
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
let extensionPoints =
|
|
288
|
+
obj
|
|
289
|
+
->Dict.get("extensionPoints")
|
|
290
|
+
->Option.flatMap(JSON.Decode.array)
|
|
291
|
+
->Option.getOr([])
|
|
292
|
+
->Array.filterMap(ep =>
|
|
293
|
+
ep
|
|
294
|
+
->JSON.Decode.object
|
|
295
|
+
->Option.flatMap(epo =>
|
|
296
|
+
switch (epo->strOf("name"), epo->strOf("eventTopic")) {
|
|
297
|
+
| (Some(name), Some(eventTopic)) =>
|
|
298
|
+
Some(
|
|
299
|
+
(
|
|
300
|
+
{
|
|
301
|
+
name,
|
|
302
|
+
commandTopic: epo->strOf("commandTopic")->Option.getOr(""),
|
|
303
|
+
eventTopic,
|
|
304
|
+
}: Reventless.Plugin.extensionPointDefinition
|
|
305
|
+
),
|
|
306
|
+
)
|
|
307
|
+
| _ => None
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
Some({
|
|
312
|
+
id,
|
|
313
|
+
status,
|
|
314
|
+
eventCollector: obj->strOf("eventCollector")->Option.getOr(""),
|
|
315
|
+
extensions,
|
|
316
|
+
extensionPoints,
|
|
317
|
+
dcbEventLog,
|
|
318
|
+
})
|
|
319
|
+
| _ => None
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// A pluginDefinition viewed as a projection (the manage logic reads only the
|
|
324
|
+
// shared subset; `status` is never read for the acting plugin). Array/option
|
|
325
|
+
// fields are js_nullable-normalized — the definition may come from a plain
|
|
326
|
+
// JSON.parse of pluginDefinition.json.
|
|
327
|
+
let projectionOfDefinition = (d: Reventless.Plugin.pluginDefinition): pluginProjection => {
|
|
328
|
+
id: d.id,
|
|
329
|
+
status: "",
|
|
330
|
+
eventCollector: d.eventCollector,
|
|
331
|
+
extensions: d.extensions,
|
|
332
|
+
extensionPoints: d.extensionPoints,
|
|
333
|
+
dcbEventLog: d.dcbEventLog->jsOption,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Cross-plugin SNS subscription manager ───────────────────────────────────
|
|
337
|
+
// When DoConnectPlugin / DoDisconnectPlugin fires on the admin EP mapping, scan
|
|
338
|
+
// the Plugin RM for currently-Connected peers and wire (or tear down) SNS
|
|
339
|
+
// subscriptions between this plugin's EventCollector queue and peer plugins'
|
|
340
|
+
// EP/DCB event topics — in both directions. subscribeQueueToTopic is idempotent
|
|
341
|
+
// (lists existing subscriptions first); unsubscribeQueueFromTopic is
|
|
342
|
+
// best-effort. Failures are logged but do not throw — admin's RM projection
|
|
343
|
+
// still proceeds and cold-start reconciliation re-attempts missed work.
|
|
344
|
+
|
|
345
|
+
type action = [#connect | #disconnect]
|
|
346
|
+
|
|
347
|
+
let pluginNameOfId = (id: string): string => id->String.split("@")->Array.get(0)->Option.getOr("")
|
|
348
|
+
|
|
349
|
+
let trySubscribe = async (queueArn: string, topicArn: string, label: string) =>
|
|
350
|
+
if queueArn != "" && topicArn != "" {
|
|
351
|
+
try {
|
|
352
|
+
let _ = await AwsSdk.SNS_Helpers.subscribeQueueToTopic(queueArn, topicArn)
|
|
353
|
+
logInfo(`subscribed ${label}: ${topicArn} -> ${queueArn}`, {comp: "manageSubscriptions"})
|
|
354
|
+
} catch {
|
|
355
|
+
| exn =>
|
|
356
|
+
logError(
|
|
357
|
+
`subscribe failed ${label}: ${topicArn} -> ${queueArn}`,
|
|
358
|
+
{comp: "manageSubscriptions", detail: exnMessage(exn)},
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let tryUnsubscribe = async (queueArn: string, topicArn: string, label: string) =>
|
|
364
|
+
if queueArn != "" && topicArn != "" {
|
|
365
|
+
try {
|
|
366
|
+
let _ = await AwsSdk.SNS_Helpers.unsubscribeQueueFromTopic(queueArn, topicArn)
|
|
367
|
+
logInfo(`unsubscribed ${label}: ${topicArn} -> ${queueArn}`, {comp: "manageSubscriptions"})
|
|
368
|
+
} catch {
|
|
369
|
+
| exn =>
|
|
370
|
+
logError(
|
|
371
|
+
`unsubscribe failed ${label}: ${topicArn} -> ${queueArn}`,
|
|
372
|
+
{comp: "manageSubscriptions", detail: exnMessage(exn)},
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let connectedFilter: array<Reventless.QueryEngine.Filter.config> = [
|
|
378
|
+
("status", Reventless.QueryEngine.Filter.Contains, Reventless.QueryEngine.String("Connected")),
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
let scanConnectedPeers = async (tableName: string, excludeId: string): array<pluginProjection> => {
|
|
382
|
+
let rows = await scanByTableName(tableName, connectedFilter, 1000)
|
|
383
|
+
rows->Array.filterMap(projectPluginRow)->Array.filter(state => state.id != excludeId)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let makeManageSubscriptions = (tableName: string): option<
|
|
387
|
+
(pluginProjection, action) => promise<unit>,
|
|
388
|
+
> =>
|
|
389
|
+
tableName == "" || tableName == "NOT_AVAILABLE"
|
|
390
|
+
? None
|
|
391
|
+
: Some(
|
|
392
|
+
async (pluginDef, action) => {
|
|
393
|
+
let isConnect = action == #connect
|
|
394
|
+
let op = isConnect ? trySubscribe : tryUnsubscribe
|
|
395
|
+
let peers = await scanConnectedPeers(tableName, pluginDef.id)
|
|
396
|
+
// Subscriptions are wired between SNS topics and SQS queues owned per
|
|
397
|
+
// PLUGIN NAME, not per version (EC queue, EP event topics, and DCB
|
|
398
|
+
// topic ARNs live in the plugin's stack, stable across upgrades).
|
|
399
|
+
// When a superseded version is Retired and its row transitions to
|
|
400
|
+
// Disconnected, this hook fires with #disconnect — but if a newer
|
|
401
|
+
// version of the same plugin is still Connected, the subscriptions
|
|
402
|
+
// are still in use; tearing them down would silently break
|
|
403
|
+
// cross-plugin event flow for the live version. Skip in that case.
|
|
404
|
+
let skip = if isConnect {
|
|
405
|
+
false
|
|
406
|
+
} else {
|
|
407
|
+
let myName = pluginNameOfId(pluginDef.id)
|
|
408
|
+
switch peers->Array.find(p => pluginNameOfId(p.id) == myName) {
|
|
409
|
+
| Some(sibling) =>
|
|
410
|
+
logInfo(
|
|
411
|
+
`disconnect skipped for ${pluginDef.id}: sibling version ${sibling.id} still Connected and shares the same EC queue / EP topics`,
|
|
412
|
+
{comp: "manageSubscriptions"},
|
|
413
|
+
)
|
|
414
|
+
true
|
|
415
|
+
| None => false
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if !skip {
|
|
419
|
+
// This plugin's extensions: peer EP topic → this EC queue.
|
|
420
|
+
for i in 0 to pluginDef.extensions->Array.length - 1 {
|
|
421
|
+
let ext = pluginDef.extensions->Array.getUnsafe(i)
|
|
422
|
+
for j in 0 to peers->Array.length - 1 {
|
|
423
|
+
let peer = peers->Array.getUnsafe(j)
|
|
424
|
+
switch peer.extensionPoints->Array.find(ep => ep.name == ext.extensionPointName) {
|
|
425
|
+
| Some(peerEp) =>
|
|
426
|
+
await op(
|
|
427
|
+
pluginDef.eventCollector,
|
|
428
|
+
peerEp.eventTopic,
|
|
429
|
+
`${ext.extensionPointName} -> ${pluginDef.id}`,
|
|
430
|
+
)
|
|
431
|
+
| None => ()
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// This plugin's EPs: this EP topic → peer EC queues.
|
|
436
|
+
for i in 0 to pluginDef.extensionPoints->Array.length - 1 {
|
|
437
|
+
let ep = pluginDef.extensionPoints->Array.getUnsafe(i)
|
|
438
|
+
for j in 0 to peers->Array.length - 1 {
|
|
439
|
+
let peer = peers->Array.getUnsafe(j)
|
|
440
|
+
switch peer.extensions->Array.find(e => e.extensionPointName == ep.name) {
|
|
441
|
+
| Some(_) => await op(peer.eventCollector, ep.eventTopic, `${ep.name} -> ${peer.id}`)
|
|
442
|
+
| None => ()
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// DCB sources this plugin's extensions consume: owning peer's DCB
|
|
447
|
+
// EventTopic → this EC queue.
|
|
448
|
+
for i in 0 to pluginDef.extensions->Array.length - 1 {
|
|
449
|
+
let ext = pluginDef.extensions->Array.getUnsafe(i)
|
|
450
|
+
for j in 0 to ext.dcbSources->Array.length - 1 {
|
|
451
|
+
let dcbSourceName = ext.dcbSources->Array.getUnsafe(j)
|
|
452
|
+
switch peers->Array.find(p =>
|
|
453
|
+
switch p.dcbEventLog {
|
|
454
|
+
| Some(l) => l.name == dcbSourceName
|
|
455
|
+
| None => false
|
|
456
|
+
}
|
|
457
|
+
) {
|
|
458
|
+
| Some(peer) =>
|
|
459
|
+
switch peer.dcbEventLog {
|
|
460
|
+
| Some(l) =>
|
|
461
|
+
await op(
|
|
462
|
+
pluginDef.eventCollector,
|
|
463
|
+
l.eventTopicArn,
|
|
464
|
+
`dcb ${dcbSourceName} -> ${pluginDef.id}`,
|
|
465
|
+
)
|
|
466
|
+
| None => ()
|
|
467
|
+
}
|
|
468
|
+
| None => ()
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// If THIS plugin has a DCB EventLog: its topic → each consuming
|
|
473
|
+
// peer's EC queue.
|
|
474
|
+
switch pluginDef.dcbEventLog {
|
|
475
|
+
| Some(myDcb) =>
|
|
476
|
+
for j in 0 to peers->Array.length - 1 {
|
|
477
|
+
let peer = peers->Array.getUnsafe(j)
|
|
478
|
+
let consuming =
|
|
479
|
+
peer.extensions->Array.some(e => e.dcbSources->Array.includes(myDcb.name))
|
|
480
|
+
if consuming {
|
|
481
|
+
await op(
|
|
482
|
+
peer.eventCollector,
|
|
483
|
+
myDcb.eventTopicArn,
|
|
484
|
+
`dcb ${myDcb.name} -> ${peer.id}`,
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
| None => ()
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
// The definition-typed view the admin EP functor expects
|
|
495
|
+
// (PluginExtensionPoint_Plugin.Make's `manageSubscriptions` hook).
|
|
496
|
+
let manageForDefinition = (
|
|
497
|
+
manage: option<(pluginProjection, action) => promise<unit>>,
|
|
498
|
+
): option<(Reventless.Plugin.pluginDefinition, action) => promise<unit>> =>
|
|
499
|
+
manage->Option.map(m => (d, a) => m(projectionOfDefinition(d), a))
|
|
500
|
+
|
|
501
|
+
// Cold-start reconciliation — scan the Plugin RM for every Connected plugin and
|
|
502
|
+
// rerun manageSubscriptions(p, #connect) for each. Idempotent at the SNS level;
|
|
503
|
+
// catches subscriptions lost out-of-band. Awaited by the shell inside the same
|
|
504
|
+
// invocation that initialised the Lambda (a floating promise would freeze with
|
|
505
|
+
// the runtime between invocations).
|
|
506
|
+
let reconcileSubscriptionsOnce = async (
|
|
507
|
+
tableName: string,
|
|
508
|
+
manage: option<(pluginProjection, action) => promise<unit>>,
|
|
509
|
+
): unit =>
|
|
510
|
+
switch manage {
|
|
511
|
+
| Some(manage) if tableName != "" && tableName != "NOT_AVAILABLE" =>
|
|
512
|
+
try {
|
|
513
|
+
let rows = await scanByTableName(tableName, connectedFilter, 1000)
|
|
514
|
+
for i in 0 to rows->Array.length - 1 {
|
|
515
|
+
switch projectPluginRow(rows->Array.getUnsafe(i)) {
|
|
516
|
+
| Some(state) =>
|
|
517
|
+
try await manage(state, #connect) catch {
|
|
518
|
+
| exn =>
|
|
519
|
+
logWarn(
|
|
520
|
+
"manageSubscriptions failed",
|
|
521
|
+
{comp: "reconcileSubscriptions", detail: exnMessage(exn)},
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
| None => ()
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
} catch {
|
|
528
|
+
| exn => logWarn("scan failed", {comp: "reconcileSubscriptions", detail: exnMessage(exn)})
|
|
529
|
+
}
|
|
530
|
+
| _ => ()
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ── Operations the shell threads into the (untyped) functor applications ────
|
|
534
|
+
|
|
535
|
+
// Stack name, not the function name — the disconnect schedule's EventBridge
|
|
536
|
+
// rule prefix must be stable across deploys and unique per stack;
|
|
537
|
+
// AWS_LAMBDA_FUNCTION_NAME carries a content hash. PluginExtensionPointEntryPoint
|
|
538
|
+
// instantiates the same admin EP module and must derive this identically.
|
|
539
|
+
let environment = processEnv->Dict.get("Environment")->Option.getOr("unknown")
|
|
540
|
+
|
|
541
|
+
// Only what the admin EP mapping's callHandler needs: sendMessageToChannel for
|
|
542
|
+
// ForwardCommand.
|
|
543
|
+
let runtimeOps: ReventlessCore.PluginRuntimeOperations.operations = {
|
|
544
|
+
messagePublish: {sendMessageToChannel: Util_PluginMessage_Runtime.sendMessage},
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let invalidNameChars = %re("/[^.\-_a-zA-Z0-9]/g")
|
|
548
|
+
let resourceNaming: ReventlessInfra.ResourceNaming.operations = {
|
|
549
|
+
validateName: n => n->String.replaceRegExp(invalidNameChars, "_"),
|
|
550
|
+
urnName: arn =>
|
|
551
|
+
arn->String.split(":")->Array.get(5)->Option.filter(s => s != "")->Option.getOr("unknown"),
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
let makeQueryEngine = (pluginReadModelTableName: string): Reventless.QueryEngine.operations => {
|
|
555
|
+
scan: (~readModelName as _, ~filterConfigs, ~limit) =>
|
|
556
|
+
scanByTableName(pluginReadModelTableName, filterConfigs, limit),
|
|
557
|
+
query: async (
|
|
558
|
+
~readModelName as _,
|
|
559
|
+
~key as _=?,
|
|
560
|
+
~id as _,
|
|
561
|
+
~subIdConfig as _=?,
|
|
562
|
+
~filterConfigs as _=?,
|
|
563
|
+
~ascending as _=?,
|
|
564
|
+
~limit as _=?,
|
|
565
|
+
) => JsError.throwWithMessage("QueryEngine.query not available in bundled Plugin EventCollector"),
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let makeScheduler = (schedulerRoleArn: string): ReventlessCore.Scheduler.operations => {
|
|
569
|
+
createSchedule: ScheduledPublisher_CloudWatchEvents_Runtime.createSchedule(
|
|
570
|
+
~roleArn=schedulerRoleArn,
|
|
571
|
+
),
|
|
572
|
+
deleteSchedule: ScheduledPublisher_CloudWatchEvents_Runtime.deleteSchedule,
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
let makeCommandTopicResources = (config: handlerConfig): array<
|
|
576
|
+
ReventlessInfra.Adapter.resolvedResource,
|
|
577
|
+
> =>
|
|
578
|
+
config.schedulerQueueArn == ""
|
|
579
|
+
? []
|
|
580
|
+
: [
|
|
581
|
+
{
|
|
582
|
+
name: config.schedulerQueueName,
|
|
583
|
+
id: config.schedulerQueueName,
|
|
584
|
+
urn: config.schedulerQueueArn,
|
|
585
|
+
service: "unknown",
|
|
586
|
+
resourceInfo: ReventlessInfra.Adapter.NoInfo,
|
|
587
|
+
role: "",
|
|
588
|
+
region: "",
|
|
589
|
+
resourceType: "",
|
|
590
|
+
configuration: Dict.make(),
|
|
591
|
+
tags: Dict.make(),
|
|
592
|
+
},
|
|
593
|
+
]
|
|
594
|
+
|
|
595
|
+
let makePublishToEventTopic = (eventTopicArn: string): ReventlessCore.EventTopic.publishJson => {
|
|
596
|
+
let topic: Util_SNS_Runtime.resolvedTopic = {
|
|
597
|
+
name: eventTopicArn,
|
|
598
|
+
id: eventTopicArn,
|
|
599
|
+
arn: eventTopicArn,
|
|
600
|
+
}
|
|
601
|
+
(id, meta, json) => EventTopicPublisher_SNS_Runtime.publish(topic, id, meta, json)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
let makePublishJsons = (queueUrl: string): ReventlessCore.CommandTopic.publishJsons => {
|
|
605
|
+
let queue: Util_SQS_Runtime.resolvedQueue = {id: queueUrl, name: queueUrl, arn: ""}
|
|
606
|
+
queue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS_FIFO)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// aggregateName → publishJsons, resolving each queue URL via the env-var name
|
|
610
|
+
// carried in HANDLER_CONFIG.publishToAggregates.
|
|
611
|
+
let buildPublishToAggregates = (map: dict<string>): dict<ReventlessCore.CommandTopic.publishJsons> =>
|
|
612
|
+
map
|
|
613
|
+
->Dict.toArray
|
|
614
|
+
->Array.map(((aggName, envVarName)) => (
|
|
615
|
+
aggName,
|
|
616
|
+
makePublishJsons(processEnv->Dict.get(envVarName)->Option.getOr("")),
|
|
617
|
+
))
|
|
618
|
+
->Dict.fromArray
|
|
619
|
+
|
|
620
|
+
// Per-extension publish dicts, filtered to the extension's declared aggregate /
|
|
621
|
+
// read-model names; entries with no env-var mapping or an empty URL are skipped
|
|
622
|
+
// (same as the former JS `continue`s).
|
|
623
|
+
type extensionDicts = {
|
|
624
|
+
publishToAggregates: dict<ReventlessCore.CommandTopic.publishJsons>,
|
|
625
|
+
publishToReadModels: dict<ReventlessCore.CommandTopic.publishJsons>,
|
|
626
|
+
readModelNamesForSourceName: dict<array<string>>,
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
let extensionPublishDicts = (config: handlerConfig, ext: extensionConfig): extensionDicts => {
|
|
630
|
+
let resolve = (names: array<string>, envVarNames: dict<string>) =>
|
|
631
|
+
names
|
|
632
|
+
->Array.filterMap(name =>
|
|
633
|
+
envVarNames
|
|
634
|
+
->Dict.get(name)
|
|
635
|
+
->Option.flatMap(envVarName => processEnv->Dict.get(envVarName))
|
|
636
|
+
->Option.filter(queueUrl => queueUrl != "")
|
|
637
|
+
->Option.map(queueUrl => (name, makePublishJsons(queueUrl)))
|
|
638
|
+
)
|
|
639
|
+
->Dict.fromArray
|
|
640
|
+
{
|
|
641
|
+
publishToAggregates: resolve(ext.aggregateNames, config.publishToAggregates),
|
|
642
|
+
publishToReadModels: resolve(ext.readModelNames, config.readModelQueueUrls),
|
|
643
|
+
// Scoped to this extension's EP — Extension_Operations uses this dict to
|
|
644
|
+
// decide which RMs receive each incoming event.
|
|
645
|
+
readModelNamesForSourceName: Dict.fromArray([
|
|
646
|
+
(
|
|
647
|
+
ext.extensionPointName,
|
|
648
|
+
config.readModelNamesForSourceName->Dict.get(ext.extensionPointName)->Option.getOr([]),
|
|
649
|
+
),
|
|
650
|
+
]),
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
let warnSkippedExtension = (ext: extensionConfig) =>
|
|
655
|
+
logWarn(
|
|
656
|
+
`extension '${ext.name}' missing module specifier(s); skipping. specModule=${ext.specModule}, mappingsModule=${ext.mappingsModule}, delegateModule=${ext.delegateModule}`,
|
|
657
|
+
{comp: "EventCollectorEntryPoint"},
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
// ── Asset files (bundled alongside index.mjs in the code archive) ───────────
|
|
661
|
+
// pluginDefinition is shipped as a file rather than packed into HANDLER_CONFIG
|
|
662
|
+
// (UpdateFunctionConfiguration has a 5120-byte limit; pluginStructure alone blew
|
|
663
|
+
// past it). process.cwd() is /var/task in Lambda, so a relative URL resolves
|
|
664
|
+
// against the extracted zip.
|
|
665
|
+
|
|
666
|
+
type url
|
|
667
|
+
@new external makeUrl: (string, string) => url = "URL"
|
|
668
|
+
@module("node:fs") external readFileSyncUrl: (url, string) => string = "readFileSync"
|
|
669
|
+
@val @scope("process") external cwd: unit => string = "cwd"
|
|
670
|
+
|
|
671
|
+
// pluginDefinition.json is written at deploy time as a stable JSON literal
|
|
672
|
+
// matching Reventless.Plugin.pluginDefinitionSchema (PluginRuntime_Builder), so
|
|
673
|
+
// the parsed object IS the record shape — cast at the documented contract (the
|
|
674
|
+
// AggregateEntryPoint_Ops.asConnectionConfig pattern). js_nullable option
|
|
675
|
+
// fields may hold null; consumers normalize via jsOption.
|
|
676
|
+
external asPluginDefinition: JSON.t => Reventless.Plugin.pluginDefinition = "%identity"
|
|
677
|
+
|
|
678
|
+
let loadPluginDefinition = (): Reventless.Plugin.pluginDefinition =>
|
|
679
|
+
try {
|
|
680
|
+
readFileSyncUrl(makeUrl("./pluginDefinition.json", `file://${cwd()}/`), "utf-8")
|
|
681
|
+
->JSON.parseOrThrow
|
|
682
|
+
->asPluginDefinition
|
|
683
|
+
} catch {
|
|
684
|
+
| exn =>
|
|
685
|
+
JsError.throwWithMessage(
|
|
686
|
+
"Failed to load pluginDefinition.json from asset zip: " ++ exnMessage(exn),
|
|
687
|
+
)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// The plugin's UI-fragment manifest — its own asset since the manifest no
|
|
691
|
+
// longer rides pluginDefinition (the UiFragmentRegistry slice owns fragment
|
|
692
|
+
// state). JSON null (plugins without a UI) and a missing file (older archives)
|
|
693
|
+
// both map to None.
|
|
694
|
+
let loadUiFragments = (): option<JSON.t> =>
|
|
695
|
+
try {
|
|
696
|
+
switch readFileSyncUrl(makeUrl("./uiFragments.json", `file://${cwd()}/`), "utf-8")->JSON.parseOrThrow {
|
|
697
|
+
| JSON.Null => None
|
|
698
|
+
| json => Some(json)
|
|
699
|
+
}
|
|
700
|
+
} catch {
|
|
701
|
+
| _ => None
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ── Handler-dict assembly + Plugin_Callback wiring ──────────────────────────
|
|
705
|
+
// The shell hands back the handlers its functor applications produced; their
|
|
706
|
+
// types are concrete (Extension/ExtensionPoint_Operations outputs), so from
|
|
707
|
+
// here on everything is typed again.
|
|
708
|
+
|
|
709
|
+
type epHandler = {
|
|
710
|
+
aggregateNames: array<string>,
|
|
711
|
+
outgoingHandler: ReventlessCore.Plugin_Callback.jsonEventsHandler,
|
|
712
|
+
}
|
|
713
|
+
type connectHandler = {
|
|
714
|
+
extensionPointName: string,
|
|
715
|
+
incomingHandler: ReventlessCore.Plugin_Callback.jsonEventsHandler,
|
|
716
|
+
}
|
|
717
|
+
type extensionHandler = {
|
|
718
|
+
extensionPointName: string,
|
|
719
|
+
aggregateNames: array<string>,
|
|
720
|
+
incomingHandler: ReventlessCore.Plugin_Callback.jsonEventsHandler,
|
|
721
|
+
outgoingHandler: ReventlessCore.Plugin_Callback.jsonEventsHandler,
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
type bundleInput = {
|
|
725
|
+
pluginDefinition: Reventless.Plugin.pluginDefinition,
|
|
726
|
+
queueUrl: string,
|
|
727
|
+
epHandlers: array<epHandler>,
|
|
728
|
+
connectExtensionHandler?: connectHandler,
|
|
729
|
+
extensionHandlers: array<extensionHandler>,
|
|
730
|
+
}
|
|
731
|
+
type bundle = {
|
|
732
|
+
sqsHandler: (
|
|
733
|
+
PulumiAws.Lambda.CallbackFunction.event,
|
|
734
|
+
PulumiAws.Lambda.context,
|
|
735
|
+
) => Effect.t<unit, string, unit>,
|
|
736
|
+
comp: string,
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
let pushHandler = (
|
|
740
|
+
d: ReventlessCore.Plugin_Callback.jsonEventsHandlersByService,
|
|
741
|
+
key: string,
|
|
742
|
+
h: ReventlessCore.Plugin_Callback.jsonEventsHandler,
|
|
743
|
+
) =>
|
|
744
|
+
switch d->Dict.get(key) {
|
|
745
|
+
| Some(arr) => arr->Array.push(h)
|
|
746
|
+
| None => d->Dict.set(key, [h])
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
let makeSqsHandlerBundle = (input: bundleInput): bundle => {
|
|
750
|
+
let outgoingExtensionPointEventHandlers: ReventlessCore.Plugin_Callback.jsonEventsHandlersByService = Dict.make()
|
|
751
|
+
input.epHandlers->Array.forEach(eph =>
|
|
752
|
+
eph.aggregateNames->Array.forEach(k =>
|
|
753
|
+
pushHandler(outgoingExtensionPointEventHandlers, k, eph.outgoingHandler)
|
|
754
|
+
)
|
|
755
|
+
)
|
|
756
|
+
let incomingConnectExtensionEventHandlers: ReventlessCore.Plugin_Callback.jsonEventsHandlersByService = switch input.connectExtensionHandler {
|
|
757
|
+
| Some(c) => Dict.fromArray([(c.extensionPointName, [c.incomingHandler])])
|
|
758
|
+
| None => Dict.make()
|
|
759
|
+
}
|
|
760
|
+
let outgoingExtensionEventHandlers: ReventlessCore.Plugin_Callback.jsonEventsHandlersByService = Dict.make()
|
|
761
|
+
let incomingExtensionEventHandlers: ReventlessCore.Plugin_Callback.jsonEventsHandlersByService = Dict.make()
|
|
762
|
+
input.extensionHandlers->Array.forEach(eh => {
|
|
763
|
+
eh.aggregateNames->Array.forEach(k =>
|
|
764
|
+
pushHandler(outgoingExtensionEventHandlers, k, eh.outgoingHandler)
|
|
765
|
+
)
|
|
766
|
+
pushHandler(incomingExtensionEventHandlers, eh.extensionPointName, eh.incomingHandler)
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
module Callback = ReventlessCore.Plugin_Callback.Make({
|
|
770
|
+
let pluginDefinition = input.pluginDefinition
|
|
771
|
+
let incomingConnectExtensionEventHandlers = incomingConnectExtensionEventHandlers
|
|
772
|
+
let outgoingExtensionPointEventHandlers = outgoingExtensionPointEventHandlers
|
|
773
|
+
let outgoingExtensionEventHandlers = outgoingExtensionEventHandlers
|
|
774
|
+
let incomingExtensionEventHandlers = incomingExtensionEventHandlers
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
let queue: Util_SQS_Runtime.resolvedQueue = {
|
|
778
|
+
id: input.queueUrl,
|
|
779
|
+
name: input.queueUrl,
|
|
780
|
+
arn: "",
|
|
781
|
+
}
|
|
782
|
+
{
|
|
783
|
+
sqsHandler: EventCollectorChannel_SQS_Runtime.handleDynamoDbOrSqsEvent(
|
|
784
|
+
queue,
|
|
785
|
+
Callback.handleJsonEvents,
|
|
786
|
+
),
|
|
787
|
+
// Backs both the admin and every per-plugin EventCollector Lambda, so the
|
|
788
|
+
// comp names the plugin whose events it drains — matching the
|
|
789
|
+
// `Plugin(<id>)` shape Plugin_Callback logs under.
|
|
790
|
+
comp: `Plugin(${input.pluginDefinition.id})`,
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ── Exported Lambda handler (dispatch boundary) ─────────────────────────────
|
|
795
|
+
|
|
796
|
+
let makeHandler = (bundlePromise: promise<bundle>) =>
|
|
797
|
+
async (event: PulumiAws.Lambda.CallbackFunction.event, context: PulumiAws.Lambda.context) => {
|
|
798
|
+
setRequestId(context.awsRequestId)
|
|
799
|
+
let records = event.records
|
|
800
|
+
logDebug(
|
|
801
|
+
`processing ${records->Array.length->Int.toString} record(s)`,
|
|
802
|
+
{comp: "PluginEventCollectorRuntime"},
|
|
803
|
+
)
|
|
804
|
+
let bundle = await bundlePromise
|
|
805
|
+
await runEffect(
|
|
806
|
+
bundle.sqsHandler(event, context),
|
|
807
|
+
{
|
|
808
|
+
correlationId: ?extractMetaField(records, "correlationId"),
|
|
809
|
+
causationId: ?extractMetaField(records, "causationId"),
|
|
810
|
+
comp: bundle.comp,
|
|
811
|
+
timestamp: ?extractSentTimestamp(records),
|
|
812
|
+
retryCount: extractRetryCount(records),
|
|
813
|
+
},
|
|
814
|
+
)
|
|
815
|
+
""
|
|
816
|
+
}
|