@reventlessdev/reventless-core 3.0.0-alpha.213 → 3.0.0-alpha.214

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.214 (2026-08-07)
7
+
8
+ ### Features
9
+
10
+ * **core,aws:** add the EventLogProvisioning seam for out-of-tree consumers ([82477b3](https://github.com/ReventlessDev/reventless-core/commit/82477b38c1ef90fbe3a6f4a114a9798770329470))
11
+
12
+
6
13
  # 3.0.0-alpha.213 (2026-08-05)
7
14
 
8
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.213",
3
+ "version": "3.0.0-alpha.214",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,15 +29,15 @@
29
29
  "sury": "11.0.0-alpha.4",
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
32
+ "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
32
33
  "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.2",
33
34
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
34
- "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
35
- "@reventlessdev/rescript-node": "2.0.0-alpha.2",
36
35
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
37
36
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.2",
38
- "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
39
37
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
38
+ "@reventlessdev/rescript-node": "2.0.0-alpha.2",
40
39
  "@reventlessdev/reventless-infra": "3.0.0-alpha.127",
40
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
41
41
  "@reventlessdev/reventless-spec": "3.0.0-alpha.101"
42
42
  },
43
43
  "devDependencies": {
@@ -0,0 +1,165 @@
1
+ /**
2
+ Deploy-time hook: every event log announces itself as it is provisioned, so an
3
+ extension can attach behaviour to it inside the program that owns it.
4
+
5
+ No-op by default. Zero behavioral change unless an extension registers a backend
6
+ via `use` before the platform/plugin build. The seam fires once per log at
7
+ provisioning time (inside the deploy program) — never at invocation, and it runs
8
+ no code in production.
9
+
10
+ What a backend does with a log is deliberately NOT a framework concern.
11
+ Aggregation, CDC export, external indexing, replication and archival all want to
12
+ react to an event log's changes; none of them belong in core, and each one added
13
+ to core is maintenance its users pay for whether or not they use it. This only
14
+ exposes the choke point through which the framework says "I just provisioned an
15
+ event log, here is everything I know about it" and leaves the reaction to the
16
+ listener.
17
+
18
+ The alternative — discovering a log's resources from outside the owning program —
19
+ is impractical on AWS: a DynamoDB stream ARN ends in a creation-time stream
20
+ label, so it cannot be synthesised, and exporting it per plugin pushes framework
21
+ internals into every deployment's stack outputs.
22
+
23
+ See `docs/plans/done/event-log-provisioned-seam.md`.
24
+ */
25
+
26
+ /**
27
+ Which event-log family provisioned the log. The two differ in what a reader finds
28
+ on the wire — a classic log is per-aggregate and its rows carry an aggregate id;
29
+ a DCB log is per-plugin and its rows carry tags — so a backend that decodes
30
+ records needs to know which it is looking at before it looks.
31
+ */
32
+ type logStyle =
33
+ | /** per-aggregate log behind `EventLog_Builder` */ Classic
34
+ | /** per-plugin tagged log behind `DcbEventLog_Builder` */ Dcb
35
+
36
+ /**
37
+ An event-log backend, registered by an extension (a deploy program) before the
38
+ platform builds. `onProvisioned` is called once per event log.
39
+
40
+ `~name` is the log's STATIC logical name — the suffixed component name the
41
+ builder passed to its storage maker (`ProductEventLog`, `CatalogDcbEventLog`). A
42
+ Pulumi-based backend derives logical resource names from it, which must be stable
43
+ and known at graph-construction time.
44
+
45
+ `~owner` is the model element the log belongs to: `{kind: Aggregate, name:
46
+ "Product"}` for a classic log, `{kind: Plugin, name: "Catalog"}` for a DCB log
47
+ (the plugin owns it, not any one slice). `~plugin` / `~platform` come from the
48
+ ambient `ResourceAttribution` context the plugin builder publishes during
49
+ construction — the same source `AWS_Tags` and `Monitoring.notify` use. Both are
50
+ `None` for a log provisioned outside any plugin construct.
51
+
52
+ `~resources` is the adapter's deploy-time resources, not a stream: a stream is a
53
+ DynamoDB-shaped idea and SQL-backed adapters have none. A backend derives
54
+ whatever its store supports and does nothing for the rest — a Postgres log
55
+ arrives with `resources: []`, and an in-memory one with nothing attachable. The
56
+ contract is uniform on purpose; filtering belongs in the backend.
57
+
58
+ `~opts` carries the log component's parent options. A backend that provisions
59
+ anything needs them, or its resources land outside the component tree.
60
+
61
+ Because `onProvisioned` fires during resource creation inside the deploy program,
62
+ a Pulumi-based backend can create its resources in the same stack, parented
63
+ naturally.
64
+
65
+ **The reader budget.** A DynamoDB table has exactly one stream, and DynamoDB
66
+ Streams throttles beyond two processes reading the same shard. Core already
67
+ spends one slot: `EventCollectorChannel_Helpers` maps every event-log stream onto
68
+ the plugin's event-collector Lambda. A backend that subscribes through this seam
69
+ is therefore reader #2 — the ceiling. A second stream consumer would throttle the
70
+ first, which is why registration is single-slot and loud (see `use`). If several
71
+ consumers are ever needed the answer is a fan-out — routing through the existing
72
+ collector, or a stream type that supports more readers — not more subscriptions.
73
+
74
+ **Snapshots share the log's table.** Classic aggregate snapshots are written to
75
+ the event-log table under `position = "SNAPSHOT"`, so a stream consumer sees them
76
+ alongside events. The seam does not filter; a backend that only wants events must
77
+ skip those rows itself.
78
+
79
+ **No ordering guarantee against other seams.** This and `Monitoring.notify` both
80
+ fire during provisioning, from different builders, in whatever order the graph is
81
+ constructed. A backend must not depend on having seen one before the other.
82
+ */
83
+ module type Backend = {
84
+ let onProvisioned: (
85
+ ~logStyle: logStyle,
86
+ ~name: string,
87
+ ~owner: option<ResourceAttribution.owner>,
88
+ ~plugin: option<string>,
89
+ ~platform: option<string>,
90
+ ~resources: array<ReventlessInfra.Adapter.resource>,
91
+ ~opts: Pulumi.CustomResourceOptions.t,
92
+ ) => unit
93
+ }
94
+
95
+ /** Default backend: does nothing. Active unless an extension calls `use`. */
96
+ module Noop: Backend = {
97
+ let onProvisioned = (
98
+ ~logStyle as _,
99
+ ~name as _,
100
+ ~owner as _,
101
+ ~plugin as _,
102
+ ~platform as _,
103
+ ~resources as _,
104
+ ~opts as _,
105
+ ) => ()
106
+ }
107
+
108
+ let backend: ref<module(Backend)> = ref(module(Noop: Backend))
109
+ let registered = ref(false)
110
+
111
+ /**
112
+ Register the event-log backend. Must run before the platform/plugin build in the
113
+ deploy program (plain statement order — the registry is consulted lazily at each
114
+ provisioning site). Called once, by the extension, first.
115
+
116
+ Single-slot and loud: a second `use` raises rather than silently winning, because
117
+ the reader budget documented on `Backend` means two registered backends is not a
118
+ last-writer-wins configuration question but two consumers competing for one
119
+ stream slot — a defect that otherwise surfaces in production as iterator-age
120
+ alarms on whichever one lost. `reset` clears the slot for tests.
121
+ */
122
+ let use = (b: module(Backend)) => {
123
+ if registered.contents {
124
+ JsError.throwWithMessage(
125
+ "EventLogProvisioning.use called twice: only one event-log backend can be registered. " ++
126
+ "A DynamoDB stream supports two readers per shard and core's event collector already holds one, " ++
127
+ "so a second backend would throttle the first. Compose the two backends into one, or fan out " ++
128
+ "behind a single registration.",
129
+ )
130
+ }
131
+ registered := true
132
+ backend := b
133
+ }
134
+
135
+ /**
136
+ Restore the default `Noop` backend and clear the registration slot. Test-support
137
+ only — deploy programs never call this.
138
+ */
139
+ let reset = () => {
140
+ backend := module(Noop: Backend)
141
+ registered := false
142
+ }
143
+
144
+ /**
145
+ Announce a provisioned event log to the registered backend. Called by the
146
+ `EventLog` and `DcbEventLog` builders at their storage-provisioning sites (once
147
+ per log). Firing from the builders rather than from each storage adapter is what
148
+ makes the contract uniform: every adapter — DynamoDB, Postgres, SQLite,
149
+ in-memory, and any future one — is covered by construction, with nothing to
150
+ remember when a new one is written. No-op until an extension registers a backend.
151
+ */
152
+ let notify = (
153
+ ~logStyle: logStyle,
154
+ ~name: string,
155
+ ~owner: option<ResourceAttribution.owner>,
156
+ ~resources: array<ReventlessInfra.Adapter.resource>,
157
+ ~opts: Pulumi.CustomResourceOptions.t,
158
+ ) => {
159
+ module B = unpack(backend.contents)
160
+ // The owning plugin/platform come from the ambient context the plugin builder publishes
161
+ // (ResourceAttribution.enter) — populated exactly when this fires, so the call sites need not
162
+ // thread the names. `None` outside a plugin construct.
163
+ let {ResourceAttribution.plugin: plugin, platform} = ResourceAttribution.current.contents
164
+ B.onProvisioned(~logStyle, ~name, ~owner, ~plugin, ~platform, ~resources, ~opts)
165
+ }
@@ -0,0 +1,49 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
4
+ import * as ResourceAttribution$ReventlessCore from "../../ResourceAttribution.res.mjs";
5
+
6
+ function onProvisioned(param, param$1, param$2, param$3, param$4, param$5, param$6) {
7
+
8
+ }
9
+
10
+ let Noop = {
11
+ onProvisioned: onProvisioned
12
+ };
13
+
14
+ let backend = {
15
+ contents: Noop
16
+ };
17
+
18
+ let registered = {
19
+ contents: false
20
+ };
21
+
22
+ function use(b) {
23
+ if (registered.contents) {
24
+ Stdlib_JsError.throwWithMessage("EventLogProvisioning.use called twice: only one event-log backend can be registered. A DynamoDB stream supports two readers per shard and core's event collector already holds one, so a second backend would throttle the first. Compose the two backends into one, or fan out behind a single registration.");
25
+ }
26
+ registered.contents = true;
27
+ backend.contents = b;
28
+ }
29
+
30
+ function reset() {
31
+ backend.contents = Noop;
32
+ registered.contents = false;
33
+ }
34
+
35
+ function notify(logStyle, name, owner, resources, opts) {
36
+ let B = backend.contents;
37
+ let match = ResourceAttribution$ReventlessCore.current.contents;
38
+ B.onProvisioned(logStyle, name, owner, match.plugin, match.platform, resources, opts);
39
+ }
40
+
41
+ export {
42
+ Noop,
43
+ backend,
44
+ registered,
45
+ use,
46
+ reset,
47
+ notify,
48
+ }
49
+ /* No side effect */
@@ -16,21 +16,34 @@ module Make = (
16
16
  let construct = (indexes, partitionTag, crossPartitionTagKeys, self, name) => {
17
17
  let opts = {Pulumi.CustomResourceOptions.parent: self->Component.toPulumiResource}
18
18
 
19
+ let logName = name->ComponentType.name(DcbEventLog.componentType)
20
+ // The DCB event log is shared across the plugin's slices, so the plugin owns
21
+ // it — not any one component.
22
+ let owner: ResourceAttribution.owner = {kind: ComponentType.Plugin, name}
19
23
  let storage = Storage.make(
20
- ~name=name->ComponentType.name(DcbEventLog.componentType),
24
+ ~name=logName,
21
25
  ~indexes,
22
26
  ~partitionTag,
23
27
  ~crossPartitionTagKeys,
24
28
  ~opts,
25
29
  )
26
30
 
31
+ // Announce the log to any registered extension. No-op unless a deploy program
32
+ // registered a backend via EventLogProvisioning.use — see
33
+ // src/adapter/EventLogProvisioning/EventLogProvisioning.res.
34
+ EventLogProvisioning.notify(
35
+ ~logStyle=Dcb,
36
+ ~name=logName,
37
+ ~owner=Some(owner),
38
+ ~resources=storage.resources,
39
+ ~opts,
40
+ )
41
+
27
42
  module SpecificEventTopic = EventTopic_Builder.Make(EventTopicSpec, EventTopicPublisher)
28
43
  let eventTopic = SpecificEventTopic.make(
29
44
  ~name,
30
45
  ~storageResources=storage.resources,
31
- // The DCB event log is shared across the plugin's slices, so the plugin
32
- // owns it — not any one component.
33
- ~owner={kind: ComponentType.Plugin, name},
46
+ ~owner,
34
47
  ~opts=opts->Util.Pulumi.ComponentResourceOptions.ofCustomResourceOptions,
35
48
  )
36
49
 
@@ -8,6 +8,7 @@ import * as DcbEventLog$ReventlessCore from "./DcbEventLog.res.mjs";
8
8
  import * as Util_Pulumi$ReventlessCore from "../../util/Util_Pulumi.res.mjs";
9
9
  import * as ComponentType$ReventlessCore from "../../ComponentType.res.mjs";
10
10
  import * as EventTopic_Builder$ReventlessCore from "../EventTopic/EventTopic_Builder.res.mjs";
11
+ import * as EventLogProvisioning$ReventlessCore from "../../adapter/EventLogProvisioning/EventLogProvisioning.res.mjs";
11
12
  import * as DcbEventLog_Operations$ReventlessCore from "./DcbEventLog_Operations.res.mjs";
12
13
 
13
14
  function Make(Storage) {
@@ -20,16 +21,19 @@ function Make(Storage) {
20
21
  let opts = {
21
22
  parent: opts_parent
22
23
  };
23
- let storage = Storage.make(ComponentType$ReventlessCore.name(extra$1, DcbEventLog$ReventlessCore.componentType), indexes, partitionTag, crossPartitionTagKeys, opts);
24
+ let logName = ComponentType$ReventlessCore.name(extra$1, DcbEventLog$ReventlessCore.componentType);
25
+ let owner = {
26
+ kind: "Plugin",
27
+ name: extra$1
28
+ };
29
+ let storage = Storage.make(logName, indexes, partitionTag, crossPartitionTagKeys, opts);
30
+ EventLogProvisioning$ReventlessCore.notify("Dcb", logName, owner, storage.resources, opts);
24
31
  let SpecificEventTopic = EventTopic_Builder$ReventlessCore.Make({
25
32
  Id: Id$Reventless.$$String,
26
33
  name: "DcbEventLog",
27
34
  eventSchema: S.json
28
35
  })(EventTopicPublisher);
29
- let eventTopic = SpecificEventTopic.make(extra$1, storage.resources, {
30
- kind: "Plugin",
31
- name: extra$1
32
- }, Util_Pulumi$ReventlessCore.ComponentResourceOptions.ofCustomResourceOptions(opts));
36
+ let eventTopic = SpecificEventTopic.make(extra$1, storage.resources, owner, Util_Pulumi$ReventlessCore.ComponentResourceOptions.ofCustomResourceOptions(opts));
33
37
  Component$ReventlessCore.setOperations(extra, Pulumi.all([
34
38
  storage.operations,
35
39
  Component$ReventlessCore.operations(eventTopic)
@@ -18,9 +18,17 @@ module Make = (
18
18
  let construct = (~owner=?, self, name) => {
19
19
  let opts = {Pulumi.CustomResourceOptions.parent: self->Component.toPulumiResource}
20
20
 
21
- let storage = Storage.make(
22
- ~name=name->ComponentType.name(EventLog.componentType),
21
+ let logName = name->ComponentType.name(EventLog.componentType)
22
+ let storage = Storage.make(~name=logName, ~owner, ~opts)
23
+
24
+ // Announce the log to any registered extension. No-op unless a deploy program
25
+ // registered a backend via EventLogProvisioning.use — see
26
+ // src/adapter/EventLogProvisioning/EventLogProvisioning.res.
27
+ EventLogProvisioning.notify(
28
+ ~logStyle=Classic,
29
+ ~name=logName,
23
30
  ~owner,
31
+ ~resources=storage.resources,
24
32
  ~opts,
25
33
  )
26
34
 
@@ -7,6 +7,7 @@ import * as Util_Pulumi$ReventlessCore from "../../util/Util_Pulumi.res.mjs";
7
7
  import * as ComponentType$ReventlessCore from "../../ComponentType.res.mjs";
8
8
  import * as EventTopic_Builder$ReventlessCore from "../EventTopic/EventTopic_Builder.res.mjs";
9
9
  import * as EventLog_Operations$ReventlessCore from "./EventLog_Operations.res.mjs";
10
+ import * as EventLogProvisioning$ReventlessCore from "../../adapter/EventLogProvisioning/EventLogProvisioning.res.mjs";
10
11
 
11
12
  function Make(Spec) {
12
13
  return Storage => (EventTopicPublisher => {
@@ -15,7 +16,9 @@ function Make(Spec) {
15
16
  let opts = {
16
17
  parent: opts_parent
17
18
  };
18
- let storage = Storage.make(ComponentType$ReventlessCore.name(extra$1, EventLog$ReventlessCore.componentType), owner, opts);
19
+ let logName = ComponentType$ReventlessCore.name(extra$1, EventLog$ReventlessCore.componentType);
20
+ let storage = Storage.make(logName, owner, opts);
21
+ EventLogProvisioning$ReventlessCore.notify("Classic", logName, owner, storage.resources, opts);
19
22
  let SpecificEventTopic = EventTopic_Builder$ReventlessCore.Make(Spec)(EventTopicPublisher);
20
23
  let eventTopic = SpecificEventTopic.make(extra$1, storage.resources, owner, Util_Pulumi$ReventlessCore.ComponentResourceOptions.ofCustomResourceOptions(opts));
21
24
  Component$ReventlessCore.setOperations(extra, Pulumi.all([
@@ -0,0 +1,207 @@
1
+ open JestGlobals
2
+
3
+ // The registry only forwards `~resources` and `~opts`; it never reads their
4
+ // fields. A structural stand-in keeps @pulumi/pulumi (whose Output.make would
5
+ // build a real resource) out of the Jest run — its ESM entry can't be imported
6
+ // under Jest. Same reason as MonitoringTest's stub.
7
+ let stubResource: ReventlessInfra.Adapter.resource = %raw(`{}`)
8
+ let noOpts: Pulumi.CustomResourceOptions.t = {}
9
+
10
+ let classicOwner: ResourceAttribution.owner = {kind: ComponentType.Aggregate, name: "Product"}
11
+ let dcbOwner: ResourceAttribution.owner = {kind: ComponentType.Plugin, name: "Catalog"}
12
+
13
+ describe("EventLogProvisioning", () => {
14
+ testSync("Noop default: notify is silent and does not throw", () => {
15
+ // No backend registered — the default Noop backend swallows the call.
16
+ EventLogProvisioning.notify(
17
+ ~logStyle=Classic,
18
+ ~name="ProductEventLog",
19
+ ~owner=Some(classicOwner),
20
+ ~resources=[],
21
+ ~opts=noOpts,
22
+ )
23
+ expect(true)->toBe(true)
24
+ })
25
+
26
+ testSync("use registers a backend that receives every notify, style and name", () => {
27
+ let recorded: array<(EventLogProvisioning.logStyle, string)> = []
28
+ module Recorder: EventLogProvisioning.Backend = {
29
+ let onProvisioned = (
30
+ ~logStyle,
31
+ ~name,
32
+ ~owner as _,
33
+ ~plugin as _,
34
+ ~platform as _,
35
+ ~resources as _,
36
+ ~opts as _,
37
+ ) => recorded->Array.push((logStyle, name))
38
+ }
39
+ EventLogProvisioning.use(module(Recorder: EventLogProvisioning.Backend))
40
+
41
+ EventLogProvisioning.notify(
42
+ ~logStyle=Classic,
43
+ ~name="ProductEventLog",
44
+ ~owner=Some(classicOwner),
45
+ ~resources=[],
46
+ ~opts=noOpts,
47
+ )
48
+ EventLogProvisioning.notify(
49
+ ~logStyle=Dcb,
50
+ ~name="CatalogDcbEventLog",
51
+ ~owner=Some(dcbOwner),
52
+ ~resources=[],
53
+ ~opts=noOpts,
54
+ )
55
+
56
+ expect(recorded)->toEqual([
57
+ (EventLogProvisioning.Classic, "ProductEventLog"),
58
+ (EventLogProvisioning.Dcb, "CatalogDcbEventLog"),
59
+ ])
60
+
61
+ // Restore the default so registry state doesn't leak to other cases.
62
+ EventLogProvisioning.reset()
63
+ })
64
+
65
+ testSync("notify forwards the owner and the adapter's resources unchanged", () => {
66
+ let seen: ref<
67
+ option<(option<ResourceAttribution.owner>, array<ReventlessInfra.Adapter.resource>)>,
68
+ > = ref(None)
69
+ module Capture: EventLogProvisioning.Backend = {
70
+ let onProvisioned = (
71
+ ~logStyle as _,
72
+ ~name as _,
73
+ ~owner,
74
+ ~plugin as _,
75
+ ~platform as _,
76
+ ~resources,
77
+ ~opts as _,
78
+ ) => seen := Some((owner, resources))
79
+ }
80
+ EventLogProvisioning.use(module(Capture: EventLogProvisioning.Backend))
81
+
82
+ EventLogProvisioning.notify(
83
+ ~logStyle=Classic,
84
+ ~name="ProductEventLog",
85
+ ~owner=Some(classicOwner),
86
+ ~resources=[stubResource],
87
+ ~opts=noOpts,
88
+ )
89
+ switch seen.contents {
90
+ | Some((owner, resources)) =>
91
+ expect(owner)->toEqual(Some(classicOwner))
92
+ expect(resources)->toHaveLength(1)
93
+ | None => expect("no notify received")->toBe("a notify")
94
+ }
95
+
96
+ EventLogProvisioning.reset()
97
+ })
98
+
99
+ testSync("a Postgres-style log with no resources still fires — the contract is uniform", () => {
100
+ // Postgres and in-memory adapters return `resources: []`. Backends that only
101
+ // implement a DynamoDB arm ignore those; the seam does not filter for them.
102
+ let styles: array<EventLogProvisioning.logStyle> = []
103
+ module Recorder: EventLogProvisioning.Backend = {
104
+ let onProvisioned = (
105
+ ~logStyle,
106
+ ~name as _,
107
+ ~owner as _,
108
+ ~plugin as _,
109
+ ~platform as _,
110
+ ~resources,
111
+ ~opts as _,
112
+ ) =>
113
+ if resources->Array.length == 0 {
114
+ styles->Array.push(logStyle)
115
+ }
116
+ }
117
+ EventLogProvisioning.use(module(Recorder: EventLogProvisioning.Backend))
118
+
119
+ EventLogProvisioning.notify(
120
+ ~logStyle=Dcb,
121
+ ~name="CatalogDcbEventLog",
122
+ ~owner=Some(dcbOwner),
123
+ ~resources=[],
124
+ ~opts=noOpts,
125
+ )
126
+
127
+ expect(styles)->toEqual([EventLogProvisioning.Dcb])
128
+ EventLogProvisioning.reset()
129
+ })
130
+
131
+ testSync("notify delivers the ambient plugin/platform inside a construct scope, None outside", () => {
132
+ let seen: array<(option<string>, option<string>)> = []
133
+ module OwnerCapture: EventLogProvisioning.Backend = {
134
+ let onProvisioned = (
135
+ ~logStyle as _,
136
+ ~name as _,
137
+ ~owner as _,
138
+ ~plugin,
139
+ ~platform,
140
+ ~resources as _,
141
+ ~opts as _,
142
+ ) => seen->Array.push((plugin, platform))
143
+ }
144
+ EventLogProvisioning.use(module(OwnerCapture: EventLogProvisioning.Backend))
145
+
146
+ // Outside any plugin construct: both None.
147
+ EventLogProvisioning.notify(
148
+ ~logStyle=Classic,
149
+ ~name="ProductEventLog",
150
+ ~owner=Some(classicOwner),
151
+ ~resources=[],
152
+ ~opts=noOpts,
153
+ )
154
+
155
+ // Inside a construct scope (what Plugin_Builder.construct establishes): both delivered.
156
+ let prev = ResourceAttribution.enter(~platform="online-shop", ~plugin="Catalog")
157
+ EventLogProvisioning.notify(
158
+ ~logStyle=Dcb,
159
+ ~name="CatalogDcbEventLog",
160
+ ~owner=Some(dcbOwner),
161
+ ~resources=[],
162
+ ~opts=noOpts,
163
+ )
164
+ ResourceAttribution.restore(prev)
165
+
166
+ // After restore: back to None.
167
+ EventLogProvisioning.notify(
168
+ ~logStyle=Classic,
169
+ ~name="OrderEventLog",
170
+ ~owner=None,
171
+ ~resources=[],
172
+ ~opts=noOpts,
173
+ )
174
+
175
+ expect(seen)->toEqual([(None, None), (Some("Catalog"), Some("online-shop")), (None, None)])
176
+
177
+ EventLogProvisioning.reset()
178
+ })
179
+
180
+ testSync("use is single-slot: a second registration throws rather than silently winning", () => {
181
+ module First: EventLogProvisioning.Backend = {
182
+ let onProvisioned = (
183
+ ~logStyle as _,
184
+ ~name as _,
185
+ ~owner as _,
186
+ ~plugin as _,
187
+ ~platform as _,
188
+ ~resources as _,
189
+ ~opts as _,
190
+ ) => ()
191
+ }
192
+ EventLogProvisioning.use(module(First: EventLogProvisioning.Backend))
193
+
194
+ let threw = try {
195
+ EventLogProvisioning.use(module(First: EventLogProvisioning.Backend))
196
+ false
197
+ } catch {
198
+ | _ => true
199
+ }
200
+ expect(threw)->toBe(true)
201
+
202
+ // reset clears the slot, so a later registration succeeds again.
203
+ EventLogProvisioning.reset()
204
+ EventLogProvisioning.use(module(First: EventLogProvisioning.Backend))
205
+ EventLogProvisioning.reset()
206
+ })
207
+ })
@@ -0,0 +1,150 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as ResourceAttribution$ReventlessCore from "../../src/ResourceAttribution.res.mjs";
4
+ import * as EventLogProvisioning$ReventlessCore from "../../src/adapter/EventLogProvisioning/EventLogProvisioning.res.mjs";
5
+
6
+ let stubResource = {};
7
+
8
+ let noOpts = {};
9
+
10
+ let classicOwner = {
11
+ kind: "Aggregate",
12
+ name: "Product"
13
+ };
14
+
15
+ let dcbOwner = {
16
+ kind: "Plugin",
17
+ name: "Catalog"
18
+ };
19
+
20
+ globalThis.describe("EventLogProvisioning", () => {
21
+ globalThis.test("Noop default: notify is silent and does not throw", () => {
22
+ EventLogProvisioning$ReventlessCore.notify("Classic", "ProductEventLog", classicOwner, [], noOpts);
23
+ globalThis.expect(true).toBe(true);
24
+ });
25
+ globalThis.test("use registers a backend that receives every notify, style and name", () => {
26
+ let recorded = [];
27
+ let onProvisioned = (logStyle, name, param, param$1, param$2, param$3, param$4) => {
28
+ recorded.push([
29
+ logStyle,
30
+ name
31
+ ]);
32
+ };
33
+ let Recorder = {
34
+ onProvisioned: onProvisioned
35
+ };
36
+ EventLogProvisioning$ReventlessCore.use(Recorder);
37
+ EventLogProvisioning$ReventlessCore.notify("Classic", "ProductEventLog", classicOwner, [], noOpts);
38
+ EventLogProvisioning$ReventlessCore.notify("Dcb", "CatalogDcbEventLog", dcbOwner, [], noOpts);
39
+ globalThis.expect(recorded).toEqual([
40
+ [
41
+ "Classic",
42
+ "ProductEventLog"
43
+ ],
44
+ [
45
+ "Dcb",
46
+ "CatalogDcbEventLog"
47
+ ]
48
+ ]);
49
+ EventLogProvisioning$ReventlessCore.reset();
50
+ });
51
+ globalThis.test("notify forwards the owner and the adapter's resources unchanged", () => {
52
+ let seen = {
53
+ contents: undefined
54
+ };
55
+ let onProvisioned = (param, param$1, owner, param$2, param$3, resources, param$4) => {
56
+ seen.contents = [
57
+ owner,
58
+ resources
59
+ ];
60
+ };
61
+ let Capture = {
62
+ onProvisioned: onProvisioned
63
+ };
64
+ EventLogProvisioning$ReventlessCore.use(Capture);
65
+ EventLogProvisioning$ReventlessCore.notify("Classic", "ProductEventLog", classicOwner, [stubResource], noOpts);
66
+ let match = seen.contents;
67
+ if (match !== undefined) {
68
+ globalThis.expect(match[0]).toEqual(classicOwner);
69
+ globalThis.expect(match[1]).toHaveLength(1);
70
+ } else {
71
+ globalThis.expect("no notify received").toBe("a notify");
72
+ }
73
+ EventLogProvisioning$ReventlessCore.reset();
74
+ });
75
+ globalThis.test("a Postgres-style log with no resources still fires — the contract is uniform", () => {
76
+ let styles = [];
77
+ let onProvisioned = (logStyle, param, param$1, param$2, param$3, resources, param$4) => {
78
+ if (resources.length === 0) {
79
+ styles.push(logStyle);
80
+ return;
81
+ }
82
+ };
83
+ let Recorder = {
84
+ onProvisioned: onProvisioned
85
+ };
86
+ EventLogProvisioning$ReventlessCore.use(Recorder);
87
+ EventLogProvisioning$ReventlessCore.notify("Dcb", "CatalogDcbEventLog", dcbOwner, [], noOpts);
88
+ globalThis.expect(styles).toEqual(["Dcb"]);
89
+ EventLogProvisioning$ReventlessCore.reset();
90
+ });
91
+ globalThis.test("notify delivers the ambient plugin/platform inside a construct scope, None outside", () => {
92
+ let seen = [];
93
+ let onProvisioned = (param, param$1, param$2, plugin, platform, param$3, param$4) => {
94
+ seen.push([
95
+ plugin,
96
+ platform
97
+ ]);
98
+ };
99
+ let OwnerCapture = {
100
+ onProvisioned: onProvisioned
101
+ };
102
+ EventLogProvisioning$ReventlessCore.use(OwnerCapture);
103
+ EventLogProvisioning$ReventlessCore.notify("Classic", "ProductEventLog", classicOwner, [], noOpts);
104
+ let prev = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
105
+ EventLogProvisioning$ReventlessCore.notify("Dcb", "CatalogDcbEventLog", dcbOwner, [], noOpts);
106
+ ResourceAttribution$ReventlessCore.restore(prev);
107
+ EventLogProvisioning$ReventlessCore.notify("Classic", "OrderEventLog", undefined, [], noOpts);
108
+ globalThis.expect(seen).toEqual([
109
+ [
110
+ undefined,
111
+ undefined
112
+ ],
113
+ [
114
+ "Catalog",
115
+ "online-shop"
116
+ ],
117
+ [
118
+ undefined,
119
+ undefined
120
+ ]
121
+ ]);
122
+ EventLogProvisioning$ReventlessCore.reset();
123
+ });
124
+ globalThis.test("use is single-slot: a second registration throws rather than silently winning", () => {
125
+ let onProvisioned = (param, param$1, param$2, param$3, param$4, param$5, param$6) => {};
126
+ let First = {
127
+ onProvisioned: onProvisioned
128
+ };
129
+ EventLogProvisioning$ReventlessCore.use(First);
130
+ let threw;
131
+ try {
132
+ EventLogProvisioning$ReventlessCore.use(First);
133
+ threw = false;
134
+ } catch (exn) {
135
+ threw = true;
136
+ }
137
+ globalThis.expect(threw).toBe(true);
138
+ EventLogProvisioning$ReventlessCore.reset();
139
+ EventLogProvisioning$ReventlessCore.use(First);
140
+ EventLogProvisioning$ReventlessCore.reset();
141
+ });
142
+ });
143
+
144
+ export {
145
+ stubResource,
146
+ noOpts,
147
+ classicOwner,
148
+ dcbOwner,
149
+ }
150
+ /* Not a pure module */