@reventlessdev/reventless-infra 3.0.0-alpha.96 → 3.0.0-alpha.97

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.97 (2026-07-11)
7
+
8
+ ### Features
9
+
10
+ * **infra:** add DeployBootstrap seam for generated deploy programs ([f0dc868](https://github.com/ReventlessDev/reventless-core/commit/f0dc8686396d21e7b39667eb0825b2e57fe4dabf))
11
+
12
+
6
13
  # 3.0.0-alpha.96 (2026-07-11)
7
14
 
8
15
  **Note:** Version bump only for package @reventlessdev/reventless-infra
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ [![npm](https://img.shields.io/npm/v/@reventlessdev/reventless-infra.svg?label=npm)](https://www.npmjs.com/package/@reventlessdev/reventless-infra)
2
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
3
+ [![Docs](https://img.shields.io/badge/docs-reventless.dev-blue)](https://docs.reventless.dev)
4
+
5
+ # @reventlessdev/reventless-infra
6
+
7
+ > ⚠️ **Alpha.** APIs and on-disk formats can change without notice between releases.
8
+ > Pin exact versions and expect breaking changes.
9
+
10
+ The **infrastructure type layer** of [Reventless](https://docs.reventless.dev) — a
11
+ spec-driven, event-sourced CQRS framework written in [ReScript](https://rescript-lang.org).
12
+ This package defines the deploy-time infrastructure types and abstract `Platform` interface
13
+ that are shared across storage/cloud adapters, so plugin-assembly code can be written once
14
+ against provider-agnostic types and satisfied by any concrete adapter. It uses
15
+ [Pulumi](https://www.pulumi.com) types for deploy-time resource references. Modules are
16
+ exposed under the `ReventlessInfra` namespace.
17
+
18
+ ## What it provides
19
+
20
+ ReScript modules, consumed by adding the package to your `rescript.json` `dependencies`.
21
+
22
+ **Platform interface & adapters (`types/`, `adapter/`)**
23
+
24
+ - **`Platform`** — the abstract factory interface for platform-agnostic component assembly.
25
+ Plugin code takes a `Platform.T` as a functor argument; a concrete adapter (e.g.
26
+ `reventless-aws`) supplies the implementation at the composition root.
27
+ - **`Adapter`** — deploy-time infrastructure primitives: `resource` (fields wrapped in
28
+ `Pulumi.Output.t`), `resolvedResource` (runtime values), and `resourceInfo` describing a
29
+ resource's structural role (storage keys, stream source, API resolver).
30
+ - **`ResourceNaming`** — provider-agnostic operations for validating and deriving
31
+ URN-safe resource names.
32
+ - **`ExtensionMapping`** / **`ExtensionPointMapping`** — the action types that route commands
33
+ and events between an extension and the extension point it wraps.
34
+ - **`Message`**, **`PluginExtensionPointSpec`**, **`NoEventMappings`** — supporting types for
35
+ message identity and plugin/extension wiring.
36
+
37
+ **Component types (`components/`)**
38
+
39
+ Deploy-time `outputs` (infrastructure references) and runtime `operations` for each Reventless
40
+ component kind — **`Component`** (the typed wrapper around a Pulumi `ComponentResource`),
41
+ **`Plugin`**, **`Aggregate`**, **`ReadModel`**, **`EventLog`** / **`DcbEventLog`**,
42
+ **`CommandTopic`** / **`EventTopic`**, **`CommandGenerator`**, **`EventCollector`**,
43
+ **`EventMapper`**, **`QueryDb`**, **`Api`**, **`Task`**, **`ExtensionPoint`** / **`Extension`**,
44
+ **`Scheduler`**, **`Heartbeat`**, and the DCB slices.
45
+
46
+ ## Where it fits
47
+
48
+ `reventless-infra` sits between the specification layer and the concrete adapters:
49
+
50
+ ```
51
+ reventless-spec (specifications)
52
+
53
+ reventless-infra (shared infrastructure types) ← this package
54
+
55
+ reventless-aws / reventless-postgres / reventless-local (storage & deployment adapters)
56
+ ```
57
+
58
+ - It builds on
59
+ [`@reventlessdev/reventless-spec`](https://www.npmjs.com/package/@reventlessdev/reventless-spec)
60
+ for the domain contracts.
61
+ - Adapters such as
62
+ [`@reventlessdev/reventless-aws`](https://www.npmjs.com/package/@reventlessdev/reventless-aws)
63
+ provide the concrete `Platform.T` implementation these types describe.
64
+
65
+ You normally obtain `reventless-infra` transitively by scaffolding an app rather than
66
+ installing it on its own.
67
+
68
+ ## Install
69
+
70
+ ```bash
71
+ pnpm add @reventlessdev/reventless-infra
72
+ ```
73
+
74
+ Then register it as a ReScript dependency in `rescript.json`:
75
+
76
+ ```json
77
+ {
78
+ "dependencies": ["@reventlessdev/reventless-infra"]
79
+ }
80
+ ```
81
+
82
+ Requires ReScript `^12.3.0` (peer dependency).
83
+
84
+ ## Links
85
+
86
+ - 📚 Documentation — [docs.reventless.dev](https://docs.reventless.dev)
87
+ - 📦 Repository — [ReventlessDev/reventless-core](https://github.com/ReventlessDev/reventless-core)
88
+ - 📋 [Changelog](./CHANGELOG.md)
89
+
90
+ ## License
91
+
92
+ [Apache-2.0](https://opensource.org/licenses/Apache-2.0)
package/package.json CHANGED
@@ -1,19 +1,29 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-infra",
3
- "version": "3.0.0-alpha.96",
3
+ "version": "3.0.0-alpha.97",
4
4
  "description": "Infrastructure types for Reventless framework",
5
5
  "license": "Apache-2.0",
6
+ "jest": {
7
+ "testMatch": [
8
+ "<rootDir>/tests/**/*Test.res.mjs"
9
+ ],
10
+ "moduleFileExtensions": [
11
+ "js",
12
+ "mjs"
13
+ ]
14
+ },
6
15
  "dependencies": {
7
16
  "sury": "11.0.0-alpha.4",
8
17
  "sury-ppx": "11.0.0-alpha.2",
9
18
  "uuid": "^13.0.0",
10
- "@reventlessdev/rescript-effect": "0.1.0-alpha.26",
11
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
12
- "@reventlessdev/reventless-spec": "3.0.0-alpha.74",
13
- "@reventlessdev/rescript-uuid": "1.1.0-alpha.14"
19
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.27",
20
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.15",
21
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.75",
22
+ "@reventlessdev/rescript-uuid": "1.1.0-alpha.15"
14
23
  },
15
24
  "devDependencies": {
16
- "rescript": "^12.3.0"
25
+ "rescript": "^12.3.0",
26
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.7"
17
27
  },
18
28
  "peerDependencies": {
19
29
  "rescript": "^12.3.0"
@@ -30,6 +40,7 @@
30
40
  "build": "rescript build",
31
41
  "start": "rescript watch",
32
42
  "clean": "rescript clean",
33
- "rebuild": "pnpm run clean && pnpm run build"
43
+ "rebuild": "pnpm run clean && pnpm run build",
44
+ "test": "NODE_OPTIONS='--experimental-vm-modules' jest"
34
45
  }
35
46
  }
package/rescript.json CHANGED
@@ -15,6 +15,11 @@
15
15
  {
16
16
  "dir": "src",
17
17
  "subdirs": true
18
+ },
19
+ {
20
+ "dir": "tests",
21
+ "subdirs": true,
22
+ "type": "dev"
18
23
  }
19
24
  ],
20
25
  "dependencies": [
@@ -22,7 +27,8 @@
22
27
  "@reventlessdev/rescript-pulumi-pulumi",
23
28
  "@reventlessdev/rescript-effect",
24
29
  "@reventlessdev/rescript-uuid",
25
- "@reventlessdev/reventless-spec"
30
+ "@reventlessdev/reventless-spec",
31
+ "@reventlessdev/rescript-jest"
26
32
  ],
27
33
  "suffix": ".res.mjs"
28
34
  }
@@ -0,0 +1,83 @@
1
+ /**
2
+ Deploy-time bootstrap seam: a named choke point through which a deploy program
3
+ runs cross-cutting activations at fixed phases, without hand-editing the
4
+ generated `Main.res`.
5
+
6
+ No-op by default. Zero registrations ⇒ `run` does nothing ⇒ existing generated
7
+ and hand-written deploy programs are byte-identical and preview with no resource
8
+ diff. An extension (a backend, an operational package) registers a contribution
9
+ via `register` — at module-load time as an import side effect, or explicitly —
10
+ and the generated program's `run(PreDeploy)` / `run(PostDeploy)` calls fire it in
11
+ registration order.
12
+
13
+ Same shape as `ReventlessCore.Monitoring.use`: a module-level registry consulted
14
+ by an emitted call, so deploy-time extension becomes *registration*, not *file
15
+ editing*. See `docs/plans/deploy-bootstrap-seam.md`.
16
+ */
17
+
18
+ /**
19
+ Ordered phases at which bootstrap contributions run during a deploy program.
20
+ */
21
+ type phase =
22
+ | /** before `deployPlatform` / `deployPlugin` — seam registration and other
23
+ ordering-sensitive activations that must precede the platform/plugin
24
+ graph build */
25
+ PreDeploy
26
+ | /** after the platform/plugin graph is registered — exports, cross-stack
27
+ output emission */
28
+ PostDeploy
29
+
30
+ type contribution = unit => unit
31
+
32
+ let preContributions: ref<array<contribution>> = ref([])
33
+ let postContributions: ref<array<contribution>> = ref([])
34
+ let preRan = ref(false)
35
+ let postRan = ref(false)
36
+
37
+ let registryFor = phase =>
38
+ switch phase {
39
+ | PreDeploy => preContributions
40
+ | PostDeploy => postContributions
41
+ }
42
+
43
+ let ranFor = phase =>
44
+ switch phase {
45
+ | PreDeploy => preRan
46
+ | PostDeploy => postRan
47
+ }
48
+
49
+ /**
50
+ Register a contribution. Contributions run in registration order within a phase.
51
+ Safe to call at module-load time (as an import side effect) or explicitly.
52
+ Defaults to `PreDeploy`.
53
+ */
54
+ let register = (~phase=PreDeploy, contribution: contribution) => {
55
+ let registry = registryFor(phase)
56
+ registry := registry.contents->Array.concat([contribution])
57
+ }
58
+
59
+ /**
60
+ Run all contributions registered for a phase, in registration order. Emitted by
61
+ generated deploy programs around the platform/plugin build. Idempotent per phase:
62
+ running an already-run phase is a no-op, and running a phase with no
63
+ registrations does nothing.
64
+ */
65
+ let run = (phase: phase) => {
66
+ let ran = ranFor(phase)
67
+ if !ran.contents {
68
+ ran := true
69
+ registryFor(phase).contents->Array.forEach(contribution => contribution())
70
+ }
71
+ }
72
+
73
+ /**
74
+ Reset all registries and run-flags. Test-support only — deploy programs never
75
+ call this. Lets a test exercise registration order, phase isolation, and
76
+ idempotency from a clean slate.
77
+ */
78
+ let reset = () => {
79
+ preContributions := []
80
+ postContributions := []
81
+ preRan := false
82
+ postRan := false
83
+ }
@@ -0,0 +1,69 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ let preContributions = {
5
+ contents: []
6
+ };
7
+
8
+ let postContributions = {
9
+ contents: []
10
+ };
11
+
12
+ let preRan = {
13
+ contents: false
14
+ };
15
+
16
+ let postRan = {
17
+ contents: false
18
+ };
19
+
20
+ function registryFor(phase) {
21
+ if (phase === "PreDeploy") {
22
+ return preContributions;
23
+ } else {
24
+ return postContributions;
25
+ }
26
+ }
27
+
28
+ function ranFor(phase) {
29
+ if (phase === "PreDeploy") {
30
+ return preRan;
31
+ } else {
32
+ return postRan;
33
+ }
34
+ }
35
+
36
+ function register(phaseOpt, contribution) {
37
+ let phase = phaseOpt !== undefined ? phaseOpt : "PreDeploy";
38
+ let registry = registryFor(phase);
39
+ registry.contents = registry.contents.concat([contribution]);
40
+ }
41
+
42
+ function run(phase) {
43
+ let ran = ranFor(phase);
44
+ if (!ran.contents) {
45
+ ran.contents = true;
46
+ registryFor(phase).contents.forEach(contribution => contribution());
47
+ return;
48
+ }
49
+ }
50
+
51
+ function reset() {
52
+ preContributions.contents = [];
53
+ postContributions.contents = [];
54
+ preRan.contents = false;
55
+ postRan.contents = false;
56
+ }
57
+
58
+ export {
59
+ preContributions,
60
+ postContributions,
61
+ preRan,
62
+ postRan,
63
+ registryFor,
64
+ ranFor,
65
+ register,
66
+ run,
67
+ reset,
68
+ }
69
+ /* No side effect */
@@ -0,0 +1,72 @@
1
+ open JestGlobals
2
+
3
+ module DB = DeployBootstrap
4
+
5
+ describe("DeployBootstrap", () => {
6
+ // The seam holds module-level state, so each case resets first for isolation.
7
+ describe("run with no registrations", () => {
8
+ testSync("is a no-op that does not throw", () => {
9
+ DB.reset()
10
+ DB.run(PreDeploy)
11
+ DB.run(PostDeploy)
12
+ expect(true)->toBe(true)
13
+ })
14
+ })
15
+
16
+ describe("registration order within a phase", () => {
17
+ testSync("runs contributions in the order they were registered", () => {
18
+ DB.reset()
19
+ let calls = []
20
+ DB.register(() => calls->Array.push("a"))
21
+ DB.register(() => calls->Array.push("b"))
22
+ DB.register(() => calls->Array.push("c"))
23
+ DB.run(PreDeploy)
24
+ expect(calls)->toEqual(["a", "b", "c"])
25
+ })
26
+ })
27
+
28
+ describe("phase isolation", () => {
29
+ testSync("run(PreDeploy) fires only PreDeploy contributions", () => {
30
+ DB.reset()
31
+ let calls = []
32
+ DB.register(~phase=PreDeploy, () => calls->Array.push("pre"))
33
+ DB.register(~phase=PostDeploy, () => calls->Array.push("post"))
34
+ DB.run(PreDeploy)
35
+ expect(calls)->toEqual(["pre"])
36
+ DB.run(PostDeploy)
37
+ expect(calls)->toEqual(["pre", "post"])
38
+ })
39
+
40
+ testSync("defaults to PreDeploy when no phase is given", () => {
41
+ DB.reset()
42
+ let calls = []
43
+ DB.register(() => calls->Array.push("default"))
44
+ DB.run(PostDeploy)
45
+ expect(calls)->toEqual([])
46
+ DB.run(PreDeploy)
47
+ expect(calls)->toEqual(["default"])
48
+ })
49
+ })
50
+
51
+ describe("idempotent run per phase", () => {
52
+ testSync("running an already-run phase does nothing", () => {
53
+ DB.reset()
54
+ let count = ref(0)
55
+ DB.register(() => count := count.contents + 1)
56
+ DB.run(PreDeploy)
57
+ DB.run(PreDeploy)
58
+ DB.run(PreDeploy)
59
+ expect(count.contents)->toBe(1)
60
+ })
61
+
62
+ testSync("a contribution registered after run does not fire", () => {
63
+ DB.reset()
64
+ let calls = []
65
+ DB.register(() => calls->Array.push("before"))
66
+ DB.run(PreDeploy)
67
+ DB.register(() => calls->Array.push("after"))
68
+ DB.run(PreDeploy)
69
+ expect(calls)->toEqual(["before"])
70
+ })
71
+ })
72
+ })
@@ -0,0 +1,100 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as DeployBootstrap$ReventlessInfra from "../src/components/DeployBootstrap.res.mjs";
4
+
5
+ globalThis.describe("DeployBootstrap", () => {
6
+ globalThis.describe("run with no registrations", () => {
7
+ globalThis.test("is a no-op that does not throw", () => {
8
+ DeployBootstrap$ReventlessInfra.reset();
9
+ DeployBootstrap$ReventlessInfra.run("PreDeploy");
10
+ DeployBootstrap$ReventlessInfra.run("PostDeploy");
11
+ globalThis.expect(true).toBe(true);
12
+ });
13
+ });
14
+ globalThis.describe("registration order within a phase", () => {
15
+ globalThis.test("runs contributions in the order they were registered", () => {
16
+ DeployBootstrap$ReventlessInfra.reset();
17
+ let calls = [];
18
+ DeployBootstrap$ReventlessInfra.register(undefined, () => {
19
+ calls.push("a");
20
+ });
21
+ DeployBootstrap$ReventlessInfra.register(undefined, () => {
22
+ calls.push("b");
23
+ });
24
+ DeployBootstrap$ReventlessInfra.register(undefined, () => {
25
+ calls.push("c");
26
+ });
27
+ DeployBootstrap$ReventlessInfra.run("PreDeploy");
28
+ globalThis.expect(calls).toEqual([
29
+ "a",
30
+ "b",
31
+ "c"
32
+ ]);
33
+ });
34
+ });
35
+ globalThis.describe("phase isolation", () => {
36
+ globalThis.test("run(PreDeploy) fires only PreDeploy contributions", () => {
37
+ DeployBootstrap$ReventlessInfra.reset();
38
+ let calls = [];
39
+ DeployBootstrap$ReventlessInfra.register("PreDeploy", () => {
40
+ calls.push("pre");
41
+ });
42
+ DeployBootstrap$ReventlessInfra.register("PostDeploy", () => {
43
+ calls.push("post");
44
+ });
45
+ DeployBootstrap$ReventlessInfra.run("PreDeploy");
46
+ globalThis.expect(calls).toEqual(["pre"]);
47
+ DeployBootstrap$ReventlessInfra.run("PostDeploy");
48
+ globalThis.expect(calls).toEqual([
49
+ "pre",
50
+ "post"
51
+ ]);
52
+ });
53
+ globalThis.test("defaults to PreDeploy when no phase is given", () => {
54
+ DeployBootstrap$ReventlessInfra.reset();
55
+ let calls = [];
56
+ DeployBootstrap$ReventlessInfra.register(undefined, () => {
57
+ calls.push("default");
58
+ });
59
+ DeployBootstrap$ReventlessInfra.run("PostDeploy");
60
+ globalThis.expect(calls).toEqual([]);
61
+ DeployBootstrap$ReventlessInfra.run("PreDeploy");
62
+ globalThis.expect(calls).toEqual(["default"]);
63
+ });
64
+ });
65
+ globalThis.describe("idempotent run per phase", () => {
66
+ globalThis.test("running an already-run phase does nothing", () => {
67
+ DeployBootstrap$ReventlessInfra.reset();
68
+ let count = {
69
+ contents: 0
70
+ };
71
+ DeployBootstrap$ReventlessInfra.register(undefined, () => {
72
+ count.contents = count.contents + 1 | 0;
73
+ });
74
+ DeployBootstrap$ReventlessInfra.run("PreDeploy");
75
+ DeployBootstrap$ReventlessInfra.run("PreDeploy");
76
+ DeployBootstrap$ReventlessInfra.run("PreDeploy");
77
+ globalThis.expect(count.contents).toBe(1);
78
+ });
79
+ globalThis.test("a contribution registered after run does not fire", () => {
80
+ DeployBootstrap$ReventlessInfra.reset();
81
+ let calls = [];
82
+ DeployBootstrap$ReventlessInfra.register(undefined, () => {
83
+ calls.push("before");
84
+ });
85
+ DeployBootstrap$ReventlessInfra.run("PreDeploy");
86
+ DeployBootstrap$ReventlessInfra.register(undefined, () => {
87
+ calls.push("after");
88
+ });
89
+ DeployBootstrap$ReventlessInfra.run("PreDeploy");
90
+ globalThis.expect(calls).toEqual(["before"]);
91
+ });
92
+ });
93
+ });
94
+
95
+ let DB;
96
+
97
+ export {
98
+ DB,
99
+ }
100
+ /* Not a pure module */