@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 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.342",
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.10",
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-core": "3.0.0-alpha.260",
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-interop": "3.0.0-alpha.35",
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
- // Through an output property, not the resource. `apply` receives the Function
681
- // record as soon as the outer Output carries it — at construction, fields
682
- // still unresolved — so discarding it reported ready mid-update, and the
683
- // re-detect reached a Lambda still serving the previous bundle. `flatMap`
684
- // returns the Output itself, which Pulumi awaits.
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 = Output$Pulumi.flatMap(runtime.parts.lambda, fn => fn.arn).apply(param => {});
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`, {
@@ -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 `eventCollectorReadyRef` waits for.
3
+ // Guards what `Util_Lambda.updateLanded` waits for — the barrier the deploy
4
+ // publishes its synthetic re-detect behind.
4
5
  //
5
- // The deploy publishes a synthetic re-detect once the EventCollector Lambda is
6
- // ready, because that Lambda is what ANSWERS the handshake: reached while it is
7
- // still serving the previous bundle, it replies with the previous definition,
8
- // Connect sees a definition it already holds and emits nothing, and the
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 gate used to be `lambda->Output.apply(_ => ())`. `apply` receives the
12
- // Function record as soon as the OUTER Output carries it, which is at
13
- // construction, with every field inside still an unresolved Output — so the gate
14
- // settled while the update was in flight and the whole barrier was inert. No
15
- // deploy-shaped test catches it: the answer is only stale when a deploy changes a
16
- // structure, and the race is won more often than it is lost.
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 this asserts the barrier directly — the gate must not settle until the
19
- // resource's own outputs do.
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 would settle on the next tick either
23
- // way, which is the difference under test.
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(Set.make(), p, Promise.resolve(true), Promise.resolve(false), Promise.resolve(Set.make())),
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
- // A Lambda mid-update: the resource record exists, its outputs do not yet.
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 (arn, complete) = pending("arn:aws:lambda:eu-west-1:123456789012:function:CatalogPluginEventColl")
56
+ let (lastModified, complete) = pending("2026-09-08T12:13:41.891+0000")
49
57
  let fn: PulumiAws.Lambda.Function.t = {
50
- arn,
51
- id: "CatalogPluginEventColl"->Pulumi.Output.make,
52
- name: "CatalogPluginEventColl"->Pulumi.Output.make,
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("the EventCollector readiness gate", () => {
66
- testAsync("does not settle while the function's outputs are still pending", async () => {
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->Pulumi.Output.flatMap(fn => fn.arn)->Pulumi.Output.apply(_ => ())->observe
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("settles once they resolve", async () => {
85
+ testAsync("opens once AWS reports the update", async () => {
74
86
  let (lambda, complete) = deploying()
75
- let settled = lambda->Pulumi.Output.flatMap(fn => fn.arn)->Pulumi.Output.apply(_ => ())->observe
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
- // The shape that shipped, kept as the reason the one above is written the way it
82
- // is: applying to the resource settles against a Lambda still being updated.
83
- testAsync("settles immediately when taken off the resource instead", async () => {
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 settled = lambda->Pulumi.Output.apply(_ => ())->observe
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(settled.contents)->toBe(true)
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("arn:aws:lambda:eu-west-1:123456789012:function:CatalogPluginEventColl");
27
- let fn_arn = match[0];
28
- let fn_id = Pulumi.output("CatalogPluginEventColl");
29
- let fn_name = Pulumi.output("CatalogPluginEventColl");
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("the EventCollector readiness gate", () => {
54
- globalThis.test("does not settle while the function's outputs are still pending", async () => {
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(Output$Pulumi.flatMap(match[0], fn => fn.arn).apply(param => {}));
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("settles once they resolve", async () => {
63
+ globalThis.test("opens once AWS reports the update", async () => {
61
64
  let match = deploying();
62
- let settled = observe(Output$Pulumi.flatMap(match[0], fn => fn.arn).apply(param => {}));
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("settles immediately when taken off the resource instead", async () => {
70
+ globalThis.test("the shapes that shipped before open too early", async () => {
68
71
  let match = deploying();
69
- let settled = observe(match[0].apply(param => {}));
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(settled.contents).toBe(true);
76
+ globalThis.expect(onResource.contents).toBe(true);
77
+ globalThis.expect(onArn.contents).toBe(true);
72
78
  });
73
79
  });
74
80