@reventlessdev/reventless-aws 3.0.0-alpha.303 → 3.0.0-alpha.305

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 CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.305 (2026-08-16)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** move the EventCollector's queue-URL maps out of the environment ([31172cb](https://github.com/ReventlessDev/reventless-core/commit/31172cb793d267729f201794ff785f9f982315fa))
11
+
12
+
13
+ # 3.0.0-alpha.304 (2026-08-16)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **aws:** ship the DCB slice registry in the archive, not the environment ([3cda4cb](https://github.com/ReventlessDev/reventless-core/commit/3cda4cb780848598a436ab8e80843cf15a4d22b6))
18
+
19
+
6
20
  # 3.0.0-alpha.303 (2026-08-16)
7
21
 
8
22
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.303",
3
+ "version": "3.0.0-alpha.305",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -17,11 +17,11 @@
17
17
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
18
18
  "@reventlessdev/rescript-node": "2.0.0-alpha.7",
19
19
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.77",
20
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
20
21
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
21
22
  "@reventlessdev/reventless-core": "3.0.0-alpha.237",
22
23
  "@reventlessdev/reventless-infra": "3.0.0-alpha.143",
23
24
  "@reventlessdev/reventless-interop": "3.0.0-alpha.31",
24
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
25
25
  "@reventlessdev/reventless-postgres": "3.0.0-alpha.101",
26
26
  "@reventlessdev/reventless-spec": "3.0.0-alpha.115"
27
27
  },
@@ -3,6 +3,8 @@
3
3
  // builds shared DcbEventLog operations and per-slice handlers keyed by command type name.
4
4
  // Dual routing: AppSync direct invocation and SQS CommandTopic events.
5
5
 
6
+ import { readFileSync } from "node:fs";
7
+ import { join } from "node:path";
6
8
  import * as Chunk from "effect/Chunk";
7
9
  import * as Effect from "effect/Effect";
8
10
  import * as Stream from "effect/Stream";
@@ -245,6 +247,16 @@ async function buildHandler() {
245
247
  // Lambda always sets HANDLER_CONFIG; the production path is unchanged.
246
248
  if (!configStr) return [null, null, undefined, undefined];
247
249
  const config = JSON.parse(configStr);
250
+ // The slice registry rides in the archive, not the environment — two module
251
+ // specifiers per slice outgrow Lambda's 4KB env-var ceiling (see the asset in
252
+ // StateChangeSliceRuntime_Builder_Single). An inline registry still wins, so a
253
+ // caller that supplies one — every test that drives a config directly — needs
254
+ // no asset on disk. process.cwd() is /var/task, where the archive unpacks.
255
+ if (config.stateChangeSliceModules === undefined) {
256
+ config.stateChangeSliceModules = JSON.parse(
257
+ readFileSync(join(process.cwd(), "sliceModules.json"), "utf-8"),
258
+ );
259
+ }
248
260
  return buildHandlersForConfig(config, { loadModule: dynamicImport });
249
261
  }
250
262
 
@@ -46,12 +46,9 @@
46
46
  // "readModelNames": string[] // RMs this extension may enqueue events into (filter into readModelQueueUrls)
47
47
  // }
48
48
  // ],
49
- // "publishToAggregates": { // aggregateName env-var name holding the aggregate's cmd-topic SQS URL
50
- // "Plugin": "PTA_Plugin_QUEUE_URL"
51
- // },
52
- // "readModelQueueUrls": { // rmName → env-var name holding the RM's EventCollector SQS URL
53
- // "ProductDemands": "PRM_ProductDemands_QUEUE_URL"
54
- // },
49
+ // (publishToAggregates / readModelQueueUrls moved to the queueUrls.json asset
50
+ // one entry per target, so they outgrew the 4KB environment. Each still maps a
51
+ // target name to the PTA_/PRM_ env var holding its SQS URL.)
55
52
  // "readModelNamesForSourceName": { // deploy-side inversion of RM.sourceNames; sourceServiceName → [rmName]
56
53
  // "Ordering.Orders": ["ProductDemands"]
57
54
  // }
@@ -100,6 +97,8 @@ async function buildHandler() {
100
97
  await runtimeExtensionsReady;
101
98
  const config = Ops.parseHandlerConfig(process.env["HANDLER_CONFIG"] || "");
102
99
  const pluginDefinition = Ops.loadPluginDefinition();
100
+ // target → env-var name; the URLs themselves stay in the environment.
101
+ const queueUrlNames = Ops.loadQueueUrlNames();
103
102
 
104
103
  const queryEngine = Ops.makeQueryEngine(config.pluginReadModelTableName);
105
104
  const scheduler = Ops.makeScheduler(config.schedulerRoleArn);
@@ -165,7 +164,7 @@ async function buildHandler() {
165
164
  return { aggregateNames: ep.aggregateNames, outgoingHandler: ops.outgoingJsonEventsHandler };
166
165
  }));
167
166
 
168
- const publishToAggregates = Ops.buildPublishToAggregates(config.publishToAggregates);
167
+ const publishToAggregates = Ops.buildPublishToAggregates(queueUrlNames.publishToAggregates);
169
168
  const publishToPluginExtensionPoint = Ops.makePublishJsons(config.pluginExtensionPointCmdTopicUrl);
170
169
 
171
170
  // Connect extension — exactly one entry per plugin Lambda; null for admin.
@@ -230,7 +229,7 @@ async function buildHandler() {
230
229
  mappings: [transformedMapping],
231
230
  };
232
231
 
233
- const dicts = Ops.extensionPublishDicts(config, ext);
232
+ const dicts = Ops.extensionPublishDicts(config, queueUrlNames, ext);
234
233
  const ops = extensionOperationsMake(epSpec)(mappingsModule)({
235
234
  publishToAggregates: dicts.publishToAggregates,
236
235
  publishToPluginExtensionPoint,
@@ -109,9 +109,16 @@ type handlerConfig = {
109
109
  extensionPoints: array<extensionPointConfig>,
110
110
  connectExtension: option<connectExtensionConfig>,
111
111
  extensions: array<extensionConfig>,
112
+ readModelNamesForSourceName: dict<array<string>>,
113
+ }
114
+
115
+ // The target→env-var maps that used to ride HANDLER_CONFIG, now read from
116
+ // queueUrls.json (PluginRuntime_Builder). Values are env var NAMES, not URLs —
117
+ // the URLs stay in the environment because the builder cannot resolve those
118
+ // Outputs in bulk.
119
+ type queueUrlNames = {
112
120
  publishToAggregates: dict<string>,
113
121
  readModelQueueUrls: dict<string>,
114
- readModelNamesForSourceName: dict<array<string>>,
115
122
  }
116
123
 
117
124
  let strOf = (obj: dict<JSON.t>, key: string): option<string> =>
@@ -149,7 +156,6 @@ let parseHandlerConfig = (rawJson: string): handlerConfig => {
149
156
  "schedulerQueueName",
150
157
  "extensionPoints",
151
158
  "extensions",
152
- "publishToAggregates",
153
159
  ]->Array.forEach(k =>
154
160
  if obj->Dict.get(k)->Option.isNone {
155
161
  JsError.throwWithMessage("HANDLER_CONFIG missing required field: " ++ k)
@@ -225,8 +231,6 @@ let parseHandlerConfig = (rawJson: string): handlerConfig => {
225
231
  extensionPoints,
226
232
  connectExtension,
227
233
  extensions,
228
- publishToAggregates: obj->strDictOf("publishToAggregates"),
229
- readModelQueueUrls: obj->strDictOf("readModelQueueUrls"),
230
234
  readModelNamesForSourceName,
231
235
  }
232
236
  }
@@ -625,7 +629,11 @@ type extensionDicts = {
625
629
  readModelNamesForSourceName: dict<array<string>>,
626
630
  }
627
631
 
628
- let extensionPublishDicts = (config: handlerConfig, ext: extensionConfig): extensionDicts => {
632
+ let extensionPublishDicts = (
633
+ config: handlerConfig,
634
+ names: queueUrlNames,
635
+ ext: extensionConfig,
636
+ ): extensionDicts => {
629
637
  let resolve = (names: array<string>, envVarNames: dict<string>) =>
630
638
  names
631
639
  ->Array.filterMap(name =>
@@ -637,8 +645,8 @@ let extensionPublishDicts = (config: handlerConfig, ext: extensionConfig): exten
637
645
  )
638
646
  ->Dict.fromArray
639
647
  {
640
- publishToAggregates: resolve(ext.aggregateNames, config.publishToAggregates),
641
- publishToReadModels: resolve(ext.readModelNames, config.readModelQueueUrls),
648
+ publishToAggregates: resolve(ext.aggregateNames, names.publishToAggregates),
649
+ publishToReadModels: resolve(ext.readModelNames, names.readModelQueueUrls),
642
650
  // Scoped to this extension's EP — Extension_Operations uses this dict to
643
651
  // decide which RMs receive each incoming event.
644
652
  readModelNamesForSourceName: Dict.fromArray([
@@ -680,6 +688,26 @@ let loadPluginDefinition = (): Reventless.Plugin.pluginDefinition =>
680
688
  )
681
689
  }
682
690
 
691
+ // The target→env-var maps, read once at cold start. A plugin that publishes
692
+ // nowhere writes empty maps rather than omitting the file, so a read failure is
693
+ // a genuinely broken archive and not an empty-map case worth guessing at.
694
+ let loadQueueUrlNames = (): queueUrlNames =>
695
+ try {
696
+ let obj =
697
+ assetPath("queueUrls.json")
698
+ ->NodeFs.readFileSync
699
+ ->JSON.parseOrThrow
700
+ ->JSON.Decode.object
701
+ ->Option.getOr(Dict.make())
702
+ {
703
+ publishToAggregates: obj->strDictOf("publishToAggregates"),
704
+ readModelQueueUrls: obj->strDictOf("readModelQueueUrls"),
705
+ }
706
+ } catch {
707
+ | exn =>
708
+ JsError.throwWithMessage("Failed to load queueUrls.json from asset zip: " ++ exnMessage(exn))
709
+ }
710
+
683
711
  // The plugin's UI-fragment manifest — its own asset since the manifest no
684
712
  // longer rides pluginDefinition (the UiFragmentRegistry slice owns fragment
685
713
  // state). JSON null (plugins without a UI) and a missing file (older archives)
@@ -111,8 +111,7 @@ function parseHandlerConfig(rawJson) {
111
111
  "schedulerQueueArn",
112
112
  "schedulerQueueName",
113
113
  "extensionPoints",
114
- "extensions",
115
- "publishToAggregates"
114
+ "extensions"
116
115
  ].forEach(k => {
117
116
  if (Stdlib_Option.isNone(obj[k])) {
118
117
  return Stdlib_JsError.throwWithMessage("HANDLER_CONFIG missing required field: " + k);
@@ -159,8 +158,6 @@ function parseHandlerConfig(rawJson) {
159
158
  extensionPoints: extensionPoints,
160
159
  connectExtension: connectExtension,
161
160
  extensions: extensions,
162
- publishToAggregates: strDictOf(obj, "publishToAggregates"),
163
- readModelQueueUrls: strDictOf(obj, "readModelQueueUrls"),
164
161
  readModelNamesForSourceName: readModelNamesForSourceName
165
162
  };
166
163
  }
@@ -495,14 +492,14 @@ function buildPublishToAggregates(map) {
495
492
  ]));
496
493
  }
497
494
 
498
- function extensionPublishDicts(config, ext) {
495
+ function extensionPublishDicts(config, names, ext) {
499
496
  let resolve = (names, envVarNames) => Object.fromEntries(Stdlib_Array.filterMap(names, name => Stdlib_Option.map(Stdlib_Option.filter(Stdlib_Option.flatMap(envVarNames[name], envVarName => process.env[envVarName]), queueUrl => queueUrl !== ""), queueUrl => [
500
497
  name,
501
498
  makePublishJsons(queueUrl)
502
499
  ])));
503
500
  return {
504
- publishToAggregates: resolve(ext.aggregateNames, config.publishToAggregates),
505
- publishToReadModels: resolve(ext.readModelNames, config.readModelQueueUrls),
501
+ publishToAggregates: resolve(ext.aggregateNames, names.publishToAggregates),
502
+ publishToReadModels: resolve(ext.readModelNames, names.readModelQueueUrls),
506
503
  readModelNamesForSourceName: Object.fromEntries([[
507
504
  ext.extensionPointName,
508
505
  Stdlib_Option.getOr(config.readModelNamesForSourceName[ext.extensionPointName], [])
@@ -529,6 +526,19 @@ function loadPluginDefinition() {
529
526
  }
530
527
  }
531
528
 
529
+ function loadQueueUrlNames() {
530
+ try {
531
+ let obj = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(JSON.parse(Nodefs.readFileSync(assetPath("queueUrls.json"), "utf8"))), {});
532
+ return {
533
+ publishToAggregates: strDictOf(obj, "publishToAggregates"),
534
+ readModelQueueUrls: strDictOf(obj, "readModelQueueUrls")
535
+ };
536
+ } catch (raw_exn) {
537
+ let exn = Primitive_exceptions.internalToException(raw_exn);
538
+ return Stdlib_JsError.throwWithMessage("Failed to load queueUrls.json from asset zip: " + exnMessage(exn));
539
+ }
540
+ }
541
+
532
542
  function loadUiFragments() {
533
543
  try {
534
544
  let json = JSON.parse(Nodefs.readFileSync(assetPath("uiFragments.json"), "utf8"));
@@ -649,6 +659,7 @@ export {
649
659
  warnSkippedExtension,
650
660
  assetPath,
651
661
  loadPluginDefinition,
662
+ loadQueueUrlNames,
652
663
  loadUiFragments,
653
664
  pushHandler,
654
665
  makeSqsHandlerBundle,
@@ -113,9 +113,16 @@ let forDcbCommandTopic = (
113
113
  envVars->Dict.set("LOG_LEVEL", level->Pulumi.Output.make->Pulumi.Output.asInput)
114
114
  )
115
115
 
116
- // Build HANDLER_CONFIG JSON: array of {spec, behavior} objects so the entry point
116
+ // The slice registry: an array of {spec, behavior} objects so the entry point
117
117
  // can dynamically import both modules and apply the curried StateChangeSlice_Callback.Make
118
118
  // functor (Make(Spec)(Behavior)).
119
+ //
120
+ // Shipped as a `sliceModules.json` asset rather than inline in HANDLER_CONFIG.
121
+ // Two absolute module specifiers per slice is the one term here that grows
122
+ // with the plugin, and a plugin with a dozen-odd slices carries enough of
123
+ // them to pass Lambda's 4KB UpdateFunctionConfiguration ceiling on the whole
124
+ // environment — the same wall `pluginDefinition` hit in PluginRuntime_Builder,
125
+ // and it fails at deploy with the registry already committed to the stack.
119
126
  let sliceModulesJson =
120
127
  slicePaths
121
128
  ->Array.map(((specPath, behaviorPath)) => {
@@ -172,7 +179,9 @@ let forDcbCommandTopic = (
172
179
  Pulumi.Output.all4((dcbTableName, queue.id, pgConnectionFragment, inboundFragment))
173
180
  ->Pulumi.Output.apply(((table, queueUrl, pgFragment, inbFragment)) => {
174
181
  let pluginNameJson = pluginName->JSON.stringifyAny->Option.getOr(`""`)
175
- `{"dcbEventLogTableName":"${table}","queueUrl":"${queueUrl}","pluginName":${pluginNameJson},"stateChangeSliceModules":[${sliceModulesJson}]${pgFragment}${inbFragment}}`
182
+ let json = `{"dcbEventLogTableName":"${table}","queueUrl":"${queueUrl}","pluginName":${pluginNameJson}${pgFragment}${inbFragment}}`
183
+ Util_LambdaEnvBudget.check(~lambdaName=name, ~handlerConfigJson=json)
184
+ json
176
185
  })
177
186
  envVars->Dict.set("HANDLER_CONFIG", handlerConfigJson->Pulumi.Output.asInput)
178
187
 
@@ -205,9 +214,16 @@ let forDcbCommandTopic = (
205
214
  Util_Bundle.resolvePackageRoot("@reventlessdev/reventless-core"),
206
215
  )
207
216
 
217
+ // Carries no Pulumi Output, so the archive is still built synchronously —
218
+ // the audit table names that do are in the inbound fragment, which stays in
219
+ // HANDLER_CONFIG.
220
+ let extraStringAssets = Dict.make()
221
+ extraStringAssets->Dict.set("sliceModules.json", `[${sliceModulesJson}]`)
222
+
208
223
  let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
209
224
  ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs",
210
225
  ~packageDirs,
226
+ ~extraStringAssets,
211
227
  )
212
228
 
213
229
  cfg.sqsBatchSize->Option.forEach(CommandTopicChannel.SQS.setBatchSize)
@@ -11,6 +11,7 @@ import * as DcbBackend$ReventlessAws from "../DcbEventLog/DcbBackend.res.mjs";
11
11
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
12
12
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
13
13
  import * as PgConnection$ReventlessAws from "../Postgres/PgConnection.res.mjs";
14
+ import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBudget.res.mjs";
14
15
  import * as CommandTopicChannel_SQS$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS.res.mjs";
15
16
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
16
17
 
@@ -79,7 +80,9 @@ function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginNa
79
80
  inboundFragment
80
81
  ]).apply(param => {
81
82
  let pluginNameJson = Stdlib_Option.getOr(JSON.stringify(pluginName), `""`);
82
- return `{"dcbEventLogTableName":"` + param[0] + `","queueUrl":"` + param[1] + `","pluginName":` + pluginNameJson + `,"stateChangeSliceModules":[` + sliceModulesJson + `]` + param[2] + param[3] + `}`;
83
+ let json = `{"dcbEventLogTableName":"` + param[0] + `","queueUrl":"` + param[1] + `","pluginName":` + pluginNameJson + param[2] + param[3] + `}`;
84
+ Util_LambdaEnvBudget$ReventlessAws.check(name, json, undefined);
85
+ return json;
83
86
  });
84
87
  envVars["HANDLER_CONFIG"] = handlerConfigJson;
85
88
  let packageDirs = {};
@@ -97,7 +100,9 @@ function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginNa
97
100
  });
98
101
  packageDirs["@reventlessdev/reventless-aws"] = Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws");
99
102
  packageDirs["@reventlessdev/reventless-core"] = Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-core");
100
- let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs", packageDirs, undefined, undefined);
103
+ let extraStringAssets = {};
104
+ extraStringAssets["sliceModules.json"] = `[` + sliceModulesJson + `]`;
105
+ let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs", packageDirs, extraStringAssets, undefined);
101
106
  Stdlib_Option.forEach(cfg.sqsBatchSize, CommandTopicChannel_SQS$ReventlessAws.setBatchSize);
102
107
  let vpcConfig = pgSelection !== undefined ? pgSelection.securityGroupId.apply(sgId => ({
103
108
  subnetIds: pgSelection.subnetIds,
@@ -503,27 +503,13 @@ module Make = (
503
503
  })
504
504
  dict->Dict.set("extensions", JSON.Encode.array(extensionsArr))
505
505
 
506
- let publishToAggregatesDict = Dict.make()
507
- context.publishToAggregates
508
- ->Dict.keysToArray
509
- ->Array.forEach(aggName =>
510
- publishToAggregatesDict->Dict.set(
511
- aggName,
512
- JSON.Encode.string(`PTA_${aggName}_QUEUE_URL`),
513
- )
514
- )
515
- dict->Dict.set("publishToAggregates", JSON.Encode.object(publishToAggregatesDict))
516
-
517
- let readModelQueueUrlsDict = Dict.make()
518
- context.readModelQueueUrls
519
- ->Dict.keysToArray
520
- ->Array.forEach(rmName =>
521
- readModelQueueUrlsDict->Dict.set(
522
- rmName,
523
- JSON.Encode.string(`PRM_${rmName}_QUEUE_URL`),
524
- )
525
- )
526
- dict->Dict.set("readModelQueueUrls", JSON.Encode.object(readModelQueueUrlsDict))
506
+ // publishToAggregates / readModelQueueUrls live in queueUrls.json (see the
507
+ // asset bundle below). Both are one entry per target, so they grow with the
508
+ // plugin, and together with the PTA_/PRM_ vars they name they carried the
509
+ // catalog EventCollector past Lambda's 4KB environment ceiling. Only the
510
+ // maps move: they are built from Dict.keysToArray, so they hold no Pulumi
511
+ // Output and can be serialized here. The URLs themselves cannot follow —
512
+ // see the note on the aggregate walk further down.
527
513
 
528
514
  let rmNamesForSourceDict = Dict.make()
529
515
  context.readModelNamesForSourceName
@@ -536,7 +522,22 @@ module Make = (
536
522
  )
537
523
  dict->Dict.set("readModelNamesForSourceName", JSON.Encode.object(rmNamesForSourceDict))
538
524
 
539
- JSON.Encode.object(dict)->JSON.stringify
525
+ let json = JSON.Encode.object(dict)->JSON.stringify
526
+ // The PTA_/PRM_ vars are the rest of this Lambda's environment and the
527
+ // half that grows with the plugin. Their names are known here; their
528
+ // values are still Outputs, so the budget check estimates those.
529
+ let queueUrlKeys = Array.concat(
530
+ context.publishToAggregates
531
+ ->Dict.keysToArray
532
+ ->Array.map(n => `PTA_${n}_QUEUE_URL`),
533
+ context.readModelQueueUrls->Dict.keysToArray->Array.map(n => `PRM_${n}_QUEUE_URL`),
534
+ )
535
+ Util_LambdaEnvBudget.check(
536
+ ~lambdaName=name,
537
+ ~handlerConfigJson=json,
538
+ ~outputValuedKeys=queueUrlKeys,
539
+ )
540
+ json
540
541
  })
541
542
  envVars->Dict.set("HANDLER_CONFIG", handlerConfigJson->Pulumi.Output.asInput)
542
543
 
@@ -599,6 +600,29 @@ module Make = (
599
600
  ->Pulumi.Output.apply(((pluginDefinitionJson, uiFragmentsJson)) => {
600
601
  let extraStringAssets = Dict.make()
601
602
  extraStringAssets->Dict.set("pluginDefinition.json", pluginDefinitionJson)
603
+ // target name → the env var holding that target's queue URL. The
604
+ // indirection stays (the URLs are Outputs this builder must not resolve
605
+ // in bulk); only the naming map moves out of HANDLER_CONFIG.
606
+ let nameMap = (names: array<string>, prefix: string) => {
607
+ let d = Dict.make()
608
+ names
609
+ ->Array.toSorted(String.compare)
610
+ ->Array.forEach(n => d->Dict.set(n, JSON.Encode.string(`${prefix}_${n}_QUEUE_URL`)))
611
+ JSON.Encode.object(d)
612
+ }
613
+ let queueUrlsDict = Dict.make()
614
+ queueUrlsDict->Dict.set(
615
+ "publishToAggregates",
616
+ nameMap(context.publishToAggregates->Dict.keysToArray, "PTA"),
617
+ )
618
+ queueUrlsDict->Dict.set(
619
+ "readModelQueueUrls",
620
+ nameMap(context.readModelQueueUrls->Dict.keysToArray, "PRM"),
621
+ )
622
+ extraStringAssets->Dict.set(
623
+ "queueUrls.json",
624
+ JSON.Encode.object(queueUrlsDict)->JSON.stringify,
625
+ )
602
626
  // The UI-fragment manifest no longer rides pluginDefinition — ship it as
603
627
  // its own asset ("null" for plugins without a UI) so the bundled Connect
604
628
  // extension can emit RegisterUiFragment in the handshake answer.
@@ -16,6 +16,7 @@ import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/sr
16
16
  import * as EventCollector$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventCollector/EventCollector.res.mjs";
17
17
  import * as EventLogBackend$ReventlessAws from "../../adapter/EventLog/EventLogBackend.res.mjs";
18
18
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
19
+ import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBudget.res.mjs";
19
20
  import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
20
21
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "../../adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs";
21
22
  import * as StateChangeSliceRuntime_Builder_Single$ReventlessAws from "../../adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs";
@@ -323,22 +324,15 @@ function Make(EventCollectorChannel) {
323
324
  return entryDict;
324
325
  });
325
326
  dict["extensions"] = extensionsArr;
326
- let publishToAggregatesDict = {};
327
- Object.keys(context.publishToAggregates).forEach(aggName => {
328
- publishToAggregatesDict[aggName] = `PTA_` + aggName + `_QUEUE_URL`;
329
- });
330
- dict["publishToAggregates"] = publishToAggregatesDict;
331
- let readModelQueueUrlsDict = {};
332
- Object.keys(context.readModelQueueUrls).forEach(rmName => {
333
- readModelQueueUrlsDict[rmName] = `PRM_` + rmName + `_QUEUE_URL`;
334
- });
335
- dict["readModelQueueUrls"] = readModelQueueUrlsDict;
336
327
  let rmNamesForSourceDict = {};
337
328
  Object.entries(context.readModelNamesForSourceName).forEach(param => {
338
329
  rmNamesForSourceDict[param[0]] = param[1].map(prim => prim);
339
330
  });
340
331
  dict["readModelNamesForSourceName"] = rmNamesForSourceDict;
341
- return JSON.stringify(dict);
332
+ let json = JSON.stringify(dict);
333
+ let queueUrlKeys = Object.keys(context.publishToAggregates).map(n => `PTA_` + n + `_QUEUE_URL`).concat(Object.keys(context.readModelQueueUrls).map(n => `PRM_` + n + `_QUEUE_URL`));
334
+ Util_LambdaEnvBudget$ReventlessAws.check(name, json, queueUrlKeys);
335
+ return json;
342
336
  });
343
337
  envVars["HANDLER_CONFIG"] = handlerConfigJson;
344
338
  let packageDirs = {};
@@ -374,6 +368,17 @@ function Make(EventCollectorChannel) {
374
368
  ]).apply(param => {
375
369
  let extraStringAssets = {};
376
370
  extraStringAssets["pluginDefinition.json"] = param[0];
371
+ let nameMap = (names, prefix) => {
372
+ let d = {};
373
+ names.toSorted(Primitive_string.compare).forEach(n => {
374
+ d[n] = prefix + `_` + n + `_QUEUE_URL`;
375
+ });
376
+ return d;
377
+ };
378
+ let queueUrlsDict = {};
379
+ queueUrlsDict["publishToAggregates"] = nameMap(Object.keys(context.publishToAggregates), "PTA");
380
+ queueUrlsDict["readModelQueueUrls"] = nameMap(Object.keys(context.readModelQueueUrls), "PRM");
381
+ extraStringAssets["queueUrls.json"] = JSON.stringify(queueUrlsDict);
377
382
  extraStringAssets["uiFragments.json"] = param[1];
378
383
  return Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/EventCollectorEntryPoint.mjs", packageDirs, extraStringAssets, undefined);
379
384
  });
@@ -0,0 +1,74 @@
1
+ // Deploy-time check on a Lambda's environment size.
2
+ //
3
+ // AWS caps the whole environment of a function at 4096 bytes and rejects the
4
+ // UpdateFunctionConfiguration call that exceeds it. That rejection lands in the
5
+ // middle of a `pulumi up`, after the surrounding resources have already moved,
6
+ // so the stack is left half-updated and the message names a byte count rather
7
+ // than the field that grew. This has now bitten three times: `pluginDefinition`
8
+ // inline in HANDLER_CONFIG, the DCB slice registry, and the EventCollector's
9
+ // publishToAggregates map plus the PTA_ vars it names.
10
+ //
11
+ // The check runs where HANDLER_CONFIG is still a plain string — inside the
12
+ // builders' `Pulumi.Output.apply` — because that is the last point anything is
13
+ // measurable. By the time the env reaches `RuntimeEnvironment_Lambda`, the big
14
+ // values are Outputs, and totalling them there would mean a bulk resolve of the
15
+ // very dict `PluginRuntime_Builder` documents as unsafe to resolve in bulk.
16
+ //
17
+ // So the total is part exact, part estimated:
18
+ // * exact — the fixed framework vars, HANDLER_CONFIG, and every env var NAME
19
+ // * estimated — one allowance per Output-valued env var, since the value (an SQS
20
+ // URL) is not resolved yet
21
+ //
22
+ // Exceeding the limit on the exact part alone is a certainty, and fails the
23
+ // deploy. Exceeding it only once the allowances are added is a likelihood, and
24
+ // warns: a wrong estimate must not block a deploy that would have succeeded.
25
+
26
+ let limit = 4096
27
+
28
+ // Environment, NODE_OPTIONS, ESM_FALLBACK_DIRS, LOG_LEVEL,
29
+ // REVENTLESS_ELEVATED_GROUPS — set for every Lambda by RuntimeEnvironment_Lambda.
30
+ let frameworkVarsBytes = 191
31
+
32
+ // An SQS queue URL: https://sqs.<region>.amazonaws.com/<account>/<name>[.fifo].
33
+ // Rounded up from the longest in the example platforms, so the estimate errs
34
+ // toward warning early rather than reporting a limit breach as headroom.
35
+ let outputValueAllowance = 100
36
+
37
+ let log = ReventlessCore.Logger.fromEnv()
38
+
39
+ /**
40
+ * Check one Lambda's environment against the 4KB limit.
41
+ *
42
+ * `~outputValuedKeys` are env vars whose value is still an unresolved Output —
43
+ * counted by name plus `outputValueAllowance`. Raises when the exact portion
44
+ * alone is over the limit; warns when only the estimate is.
45
+ */
46
+ let check = (
47
+ ~lambdaName: string,
48
+ ~handlerConfigJson: string,
49
+ ~outputValuedKeys: array<string>=[],
50
+ ) => {
51
+ let handlerConfigBytes = String.length("HANDLER_CONFIG") + String.length(handlerConfigJson)
52
+ let keyBytes = outputValuedKeys->Array.reduce(0, (acc, k) => acc + String.length(k))
53
+ let exact = frameworkVarsBytes + handlerConfigBytes + keyBytes
54
+ let estimated = exact + outputValuedKeys->Array.length * outputValueAllowance
55
+
56
+ if exact > limit {
57
+ JsError.throwWithMessage(
58
+ `${lambdaName}: Lambda environment is ${exact->Int.toString} bytes before the ` ++
59
+ `${outputValuedKeys->Array.length->Int.toString} queue-URL vars are resolved, over the ` ++
60
+ `${limit->Int.toString}-byte limit. HANDLER_CONFIG is ${handlerConfigBytes->Int.toString} ` ++
61
+ `bytes of it — move the term that grows with the plugin into a code-archive asset ` ++
62
+ `(Util_Bundle.buildCodeArchive ~extraStringAssets), as pluginDefinition.json and ` ++
63
+ `sliceModules.json already are.`,
64
+ )
65
+ } else if estimated > limit {
66
+ log.warn(
67
+ ~comp="Util_LambdaEnvBudget",
68
+ `${lambdaName}: Lambda environment is about ${estimated->Int.toString} bytes ` ++
69
+ `(${exact->Int.toString} exact + ${outputValuedKeys->Array.length->Int.toString} queue-URL ` ++
70
+ `vars), at or over the ${limit->Int.toString}-byte limit. The deploy may fail on ` ++
71
+ `UpdateFunctionConfiguration; move a plugin-sized term out of HANDLER_CONFIG.`,
72
+ )
73
+ }
74
+ }
@@ -0,0 +1,37 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
5
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
6
+
7
+ let log = Logger$ReventlessCore.fromEnv();
8
+
9
+ function check(lambdaName, handlerConfigJson, outputValuedKeysOpt) {
10
+ let outputValuedKeys = outputValuedKeysOpt !== undefined ? outputValuedKeysOpt : [];
11
+ let handlerConfigBytes = "HANDLER_CONFIG".length + handlerConfigJson.length | 0;
12
+ let keyBytes = Stdlib_Array.reduce(outputValuedKeys, 0, (acc, k) => acc + k.length | 0);
13
+ let exact = (191 + handlerConfigBytes | 0) + keyBytes | 0;
14
+ let estimated = exact + (outputValuedKeys.length * 100 | 0) | 0;
15
+ if (exact > 4096) {
16
+ return Stdlib_JsError.throwWithMessage(lambdaName + `: Lambda environment is ` + exact.toString() + ` bytes before the ` + (outputValuedKeys.length.toString() + ` queue-URL vars are resolved, over the `) + ((4096).toString() + `-byte limit. HANDLER_CONFIG is ` + handlerConfigBytes.toString() + ` `) + `bytes of it — move the term that grows with the plugin into a code-archive asset (Util_Bundle.buildCodeArchive ~extraStringAssets), as pluginDefinition.json and sliceModules.json already are.`);
17
+ } else if (estimated > 4096) {
18
+ return log.warn("Util_LambdaEnvBudget", undefined, lambdaName + `: Lambda environment is about ` + estimated.toString() + ` bytes ` + (`(` + exact.toString() + ` exact + ` + outputValuedKeys.length.toString() + ` queue-URL `) + (`vars), at or over the ` + (4096).toString() + `-byte limit. The deploy may fail on `) + `UpdateFunctionConfiguration; move a plugin-sized term out of HANDLER_CONFIG.`);
19
+ } else {
20
+ return;
21
+ }
22
+ }
23
+
24
+ let limit = 4096;
25
+
26
+ let frameworkVarsBytes = 191;
27
+
28
+ let outputValueAllowance = 100;
29
+
30
+ export {
31
+ limit,
32
+ frameworkVarsBytes,
33
+ outputValueAllowance,
34
+ log,
35
+ check,
36
+ }
37
+ /* log Not a pure module */
@@ -107,7 +107,6 @@ let minimalConfig = (extras: array<(string, JSON.t)>): string =>
107
107
  ("schedulerQueueName", str("")),
108
108
  ("extensionPoints", JSON.Encode.array([])),
109
109
  ("extensions", JSON.Encode.array([])),
110
- ("publishToAggregates", obj([])),
111
110
  ],
112
111
  extras,
113
112
  )
@@ -139,7 +138,6 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
139
138
  let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(minimalConfig([]))
140
139
  expect(c.queueUrl)->toBe("q-url")
141
140
  expect(c.connectExtension->Option.isNone)->toBe(true) // absent → None
142
- expect(c.readModelQueueUrls->Dict.keysToArray->Array.length)->toBe(0)
143
141
  expect(c.readModelNamesForSourceName->Dict.keysToArray->Array.length)->toBe(0)
144
142
  })
145
143
 
@@ -198,7 +196,6 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
198
196
  ]),
199
197
  ]),
200
198
  ),
201
- ("publishToAggregates", obj([("Plugin", str("PTA_Plugin_QUEUE_URL"))])),
202
199
  (
203
200
  "readModelNamesForSourceName",
204
201
  obj([("Ordering.Orders", JSON.Encode.array([str("ProductDemands")]))]),
@@ -208,9 +205,21 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
208
205
  let ep = c.extensionPoints->Array.getUnsafe(0)
209
206
  expect(ep.eventTopicArn)->toBe("arn:ep")
210
207
  expect(ep.aggregateNames)->toEqual(["Plugin"])
211
- expect(c.publishToAggregates->Dict.get("Plugin"))->toEqual(Some("PTA_Plugin_QUEUE_URL"))
212
208
  expect(c.readModelNamesForSourceName->Dict.get("Ordering.Orders"))->toEqual(
213
209
  Some(["ProductDemands"]),
214
210
  )
215
211
  })
212
+
213
+ // The target→env-var maps left HANDLER_CONFIG for queueUrls.json, so a config
214
+ // still carrying them must parse rather than trip the required-field check —
215
+ // that is what a Lambda running an older archive sends.
216
+ testSync("ignores publishToAggregates left over in HANDLER_CONFIG", () => {
217
+ let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(
218
+ minimalConfig([
219
+ ("publishToAggregates", obj([("Plugin", str("PTA_Plugin_QUEUE_URL"))])),
220
+ ("readModelQueueUrls", obj([("ProductDemands", str("PRM_ProductDemands_QUEUE_URL"))])),
221
+ ]),
222
+ )
223
+ expect(c.queueUrl)->toBe("q-url")
224
+ })
216
225
  })
@@ -210,10 +210,6 @@ function minimalConfig(extras) {
210
210
  [
211
211
  "extensions",
212
212
  []
213
- ],
214
- [
215
- "publishToAggregates",
216
- Object.fromEntries([])
217
213
  ]
218
214
  ].concat(extras)));
219
215
  }
@@ -238,7 +234,6 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
238
234
  let c = EventCollectorEntryPoint_Ops$ReventlessAws.parseHandlerConfig(minimalConfig([]));
239
235
  globalThis.expect(c.queueUrl).toBe("q-url");
240
236
  globalThis.expect(Stdlib_Option.isNone(c.connectExtension)).toBe(true);
241
- globalThis.expect(Object.keys(c.readModelQueueUrls).length).toBe(0);
242
237
  globalThis.expect(Object.keys(c.readModelNamesForSourceName).length).toBe(0);
243
238
  });
244
239
  globalThis.test("connectExtension: JSON null (admin) maps to None; a record decodes", () => {
@@ -304,13 +299,6 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
304
299
  ]
305
300
  ])]
306
301
  ],
307
- [
308
- "publishToAggregates",
309
- Object.fromEntries([[
310
- "Plugin",
311
- "PTA_Plugin_QUEUE_URL"
312
- ]])
313
- ],
314
302
  [
315
303
  "readModelNamesForSourceName",
316
304
  Object.fromEntries([[
@@ -322,9 +310,27 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
322
310
  let ep = c.extensionPoints[0];
323
311
  globalThis.expect(ep.eventTopicArn).toBe("arn:ep");
324
312
  globalThis.expect(ep.aggregateNames).toEqual(["Plugin"]);
325
- globalThis.expect(c.publishToAggregates["Plugin"]).toEqual("PTA_Plugin_QUEUE_URL");
326
313
  globalThis.expect(c.readModelNamesForSourceName["Ordering.Orders"]).toEqual(["ProductDemands"]);
327
314
  });
315
+ globalThis.test("ignores publishToAggregates left over in HANDLER_CONFIG", () => {
316
+ let c = EventCollectorEntryPoint_Ops$ReventlessAws.parseHandlerConfig(minimalConfig([
317
+ [
318
+ "publishToAggregates",
319
+ Object.fromEntries([[
320
+ "Plugin",
321
+ "PTA_Plugin_QUEUE_URL"
322
+ ]])
323
+ ],
324
+ [
325
+ "readModelQueueUrls",
326
+ Object.fromEntries([[
327
+ "ProductDemands",
328
+ "PRM_ProductDemands_QUEUE_URL"
329
+ ]])
330
+ ]
331
+ ]));
332
+ globalThis.expect(c.queueUrl).toBe("q-url");
333
+ });
328
334
  });
329
335
 
330
336
  let project = EventCollectorEntryPoint_Ops$ReventlessAws.projectPluginRow;
@@ -0,0 +1,78 @@
1
+ // Guards the deploy-time check on a Lambda's 4KB environment.
2
+ //
3
+ // The check exists because the limit is enforced by AWS mid-`pulumi up`, on a
4
+ // call that names a byte count and not the field that grew. The two cases worth
5
+ // pinning are the ones that decide whether a deploy proceeds: an exact total
6
+ // already over the limit must stop it, and an over-limit total that only appears
7
+ // once the unresolved queue-URL vars are estimated must not — a wrong estimate
8
+ // blocking a good deploy would be worse than the failure it prevents.
9
+
10
+ open JestGlobals
11
+
12
+ let budget = Util_LambdaEnvBudget.limit
13
+
14
+ let throws = (f: unit => 'a): option<string> =>
15
+ try {
16
+ let _ = f()
17
+ None
18
+ } catch {
19
+ | exn => exn->JsExn.fromException->Option.flatMap(JsExn.message)
20
+ }
21
+
22
+ describe("Util_LambdaEnvBudget.check", () => {
23
+ testSync("passes a small environment", () => {
24
+ let msg = throws(() =>
25
+ Util_LambdaEnvBudget.check(~lambdaName="Small", ~handlerConfigJson=`{"queueUrl":"q"}`)
26
+ )
27
+ expect(msg)->toEqual(None)
28
+ })
29
+
30
+ testSync("throws when HANDLER_CONFIG alone exceeds the limit", () => {
31
+ let big = `{"x":"` ++ String.repeat("a", budget) ++ `"}`
32
+ let msg = throws(() => Util_LambdaEnvBudget.check(~lambdaName="TooBig", ~handlerConfigJson=big))
33
+ expect(msg->Option.isSome)->toBe(true)
34
+ // Names the Lambda and points at the fix, not just the byte count.
35
+ expect(msg->Option.mapOr(false, m => m->String.includes("TooBig")))->toBe(true)
36
+ expect(msg->Option.mapOr(false, m => m->String.includes("extraStringAssets")))->toBe(true)
37
+ })
38
+
39
+ testSync("counts the queue-URL var names toward the exact total", () => {
40
+ // Sized so HANDLER_CONFIG alone fits and the var NAMES alone push it over —
41
+ // the names are known at check time, so this must throw rather than warn.
42
+ let keys = Array.make(~length=40, "")->Array.mapWithIndex((_, i) =>
43
+ `PTA_AVeryLongAggregateNameNumber${i->Int.toString}_QUEUE_URL`
44
+ )
45
+ let keyBytes = keys->Array.reduce(0, (acc, k) => acc + String.length(k))
46
+ let fixed = Util_LambdaEnvBudget.frameworkVarsBytes + String.length("HANDLER_CONFIG")
47
+ // Just past the limit on the exact total alone — the names are enough.
48
+ let json = String.repeat("a", budget - fixed - keyBytes + 1)
49
+ let msg = throws(() =>
50
+ Util_LambdaEnvBudget.check(
51
+ ~lambdaName="ManyTargets",
52
+ ~handlerConfigJson=json,
53
+ ~outputValuedKeys=keys,
54
+ )
55
+ )
56
+ expect(msg->Option.isSome)->toBe(true)
57
+ })
58
+
59
+ testSync("only estimating over the limit warns rather than throwing", () => {
60
+ // Exact total sits under the limit; it is the per-URL allowance that crosses
61
+ // it. The deploy must be allowed to proceed and find out.
62
+ let keys = Array.make(~length=20, "")->Array.mapWithIndex((_, i) => `PTA_A${i->Int.toString}_URL`)
63
+ let exact =
64
+ Util_LambdaEnvBudget.frameworkVarsBytes +
65
+ String.length("HANDLER_CONFIG") +
66
+ keys->Array.reduce(0, (acc, k) => acc + String.length(k))
67
+ let room = budget - exact - 100
68
+ let json = String.repeat("a", room)
69
+ let msg = throws(() =>
70
+ Util_LambdaEnvBudget.check(
71
+ ~lambdaName="Estimated",
72
+ ~handlerConfigJson=json,
73
+ ~outputValuedKeys=keys,
74
+ )
75
+ )
76
+ expect(msg)->toEqual(None)
77
+ })
78
+ })
@@ -0,0 +1,55 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
7
+ import * as Util_LambdaEnvBudget$ReventlessAws from "../src/util/Util_LambdaEnvBudget.res.mjs";
8
+
9
+ function throws(f) {
10
+ try {
11
+ f();
12
+ return;
13
+ } catch (raw_exn) {
14
+ let exn = Primitive_exceptions.internalToException(raw_exn);
15
+ return Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message);
16
+ }
17
+ }
18
+
19
+ globalThis.describe("Util_LambdaEnvBudget.check", () => {
20
+ globalThis.test("passes a small environment", () => {
21
+ let msg = throws(() => Util_LambdaEnvBudget$ReventlessAws.check("Small", `{"queueUrl":"q"}`, undefined));
22
+ globalThis.expect(msg).toEqual(undefined);
23
+ });
24
+ globalThis.test("throws when HANDLER_CONFIG alone exceeds the limit", () => {
25
+ let big = `{"x":"` + "a".repeat(Util_LambdaEnvBudget$ReventlessAws.limit) + `"}`;
26
+ let msg = throws(() => Util_LambdaEnvBudget$ReventlessAws.check("TooBig", big, undefined));
27
+ globalThis.expect(Stdlib_Option.isSome(msg)).toBe(true);
28
+ globalThis.expect(Stdlib_Option.mapOr(msg, false, m => m.includes("TooBig"))).toBe(true);
29
+ globalThis.expect(Stdlib_Option.mapOr(msg, false, m => m.includes("extraStringAssets"))).toBe(true);
30
+ });
31
+ globalThis.test("counts the queue-URL var names toward the exact total", () => {
32
+ let keys = Stdlib_Array.make(40, "").map((param, i) => `PTA_AVeryLongAggregateNameNumber` + i.toString() + `_QUEUE_URL`);
33
+ let keyBytes = Stdlib_Array.reduce(keys, 0, (acc, k) => acc + k.length | 0);
34
+ let fixed = Util_LambdaEnvBudget$ReventlessAws.frameworkVarsBytes + "HANDLER_CONFIG".length | 0;
35
+ let json = "a".repeat(((Util_LambdaEnvBudget$ReventlessAws.limit - fixed | 0) - keyBytes | 0) + 1 | 0);
36
+ let msg = throws(() => Util_LambdaEnvBudget$ReventlessAws.check("ManyTargets", json, keys));
37
+ globalThis.expect(Stdlib_Option.isSome(msg)).toBe(true);
38
+ });
39
+ globalThis.test("only estimating over the limit warns rather than throwing", () => {
40
+ let keys = Stdlib_Array.make(20, "").map((param, i) => `PTA_A` + i.toString() + `_URL`);
41
+ let exact = (Util_LambdaEnvBudget$ReventlessAws.frameworkVarsBytes + "HANDLER_CONFIG".length | 0) + Stdlib_Array.reduce(keys, 0, (acc, k) => acc + k.length | 0) | 0;
42
+ let room = (Util_LambdaEnvBudget$ReventlessAws.limit - exact | 0) - 100 | 0;
43
+ let json = "a".repeat(room);
44
+ let msg = throws(() => Util_LambdaEnvBudget$ReventlessAws.check("Estimated", json, keys));
45
+ globalThis.expect(msg).toEqual(undefined);
46
+ });
47
+ });
48
+
49
+ let budget = Util_LambdaEnvBudget$ReventlessAws.limit;
50
+
51
+ export {
52
+ budget,
53
+ throws,
54
+ }
55
+ /* Not a pure module */