@reventlessdev/reventless-aws 3.0.0-alpha.342 → 3.0.0-alpha.343
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 +7 -0
- package/package.json +5 -5
- package/src/plugin/runtime/PluginRuntime_Builder.res +6 -7
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +2 -2
- 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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
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.343 (2026-09-08)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** the collector gate waits on the one property an update changes ([2c48193](https://github.com/ReventlessDev/reventless-core/commit/2c481934d58c64b8e637d0df1b051b03cf59a35c))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.342 (2026-09-08)
|
|
7
14
|
|
|
8
15
|
### 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.343",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
20
20
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
21
21
|
"@reventlessdev/rescript-node": "2.0.0-alpha.10",
|
|
22
|
-
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.
|
|
22
|
+
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.11",
|
|
23
23
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
24
24
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
25
|
-
"@reventlessdev/reventless-
|
|
25
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.161",
|
|
26
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.35",
|
|
26
27
|
"@reventlessdev/reventless-postgres": "3.0.0-alpha.124",
|
|
27
28
|
"@reventlessdev/reventless-spec": "3.0.0-alpha.133",
|
|
28
|
-
"@reventlessdev/reventless-
|
|
29
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.161"
|
|
29
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.260"
|
|
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_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
|
|