@reventlessdev/reventless-aws 3.0.0-alpha.342 → 3.0.0-alpha.344
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 +14 -0
- package/package.json +6 -6
- package/src/plugin/runtime/PluginRuntime_Builder.res +6 -7
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +2 -2
- package/src/util/Util_Bundle.res +98 -25
- package/src/util/Util_Bundle.res.mjs +64 -13
- package/src/util/Util_Lambda.res +13 -0
- package/src/util/Util_Lambda.res.mjs +7 -1
- package/tests/EventCollectorConnectLambdaPreviewTest.res +1 -0
- package/tests/EventCollectorConnectLambdaPreviewTest.res.mjs +2 -1
- package/tests/EventCollectorReadyGateTest.res +46 -32
- package/tests/EventCollectorReadyGateTest.res.mjs +19 -13
- package/tests/Util_BundleDependencyClosureTest.res +26 -0
- package/tests/Util_BundleDependencyClosureTest.res.mjs +11 -0
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.344 (2026-09-09)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** the archive asks what the layer holds, not what resolves ([fca352f](https://github.com/ReventlessDev/reventless-core/commit/fca352f7a19f0518a84944d4efd23eb98285f6bf))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.343 (2026-09-08)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** the collector gate waits on the one property an update changes ([2c48193](https://github.com/ReventlessDev/reventless-core/commit/2c481934d58c64b8e637d0df1b051b03cf59a35c))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.342 (2026-09-08)
|
|
7
21
|
|
|
8
22
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.344",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -15,18 +15,18 @@
|
|
|
15
15
|
"@aws-sdk/s3-request-presigner": "3.970.0",
|
|
16
16
|
"sury": "11.0.0-rc.2",
|
|
17
17
|
"uuid": "^13.0.0",
|
|
18
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.16",
|
|
19
18
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
20
19
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
20
|
+
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.11",
|
|
21
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.16",
|
|
21
22
|
"@reventlessdev/rescript-node": "2.0.0-alpha.10",
|
|
22
|
-
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.10",
|
|
23
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
24
23
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
25
24
|
"@reventlessdev/reventless-core": "3.0.0-alpha.260",
|
|
25
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.35",
|
|
26
26
|
"@reventlessdev/reventless-postgres": "3.0.0-alpha.124",
|
|
27
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.161",
|
|
27
28
|
"@reventlessdev/reventless-spec": "3.0.0-alpha.133",
|
|
28
|
-
"@reventlessdev/
|
|
29
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.161"
|
|
29
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"rescript": "12.3.0",
|
|
@@ -677,13 +677,12 @@ module Make = (
|
|
|
677
677
|
~opts,
|
|
678
678
|
)
|
|
679
679
|
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
eventCollectorReadyRef :=
|
|
686
|
-
runtime.parts.lambda->Pulumi.Output.flatMap(fn => fn.arn)->Pulumi.Output.apply(_ => ())
|
|
680
|
+
// Not the resource, and not one of its identifiers: both are known before the
|
|
681
|
+
// update lands, so either opens the gate while the collector is still serving
|
|
682
|
+
// the previous bundle — and the re-detect that follows is answered with the
|
|
683
|
+
// previous definition. `updateLanded` depends on the one property that cannot
|
|
684
|
+
// be known early.
|
|
685
|
+
eventCollectorReadyRef := runtime.parts.lambda->Util_Lambda.updateLanded
|
|
687
686
|
|
|
688
687
|
// Admin-only cross-plugin SNS subscription permissions. The admin EC
|
|
689
688
|
// Lambda's manageSubscriptions hook (Phase 3 Step 1) creates SNS
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Aws from "@pulumi/aws";
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
-
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
6
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
8
7
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
@@ -15,6 +14,7 @@ import * as Heartbeat$ReventlessCore from "@reventlessdev/reventless-core/src/co
|
|
|
15
14
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
16
15
|
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
17
16
|
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
17
|
+
import * as Util_Lambda$ReventlessAws from "../../util/Util_Lambda.res.mjs";
|
|
18
18
|
import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/src/ComponentType.res.mjs";
|
|
19
19
|
import * as EventCollector$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventCollector/EventCollector.res.mjs";
|
|
20
20
|
import * as EventLogBackend$ReventlessAws from "../../adapter/EventLog/EventLogBackend.res.mjs";
|
|
@@ -414,7 +414,7 @@ function Make(EventCollectorChannel) {
|
|
|
414
414
|
eventTopics: eventTopics,
|
|
415
415
|
resources: resources
|
|
416
416
|
}], runtime, opts);
|
|
417
|
-
eventCollectorReadyRef.contents =
|
|
417
|
+
eventCollectorReadyRef.contents = Util_Lambda$ReventlessAws.updateLanded(runtime.parts.lambda);
|
|
418
418
|
let isAdminEventCollector = Stdlib_Option.isNone(Plugin_Helpers$ReventlessCore.eventCollectorContextRef.contents[name]);
|
|
419
419
|
if (isAdminEventCollector) {
|
|
420
420
|
new (Aws.iam.RolePolicy)(name + `-snsManageSubs`, {
|
package/src/util/Util_Bundle.res
CHANGED
|
@@ -349,32 +349,35 @@ let isRuntimeProvided = (specifier: string, ~pkgName: string): bool =>
|
|
|
349
349
|
}
|
|
350
350
|
)
|
|
351
351
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
* what the closure walk below must treat as a starting point rather than as
|
|
364
|
-
* something the runtime provides.
|
|
365
|
-
*/
|
|
366
|
-
let isFrameworkPackage = (pkgName: string): bool =>
|
|
367
|
-
switch frameworkPackageCache->Dict.get(pkgName) {
|
|
368
|
-
| Some(known) => known
|
|
369
|
-
| None =>
|
|
370
|
-
let known = try {
|
|
371
|
-
let _ = localRequire->NodeModule.requireResolve(pkgName ++ "/package.json")
|
|
372
|
-
true
|
|
373
|
-
} catch {
|
|
374
|
-
| _ => false
|
|
352
|
+
/** The framework's own package root: this module ships inside reventless-aws, so
|
|
353
|
+
the nearest package.json walking up from it is reventless-aws's own. */
|
|
354
|
+
let frameworkPackageRoot: string = {
|
|
355
|
+
let filePath = newURL(%raw("import.meta.url"))->pathname
|
|
356
|
+
let dirRef = ref(NodePath.dirname(filePath))
|
|
357
|
+
let foundRef: ref<option<string>> = ref(None)
|
|
358
|
+
while dirRef.contents != "/" && foundRef.contents->Option.isNone {
|
|
359
|
+
if NodeFs.existsSync(NodePath.join([dirRef.contents, "package.json"])) {
|
|
360
|
+
foundRef := Some(dirRef.contents)
|
|
361
|
+
} else {
|
|
362
|
+
dirRef := NodePath.dirname(dirRef.contents)
|
|
375
363
|
}
|
|
376
|
-
|
|
377
|
-
|
|
364
|
+
}
|
|
365
|
+
foundRef.contents->Option.getOr(NodePath.dirname(filePath))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** The `dependencies` a package declares — the edges the layer's own npm install
|
|
369
|
+
follows. devDependencies are deliberately not read: the layer is a production
|
|
370
|
+
install, so a dev-only package is not in it. */
|
|
371
|
+
let declaredDependencies = (packageRoot: string): array<string> =>
|
|
372
|
+
try {
|
|
373
|
+
NodeFs.readFileSync(NodePath.join([packageRoot, "package.json"]))
|
|
374
|
+
->JSON.parseOrThrow
|
|
375
|
+
->JSON.Decode.object
|
|
376
|
+
->Option.flatMap(o => o->Dict.get("dependencies"))
|
|
377
|
+
->Option.flatMap(JSON.Decode.object)
|
|
378
|
+
->Option.mapOr([], Dict.keysToArray)
|
|
379
|
+
} catch {
|
|
380
|
+
| _ => []
|
|
378
381
|
}
|
|
379
382
|
|
|
380
383
|
// The distinct bare packages a package's own bundled files import, memoised per
|
|
@@ -427,6 +430,76 @@ let resolvePackageRootFrom = (~fromRoot: string, pkgName: string): option<string
|
|
|
427
430
|
| _ => None
|
|
428
431
|
}
|
|
429
432
|
|
|
433
|
+
/** Packages the layer carries that reventless-aws does not declare, mirroring
|
|
434
|
+
`includeModules` in the layer builder's config. `@rescript/runtime` is the
|
|
435
|
+
case that matters: it is a transitive of `rescript`, which the layer excludes
|
|
436
|
+
as a build tool, so the production closure misses it while the layer holds
|
|
437
|
+
it. Without this the walk would add 19 MB to every archive.
|
|
438
|
+
|
|
439
|
+
The layer's `excludeModules` are deliberately NOT mirrored. Those are
|
|
440
|
+
deploy-time-only packages (the @pulumi bindings, esbuild, the SSH stack); the
|
|
441
|
+
walk is import-driven, so a package no runtime file imports never comes up. */
|
|
442
|
+
let layerIncludedModules = Set.fromArray(["@rescript/runtime"])
|
|
443
|
+
|
|
444
|
+
// Memo for the framework's dependency closure — one walk per deploy.
|
|
445
|
+
let frameworkClosureCache: ref<option<dict<bool>>> = ref(None)
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The packages the Lambda layer carries: reventless-aws and its transitive
|
|
449
|
+
* `dependencies`, which is exactly what the layer builder npm-installs.
|
|
450
|
+
*
|
|
451
|
+
* Membership rather than resolvability, and the distinction is the whole point.
|
|
452
|
+
* Resolvability asked whether `require.resolve` reaches a package from this
|
|
453
|
+
* module — but Node resolution walks *up*, so in a consumer repo with a hoisted
|
|
454
|
+
* `node_modules` every package at the workspace root answers yes, including the
|
|
455
|
+
* plugin packages and their traits. That made this predicate true for packages
|
|
456
|
+
* the layer has never held: they were dropped from the archive as
|
|
457
|
+
* layer-provided, and the plugin package itself was filtered out of the closure
|
|
458
|
+
* walk's starting points, so its imports were never walked at all. The failure
|
|
459
|
+
* lands at the first command as `Cannot find package`, on a green deploy.
|
|
460
|
+
*
|
|
461
|
+
* Declared edges are followed from the declaring package's own directory, so a
|
|
462
|
+
* pnpm layout resolves the same way the runtime would. A dependency that is
|
|
463
|
+
* declared but not installed still counts as the layer's: the archive is not the
|
|
464
|
+
* place to compensate for an incomplete install.
|
|
465
|
+
*/
|
|
466
|
+
let frameworkPackages = (): dict<bool> =>
|
|
467
|
+
switch frameworkClosureCache.contents {
|
|
468
|
+
| Some(closure) => closure
|
|
469
|
+
| None =>
|
|
470
|
+
let closure = Dict.make()
|
|
471
|
+
let pending = [frameworkPackageRoot]
|
|
472
|
+
let next = ref(0)
|
|
473
|
+
while next.contents < pending->Array.length {
|
|
474
|
+
let root = pending->Array.getUnsafe(next.contents)
|
|
475
|
+
next := next.contents + 1
|
|
476
|
+
declaredDependencies(root)->Array.forEach(dep =>
|
|
477
|
+
if !(closure->Dict.has(dep)) {
|
|
478
|
+
closure->Dict.set(dep, true)
|
|
479
|
+
switch resolvePackageRootFrom(~fromRoot=root, dep) {
|
|
480
|
+
| Some(depRoot) => pending->Array.push(depRoot)
|
|
481
|
+
| None => ()
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
frameworkClosureCache := Some(closure)
|
|
487
|
+
closure
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Whether the deployed runtime already carries this package, so the archive
|
|
492
|
+
* need not: reventless-aws itself, or anything in its dependency closure.
|
|
493
|
+
*
|
|
494
|
+
* A package the *project* resolves but this does not is precisely a user
|
|
495
|
+
* package, which the closure walk below must treat as a starting point rather
|
|
496
|
+
* than as something the runtime provides.
|
|
497
|
+
*/
|
|
498
|
+
let isFrameworkPackage = (pkgName: string): bool =>
|
|
499
|
+
pkgName == "@reventlessdev/reventless-aws" ||
|
|
500
|
+
layerIncludedModules->Set.has(pkgName) ||
|
|
501
|
+
frameworkPackages()->Dict.has(pkgName)
|
|
502
|
+
|
|
430
503
|
/**
|
|
431
504
|
* Carry every package a bundled *user* package imports into the archive, and
|
|
432
505
|
* every package those import in turn.
|
|
@@ -232,22 +232,28 @@ function isRuntimeProvided(specifier, pkgName) {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
let
|
|
235
|
+
let filePath = new URL(import.meta.url).pathname;
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
let dirRef = Nodepath.dirname(filePath);
|
|
238
|
+
|
|
239
|
+
let foundRef;
|
|
240
|
+
|
|
241
|
+
while (dirRef !== "/" && Stdlib_Option.isNone(foundRef)) {
|
|
242
|
+
if (Nodefs.existsSync(Nodepath.join(dirRef, "package.json"))) {
|
|
243
|
+
foundRef = dirRef;
|
|
244
|
+
} else {
|
|
245
|
+
dirRef = Nodepath.dirname(dirRef);
|
|
241
246
|
}
|
|
242
|
-
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
let frameworkPackageRoot = Stdlib_Option.getOr(foundRef, Nodepath.dirname(filePath));
|
|
250
|
+
|
|
251
|
+
function declaredDependencies(packageRoot) {
|
|
243
252
|
try {
|
|
244
|
-
|
|
245
|
-
known$1 = true;
|
|
253
|
+
return Stdlib_Option.mapOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(Nodefs.readFileSync(Nodepath.join(packageRoot, "package.json"), "utf8"))), o => o["dependencies"]), Stdlib_JSON.Decode.object), [], prim => Object.keys(prim));
|
|
246
254
|
} catch (exn) {
|
|
247
|
-
|
|
255
|
+
return [];
|
|
248
256
|
}
|
|
249
|
-
frameworkPackageCache[pkgName] = known$1;
|
|
250
|
-
return known$1;
|
|
251
257
|
}
|
|
252
258
|
|
|
253
259
|
let importedPackagesCache = {};
|
|
@@ -289,6 +295,47 @@ function resolvePackageRootFrom(fromRoot, pkgName) {
|
|
|
289
295
|
}
|
|
290
296
|
}
|
|
291
297
|
|
|
298
|
+
let layerIncludedModules = new Set(["@rescript/runtime"]);
|
|
299
|
+
|
|
300
|
+
let frameworkClosureCache = {
|
|
301
|
+
contents: undefined
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
function frameworkPackages() {
|
|
305
|
+
let closure = frameworkClosureCache.contents;
|
|
306
|
+
if (closure !== undefined) {
|
|
307
|
+
return closure;
|
|
308
|
+
}
|
|
309
|
+
let closure$1 = {};
|
|
310
|
+
let pending = [frameworkPackageRoot];
|
|
311
|
+
let next = 0;
|
|
312
|
+
while (next < pending.length) {
|
|
313
|
+
let root = pending[next];
|
|
314
|
+
next = next + 1 | 0;
|
|
315
|
+
declaredDependencies(root).forEach(dep => {
|
|
316
|
+
if (dep in closure$1) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
closure$1[dep] = true;
|
|
320
|
+
let depRoot = resolvePackageRootFrom(root, dep);
|
|
321
|
+
if (depRoot !== undefined) {
|
|
322
|
+
pending.push(depRoot);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
frameworkClosureCache.contents = closure$1;
|
|
328
|
+
return closure$1;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function isFrameworkPackage(pkgName) {
|
|
332
|
+
if (pkgName === "@reventlessdev/reventless-aws" || layerIncludedModules.has(pkgName)) {
|
|
333
|
+
return true;
|
|
334
|
+
} else {
|
|
335
|
+
return pkgName in frameworkPackages();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
292
339
|
function addImportedPackageClosure(packageDirs) {
|
|
293
340
|
let pending = Object.entries(packageDirs).filter(param => !isFrameworkPackage(param[0]));
|
|
294
341
|
let next = 0;
|
|
@@ -410,11 +457,15 @@ export {
|
|
|
410
457
|
isBareSpecifier,
|
|
411
458
|
nodeBuiltins,
|
|
412
459
|
isRuntimeProvided,
|
|
413
|
-
|
|
414
|
-
|
|
460
|
+
frameworkPackageRoot,
|
|
461
|
+
declaredDependencies,
|
|
415
462
|
importedPackagesCache,
|
|
416
463
|
importedPackages,
|
|
417
464
|
resolvePackageRootFrom,
|
|
465
|
+
layerIncludedModules,
|
|
466
|
+
frameworkClosureCache,
|
|
467
|
+
frameworkPackages,
|
|
468
|
+
isFrameworkPackage,
|
|
418
469
|
addImportedPackageClosure,
|
|
419
470
|
assertRuntimeExtensionImportsResolvable,
|
|
420
471
|
buildCodeArchive,
|
package/src/util/Util_Lambda.res
CHANGED
|
@@ -26,8 +26,21 @@ let fromOutput = (output: Pulumi.Output.t<PulumiAws.Lambda.Function.t>) => {
|
|
|
26
26
|
id: output->Pulumi.Output.flatMap(({id}) => id),
|
|
27
27
|
name: output->Pulumi.Output.flatMap(({name}) => name),
|
|
28
28
|
invokeArn: output->Pulumi.Output.flatMap(({invokeArn}) => invokeArn),
|
|
29
|
+
lastModified: output->Pulumi.Output.flatMap(({lastModified}) => lastModified),
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/** Resolves once the function's update has actually landed — the barrier a
|
|
33
|
+
deploy-time handshake needs before it may publish to that function.
|
|
34
|
+
|
|
35
|
+
It must depend on `lastModified`. The identifiers (`arn`, `id`, `name`,
|
|
36
|
+
`invokeArn`) are equal before and after a code update, so the engine resolves
|
|
37
|
+
them from existing state and a gate built on one of them opens while the
|
|
38
|
+
update is still in flight — measurably, about a second before it even starts.
|
|
39
|
+
`sourceCodeHash` is no better: it is also an input, so it can be known in
|
|
40
|
+
advance for the same reason. */
|
|
41
|
+
let updateLanded = (lambda: Pulumi.Output.t<PulumiAws.Lambda.Function.t>): Pulumi.Output.t<unit> =>
|
|
42
|
+
lambda->Pulumi.Output.flatMap(({lastModified}) => lastModified)->Pulumi.Output.apply(_ => ())
|
|
43
|
+
|
|
31
44
|
let functionToResource = (~tags=?, {id, name, arn}: PulumiAws.Lambda.Function.t): ReventlessInfra.Adapter.resource =>
|
|
32
45
|
ReventlessInfra.Adapter.make(
|
|
33
46
|
~name,
|
|
@@ -26,10 +26,15 @@ function fromOutput(output) {
|
|
|
26
26
|
arn: Output$Pulumi.flatMap(output, param => param.arn),
|
|
27
27
|
id: Output$Pulumi.flatMap(output, param => param.id),
|
|
28
28
|
name: Output$Pulumi.flatMap(output, param => param.name),
|
|
29
|
-
invokeArn: Output$Pulumi.flatMap(output, param => param.invokeArn)
|
|
29
|
+
invokeArn: Output$Pulumi.flatMap(output, param => param.invokeArn),
|
|
30
|
+
lastModified: Output$Pulumi.flatMap(output, param => param.lastModified)
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
function updateLanded(lambda) {
|
|
35
|
+
return Output$Pulumi.flatMap(lambda, param => param.lastModified).apply(param => {});
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
function functionToResource(tags, param) {
|
|
34
39
|
let name = param.name;
|
|
35
40
|
return Adapter$ReventlessInfra.make(name, param.id, param.arn, name.apply(param => AWS$ReventlessAws.Lambda.service), undefined, undefined, undefined, Pulumi.output("aws:lambda:Function"), undefined, tags);
|
|
@@ -40,6 +45,7 @@ export {
|
|
|
40
45
|
toResource,
|
|
41
46
|
fromResource,
|
|
42
47
|
fromOutput,
|
|
48
|
+
updateLanded,
|
|
43
49
|
functionToResource,
|
|
44
50
|
}
|
|
45
51
|
/* @pulumi/aws Not a pure module */
|
|
@@ -110,6 +110,7 @@ let lambda: Pulumi.Output.t<PulumiAws.Lambda.Function.t> = Pulumi.Output.make({
|
|
|
110
110
|
id: "AllStateViewSlices"->Pulumi.Output.make,
|
|
111
111
|
name: "AllStateViewSlices"->Pulumi.Output.make,
|
|
112
112
|
invokeArn: "arn:aws:apigateway:invoke"->Pulumi.Output.make,
|
|
113
|
+
lastModified: "2026-01-01T00:00:00.000+0000"->Pulumi.Output.make,
|
|
113
114
|
})
|
|
114
115
|
|
|
115
116
|
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
@@ -66,7 +66,8 @@ let lambda = Pulumi.output({
|
|
|
66
66
|
arn: Pulumi.output("arn:aws:lambda:eu-west-1:123456789012:function:AllStateViewSlices"),
|
|
67
67
|
id: Pulumi.output("AllStateViewSlices"),
|
|
68
68
|
name: Pulumi.output("AllStateViewSlices"),
|
|
69
|
-
invokeArn: Pulumi.output("arn:aws:apigateway:invoke")
|
|
69
|
+
invokeArn: Pulumi.output("arn:aws:apigateway:invoke"),
|
|
70
|
+
lastModified: Pulumi.output("2026-01-01T00:00:00.000+0000")
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
globalThis.describe("EventCollectorChannel_Helpers.connectLambda under an unknown view table", () => {
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
open JestGlobals
|
|
2
2
|
|
|
3
|
-
// Guards what `
|
|
3
|
+
// Guards what `Util_Lambda.updateLanded` waits for — the barrier the deploy
|
|
4
|
+
// publishes its synthetic re-detect behind.
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// registration keeps the old structure — with no second re-detect coming.
|
|
6
|
+
// That collector Lambda is what ANSWERS the handshake: reached while it is still
|
|
7
|
+
// serving the previous bundle, it replies with the previous definition, Connect
|
|
8
|
+
// sees a definition it already holds and emits nothing, and the registration
|
|
9
|
+
// keeps the old structure — with no second re-detect coming.
|
|
10
10
|
//
|
|
11
|
-
// The
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
11
|
+
// The first attempt at this gate depended on `arn`, and failed in production
|
|
12
|
+
// exactly as it had before: the re-detect went out ~1.2s BEFORE the Lambda update
|
|
13
|
+
// even started. A Lambda's identifiers are equal on both sides of a code update,
|
|
14
|
+
// so the engine resolves them from existing state without waiting for it. The
|
|
15
|
+
// mechanism (`flatMap` awaiting the Output it returns) was never in doubt — the
|
|
16
|
+
// property was.
|
|
17
17
|
//
|
|
18
|
-
// So
|
|
19
|
-
//
|
|
18
|
+
// So these pin the property, not the mechanism. The middle test is the one that
|
|
19
|
+
// fails for an identifier-based gate.
|
|
20
20
|
|
|
21
21
|
// The engine's Output constructor, so a field can be left genuinely pending
|
|
22
|
-
// rather than faked. A resolved promise
|
|
23
|
-
//
|
|
22
|
+
// rather than faked. A resolved promise settles on the next tick either way,
|
|
23
|
+
// which is the difference under test.
|
|
24
24
|
@module("@pulumi/pulumi") @new
|
|
25
25
|
external makeOutput: (
|
|
26
26
|
Set.t<unit>,
|
|
@@ -34,7 +34,13 @@ let pending = (value: 'a): (Pulumi.Output.t<'a>, unit => unit) => {
|
|
|
34
34
|
let settle = ref(() => ())
|
|
35
35
|
let p = Promise.make((resolve, _) => settle := () => resolve(value))
|
|
36
36
|
(
|
|
37
|
-
makeOutput(
|
|
37
|
+
makeOutput(
|
|
38
|
+
Set.make(),
|
|
39
|
+
p,
|
|
40
|
+
Promise.resolve(true),
|
|
41
|
+
Promise.resolve(false),
|
|
42
|
+
Promise.resolve(Set.make()),
|
|
43
|
+
),
|
|
38
44
|
settle.contents,
|
|
39
45
|
)
|
|
40
46
|
}
|
|
@@ -43,14 +49,17 @@ let ticks = async () => await Promise.make((resolve, _) => {
|
|
|
43
49
|
let _ = setTimeout(() => resolve(), 50)
|
|
44
50
|
})
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
/** A collector mid-update, the way the engine presents one: the identifiers are
|
|
53
|
+
already known — they do not change across a code update — and only
|
|
54
|
+
`lastModified` is still pending. `complete` is AWS answering. */
|
|
47
55
|
let deploying = (): (Pulumi.Output.t<PulumiAws.Lambda.Function.t>, unit => unit) => {
|
|
48
|
-
let (
|
|
56
|
+
let (lastModified, complete) = pending("2026-09-08T12:13:41.891+0000")
|
|
49
57
|
let fn: PulumiAws.Lambda.Function.t = {
|
|
50
|
-
arn,
|
|
51
|
-
id: "
|
|
52
|
-
name: "
|
|
58
|
+
arn: "arn:aws:lambda:eu-west-1:123456789012:function:OrderingPluginEventColl"->Pulumi.Output.make,
|
|
59
|
+
id: "OrderingPluginEventColl"->Pulumi.Output.make,
|
|
60
|
+
name: "OrderingPluginEventColl"->Pulumi.Output.make,
|
|
53
61
|
invokeArn: "arn:aws:apigateway:invoke"->Pulumi.Output.make,
|
|
62
|
+
lastModified,
|
|
54
63
|
}
|
|
55
64
|
// `apply` preserves the nested Outputs, which is how the real resource arrives.
|
|
56
65
|
(()->Pulumi.Output.make->Pulumi.Output.apply(_ => fn), complete)
|
|
@@ -62,28 +71,33 @@ let observe = (gate: Pulumi.Output.t<unit>): ref<bool> => {
|
|
|
62
71
|
settled
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
describe("
|
|
66
|
-
|
|
74
|
+
describe("Util_Lambda.updateLanded", () => {
|
|
75
|
+
// The regression. Every identifier on the record is already resolved here, so a
|
|
76
|
+
// gate reading any of them opens immediately — which is the production failure,
|
|
77
|
+
// reproduced.
|
|
78
|
+
testAsync("does not open while only the identifiers are known", async () => {
|
|
67
79
|
let (lambda, _complete) = deploying()
|
|
68
|
-
let settled = lambda->
|
|
80
|
+
let settled = lambda->Util_Lambda.updateLanded->observe
|
|
69
81
|
await ticks()
|
|
70
82
|
expect(settled.contents)->toBe(false)
|
|
71
83
|
})
|
|
72
84
|
|
|
73
|
-
testAsync("
|
|
85
|
+
testAsync("opens once AWS reports the update", async () => {
|
|
74
86
|
let (lambda, complete) = deploying()
|
|
75
|
-
let settled = lambda->
|
|
87
|
+
let settled = lambda->Util_Lambda.updateLanded->observe
|
|
76
88
|
complete()
|
|
77
89
|
await ticks()
|
|
78
90
|
expect(settled.contents)->toBe(true)
|
|
79
91
|
})
|
|
80
92
|
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
testAsync("
|
|
93
|
+
// Kept as the reason the gate is written the way it is: both shapes that were
|
|
94
|
+
// tried before open against a collector still serving the previous bundle.
|
|
95
|
+
testAsync("the shapes that shipped before open too early", async () => {
|
|
84
96
|
let (lambda, _complete) = deploying()
|
|
85
|
-
let
|
|
97
|
+
let onResource = lambda->Pulumi.Output.apply(_ => ())->observe
|
|
98
|
+
let onArn = lambda->Pulumi.Output.flatMap(fn => fn.arn)->Pulumi.Output.apply(_ => ())->observe
|
|
86
99
|
await ticks()
|
|
87
|
-
expect(
|
|
100
|
+
expect(onResource.contents)->toBe(true)
|
|
101
|
+
expect(onArn.contents)->toBe(true)
|
|
88
102
|
})
|
|
89
103
|
})
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
4
4
|
import * as Pulumi from "@pulumi/pulumi";
|
|
5
|
+
import * as Util_Lambda$ReventlessAws from "../src/util/Util_Lambda.res.mjs";
|
|
5
6
|
|
|
6
7
|
function pending(value) {
|
|
7
8
|
let settle = {
|
|
@@ -23,16 +24,18 @@ async function ticks() {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
function deploying() {
|
|
26
|
-
let match = pending("
|
|
27
|
-
let fn_arn =
|
|
28
|
-
let fn_id = Pulumi.output("
|
|
29
|
-
let fn_name = Pulumi.output("
|
|
27
|
+
let match = pending("2026-09-08T12:13:41.891+0000");
|
|
28
|
+
let fn_arn = Pulumi.output("arn:aws:lambda:eu-west-1:123456789012:function:OrderingPluginEventColl");
|
|
29
|
+
let fn_id = Pulumi.output("OrderingPluginEventColl");
|
|
30
|
+
let fn_name = Pulumi.output("OrderingPluginEventColl");
|
|
30
31
|
let fn_invokeArn = Pulumi.output("arn:aws:apigateway:invoke");
|
|
32
|
+
let fn_lastModified = match[0];
|
|
31
33
|
let fn = {
|
|
32
34
|
arn: fn_arn,
|
|
33
35
|
id: fn_id,
|
|
34
36
|
name: fn_name,
|
|
35
|
-
invokeArn: fn_invokeArn
|
|
37
|
+
invokeArn: fn_invokeArn,
|
|
38
|
+
lastModified: fn_lastModified
|
|
36
39
|
};
|
|
37
40
|
return [
|
|
38
41
|
Pulumi.output().apply(() => fn),
|
|
@@ -50,25 +53,28 @@ function observe(gate) {
|
|
|
50
53
|
return settled;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
globalThis.describe("
|
|
54
|
-
globalThis.test("does not
|
|
56
|
+
globalThis.describe("Util_Lambda.updateLanded", () => {
|
|
57
|
+
globalThis.test("does not open while only the identifiers are known", async () => {
|
|
55
58
|
let match = deploying();
|
|
56
|
-
let settled = observe(
|
|
59
|
+
let settled = observe(Util_Lambda$ReventlessAws.updateLanded(match[0]));
|
|
57
60
|
await ticks();
|
|
58
61
|
globalThis.expect(settled.contents).toBe(false);
|
|
59
62
|
});
|
|
60
|
-
globalThis.test("
|
|
63
|
+
globalThis.test("opens once AWS reports the update", async () => {
|
|
61
64
|
let match = deploying();
|
|
62
|
-
let settled = observe(
|
|
65
|
+
let settled = observe(Util_Lambda$ReventlessAws.updateLanded(match[0]));
|
|
63
66
|
match[1]();
|
|
64
67
|
await ticks();
|
|
65
68
|
globalThis.expect(settled.contents).toBe(true);
|
|
66
69
|
});
|
|
67
|
-
globalThis.test("
|
|
70
|
+
globalThis.test("the shapes that shipped before open too early", async () => {
|
|
68
71
|
let match = deploying();
|
|
69
|
-
let
|
|
72
|
+
let lambda = match[0];
|
|
73
|
+
let onResource = observe(lambda.apply(param => {}));
|
|
74
|
+
let onArn = observe(Output$Pulumi.flatMap(lambda, fn => fn.arn).apply(param => {}));
|
|
70
75
|
await ticks();
|
|
71
|
-
globalThis.expect(
|
|
76
|
+
globalThis.expect(onResource.contents).toBe(true);
|
|
77
|
+
globalThis.expect(onArn.contents).toBe(true);
|
|
72
78
|
});
|
|
73
79
|
});
|
|
74
80
|
|
|
@@ -100,6 +100,32 @@ describe("Util_Bundle — bundled user packages carry their imports", () => {
|
|
|
100
100
|
expect(bundled->Array.includes("@fixture/never-installed"))->toBe(false)
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
+
testSync("a package the framework merely resolves is still bundled", () => {
|
|
104
|
+
// The layer carries reventless-aws's dependency closure and nothing else.
|
|
105
|
+
// A consumer with a hoisted node_modules puts every package at the root,
|
|
106
|
+
// where the framework's own resolution reaches it by walking up — so
|
|
107
|
+
// resolvability said "the layer has it" about packages the layer has never
|
|
108
|
+
// held, and they were dropped from the archive. `sury` above is genuinely a
|
|
109
|
+
// framework dependency; these are not, and must ride along.
|
|
110
|
+
expect(Util_Bundle.isFrameworkPackage("@fixture/closure-trait"))->toBe(false)
|
|
111
|
+
// A real, published, @reventlessdev-scoped package that is not a dependency
|
|
112
|
+
// of reventless-aws: scope is not what decides this.
|
|
113
|
+
expect(Util_Bundle.isFrameworkPackage("@reventlessdev/reventless-local"))->toBe(false)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
testSync("the framework's own closure is recognised", () => {
|
|
117
|
+
expect(Util_Bundle.isFrameworkPackage("@reventlessdev/reventless-aws"))->toBe(true)
|
|
118
|
+
expect(Util_Bundle.isFrameworkPackage("sury"))->toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
testSync("a package the layer adds outside that closure counts as the layer's", () => {
|
|
122
|
+
// @rescript/runtime is a transitive of `rescript`, which the layer excludes
|
|
123
|
+
// as a build tool, so it is absent from the production closure while the
|
|
124
|
+
// layer carries it explicitly. Every compiled ReScript file imports it, and
|
|
125
|
+
// it is 19 MB — treating it as a user package puts that in every archive.
|
|
126
|
+
expect(Util_Bundle.isFrameworkPackage("@rescript/runtime"))->toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
103
129
|
testSync("framework packages are starting points, not walked", () => {
|
|
104
130
|
// Walking reventless-aws would reach its deploy-time-only imports (the
|
|
105
131
|
// @pulumi bindings) and add tens of megabytes to every archive.
|
|
@@ -106,6 +106,17 @@ globalThis.describe("Util_Bundle — bundled user packages carry their imports",
|
|
|
106
106
|
let bundled = Object.keys((Util_Bundle$ReventlessAws.addImportedPackageClosure(packageDirs), packageDirs));
|
|
107
107
|
globalThis.expect(bundled.includes("@fixture/never-installed")).toBe(false);
|
|
108
108
|
});
|
|
109
|
+
globalThis.test("a package the framework merely resolves is still bundled", () => {
|
|
110
|
+
globalThis.expect(Util_Bundle$ReventlessAws.isFrameworkPackage("@fixture/closure-trait")).toBe(false);
|
|
111
|
+
globalThis.expect(Util_Bundle$ReventlessAws.isFrameworkPackage("@reventlessdev/reventless-local")).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
globalThis.test("the framework's own closure is recognised", () => {
|
|
114
|
+
globalThis.expect(Util_Bundle$ReventlessAws.isFrameworkPackage("@reventlessdev/reventless-aws")).toBe(true);
|
|
115
|
+
globalThis.expect(Util_Bundle$ReventlessAws.isFrameworkPackage("sury")).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
globalThis.test("a package the layer adds outside that closure counts as the layer's", () => {
|
|
118
|
+
globalThis.expect(Util_Bundle$ReventlessAws.isFrameworkPackage("@rescript/runtime")).toBe(true);
|
|
119
|
+
});
|
|
109
120
|
globalThis.test("framework packages are starting points, not walked", () => {
|
|
110
121
|
let packageDirs = Object.fromEntries([[
|
|
111
122
|
"@reventlessdev/reventless-aws",
|