@reventlessdev/reventless-core 3.0.0-alpha.216 → 3.0.0-alpha.217
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 +10 -0
- package/package.json +7 -7
- package/src/ResourceAttribution.res +60 -4
- package/src/ResourceAttribution.res.mjs +26 -0
- package/src/adapter/QueryInterception/QueryInterception.res +74 -0
- package/src/adapter/QueryInterception/QueryInterception.res.mjs +26 -0
- package/src/adapter/ResourceAttribution_Deploytime.res +43 -0
- package/src/adapter/ResourceAttribution_Deploytime.res.mjs +20 -0
- package/src/components/Builder_Helpers.res +21 -3
- package/src/components/Builder_Helpers.res.mjs +4 -3
- package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +4 -1
- package/src/components/ExtensionPoint/ExtensionPoint_Builder.res.mjs +2 -1
- package/src/plugin/component/Plugin_Builder.res +5 -1
- package/src/plugin/component/Plugin_Builder.res.mjs +3 -2
- package/src/plugin/component/Plugin_Helpers.res +7 -2
- package/src/plugin/component/Plugin_Helpers.res.mjs +4 -3
- package/tests/ResourceAttributionTest.res +115 -0
- package/tests/ResourceAttributionTest.res.mjs +106 -0
- package/tests/adapter/QueryInterceptionTest.res +55 -0
- package/tests/adapter/QueryInterceptionTest.res.mjs +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.217 (2026-08-08)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core:** attribute resources built after a builder's construct returns ([67d6d9c](https://github.com/ReventlessDev/reventless-core/commit/67d6d9c0ff3ff1d38cd9e8ad43a4b86121abf827))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **core:** let an extension switch query interception on, and provision it ([4523b10](https://github.com/ReventlessDev/reventless-core/commit/4523b10597f1a028eedccc42391b250b25a6ddd3))
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
# 3.0.0-alpha.216 (2026-08-08)
|
|
7
17
|
|
|
8
18
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.217",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -28,17 +28,17 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
31
32
|
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.2",
|
|
32
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
33
33
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
34
|
-
"@reventlessdev/rescript-
|
|
34
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
35
35
|
"@reventlessdev/rescript-node": "2.0.0-alpha.2",
|
|
36
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
37
|
-
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
38
36
|
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.2",
|
|
37
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
38
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
39
39
|
"@reventlessdev/reventless-infra": "3.0.0-alpha.128",
|
|
40
|
-
"@reventlessdev/reventless-
|
|
41
|
-
"@reventlessdev/reventless-
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.102",
|
|
41
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.30"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -112,10 +112,28 @@ large, purely mechanical churn — so the plugin builder publishes them here for
|
|
|
112
112
|
the duration of its construct, exactly as it already does for
|
|
113
113
|
`Logger.currentPluginName`.
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
The `enter`/`restore` bracket covers what runs **synchronously** inside the
|
|
116
|
+
builder's construct, and everything built there genuinely belongs to that plugin.
|
|
117
|
+
|
|
118
|
+
It does not cover all of construct. Builders defer part of their work until every
|
|
119
|
+
handler has registered, and they defer it by waiting on outputs — so that work
|
|
120
|
+
runs from a `Pulumi.Output.apply` callback, after construct has returned and
|
|
121
|
+
`restore` has already emptied the context. A resource created there would be
|
|
122
|
+
attributed to nobody, and `None` is a *meaningful* value here: it means
|
|
123
|
+
platform-scope substrate. So the miss does not read as a gap, it reads as a
|
|
124
|
+
positive answer, and a consumer cannot tell the two apart.
|
|
125
|
+
|
|
126
|
+
Deferred work therefore carries its own attribution. Capture the context while it
|
|
127
|
+
is still published and reinstate it around the callback: `deferred` for a
|
|
128
|
+
callback registered during construct and invoked later, and
|
|
129
|
+
`ResourceAttribution_Deploytime.applyAttributed` / `flatMapAttributed` for a
|
|
130
|
+
builder's own apply. Both restore afterwards, so several plugins' deferred work
|
|
131
|
+
can run from one apply without leaking into each other.
|
|
132
|
+
|
|
133
|
+
Everything Pulumi-shaped lives in that separate module, so this one stays
|
|
134
|
+
importable from code that ends up in a Lambda bundle.
|
|
135
|
+
|
|
136
|
+
Outside any plugin construct both are `None` — the correct answer for
|
|
119
137
|
platform-scope substrate.
|
|
120
138
|
*/
|
|
121
139
|
type context = {platform: option<string>, plugin: option<string>}
|
|
@@ -131,3 +149,41 @@ let enter = (~platform: string, ~plugin: string) => {
|
|
|
131
149
|
|
|
132
150
|
/** Restore the context captured by `enter`. */
|
|
133
151
|
let restore = (previous: context) => current := previous
|
|
152
|
+
|
|
153
|
+
/** Re-enter a whole captured context — including an empty one, which `enter`
|
|
154
|
+
cannot express since it takes two required strings. Returns the previous
|
|
155
|
+
context, exactly as `enter` does. */
|
|
156
|
+
let enterCaptured = (captured: context) => {
|
|
157
|
+
let previous = current.contents
|
|
158
|
+
current := captured
|
|
159
|
+
previous
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Run `fn` under `captured`, then put the previous context back — including
|
|
163
|
+
when `fn` throws, so a failing deferred callback cannot strand the context
|
|
164
|
+
and mis-attribute everything built after it. */
|
|
165
|
+
let within = (captured: context, fn: unit => 'a): 'a => {
|
|
166
|
+
let previous = enterCaptured(captured)
|
|
167
|
+
try {
|
|
168
|
+
let result = fn()
|
|
169
|
+
restore(previous)
|
|
170
|
+
result
|
|
171
|
+
} catch {
|
|
172
|
+
| e =>
|
|
173
|
+
restore(previous)
|
|
174
|
+
throw(e)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Capture the context now and wrap a callback so it runs under that context
|
|
179
|
+
whenever it is finally called. For work registered during construct but
|
|
180
|
+
executed later from an apply — a builder's deferred `finish`, whose whole
|
|
181
|
+
purpose is to run after every handler has registered.
|
|
182
|
+
|
|
183
|
+
Wrap at **registration**, not at the call site: the registries these
|
|
184
|
+
callbacks land in are module-level and shared by every plugin, so by the time
|
|
185
|
+
they run there is no single context that would be right for all of them. */
|
|
186
|
+
let deferred = (fn: unit => 'a): (unit => 'a) => {
|
|
187
|
+
let captured = current.contents
|
|
188
|
+
() => within(captured, fn)
|
|
189
|
+
}
|
|
@@ -86,11 +86,37 @@ function restore(previous) {
|
|
|
86
86
|
current.contents = previous;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function enterCaptured(captured) {
|
|
90
|
+
let previous = current.contents;
|
|
91
|
+
current.contents = captured;
|
|
92
|
+
return previous;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function within(captured, fn) {
|
|
96
|
+
let previous = enterCaptured(captured);
|
|
97
|
+
try {
|
|
98
|
+
let result = fn();
|
|
99
|
+
current.contents = previous;
|
|
100
|
+
return result;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
current.contents = previous;
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function deferred(fn) {
|
|
108
|
+
let captured = current.contents;
|
|
109
|
+
return () => within(captured, fn);
|
|
110
|
+
}
|
|
111
|
+
|
|
89
112
|
export {
|
|
90
113
|
Scope,
|
|
91
114
|
Role,
|
|
92
115
|
current,
|
|
93
116
|
enter,
|
|
94
117
|
restore,
|
|
118
|
+
enterCaptured,
|
|
119
|
+
within,
|
|
120
|
+
deferred,
|
|
95
121
|
}
|
|
96
122
|
/* No side effect */
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Deploy-time switch: an extension asks for query interception, and the framework
|
|
3
|
+
provisions whatever that costs.
|
|
4
|
+
|
|
5
|
+
`QueryDb_Callback.registerQueryInterceptor` is a runtime hook, and `RuntimeExtension`
|
|
6
|
+
made it reachable inside a deployed runtime. On a DynamoDB-backed read model there
|
|
7
|
+
was still nothing to reach: a top-level Query resolver talks to DynamoDB directly,
|
|
8
|
+
so no code of ours runs on a read at all. Something has to put a runtime in front
|
|
9
|
+
of the read, and that is a provisioning decision the extension cannot make for
|
|
10
|
+
itself — an AppSync Lambda data source needs the api handle and a service role,
|
|
11
|
+
both of which exist only inside the plugin build.
|
|
12
|
+
|
|
13
|
+
So the split follows `Monitoring` and `EventLogProvisioning`: **the extension
|
|
14
|
+
decides, the framework provisions.** An extension says only *that* interception
|
|
15
|
+
should happen; it names no data source, no api and no role, and writes nothing
|
|
16
|
+
provider-shaped. The framework already ships the handler
|
|
17
|
+
(`QueryInterceptor_Lambda` on AWS), so there is nothing left for a caller to
|
|
18
|
+
supply.
|
|
19
|
+
|
|
20
|
+
**Off by default, and silent when off.** No registration means no interceptor
|
|
21
|
+
runtime, no data source, unit resolvers exactly as before, and a byte-identical
|
|
22
|
+
code archive — the same guarantee the cold-start seam makes.
|
|
23
|
+
|
|
24
|
+
**It composes with `RuntimeExtension` without either knowing about the other.**
|
|
25
|
+
The interceptor runtime is built through the standard runtime builder, so
|
|
26
|
+
`Util_Bundle` bundles every registered extension's package into its archive and
|
|
27
|
+
`makeFromCodeAsset` writes `RUNTIME_EXTENSIONS`. An extension that registers its
|
|
28
|
+
interceptor in `onColdStart` is therefore carried into the interceptor runtime
|
|
29
|
+
with no second registration.
|
|
30
|
+
|
|
31
|
+
## What it costs, which is why it is opt-in
|
|
32
|
+
|
|
33
|
+
Interception puts a Lambda invocation in front of **every read** on a
|
|
34
|
+
DynamoDB-backed read model. Reads outrun writes by orders of magnitude, so this
|
|
35
|
+
is the most expensive thing the framework can be asked to switch on. Two
|
|
36
|
+
properties bound it:
|
|
37
|
+
|
|
38
|
+
- **It is a property of the read-model backend, not of the cloud.** A
|
|
39
|
+
Postgres-backed read model already routes through a resolver Lambda, which
|
|
40
|
+
consults the same hook, so interception there costs nothing extra. Only the
|
|
41
|
+
DynamoDB direct-resolver path pays.
|
|
42
|
+
- **It is the only place a read can be refused.** Anything cheaper — a log
|
|
43
|
+
subscription, a stream-fed counter — observes a read after the fact but cannot
|
|
44
|
+
deny it. Where the requirement is enforcement rather than observation, this
|
|
45
|
+
cost is the requirement's price rather than overhead.
|
|
46
|
+
|
|
47
|
+
Granularity is per-deployment, deliberately: an operator asking "what does
|
|
48
|
+
interception cost me" wants one answer, not one per component. Narrowing later is
|
|
49
|
+
additive (a predicate on the registration); widening a per-component switch is
|
|
50
|
+
not.
|
|
51
|
+
|
|
52
|
+
See `docs/plans/done/query-interception-provisioning.md`.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
let enabled = ref(false)
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
Switch query interception on for this deployment. Must run before the
|
|
59
|
+
platform/plugin build in the deploy program (plain statement order — the switch is
|
|
60
|
+
read lazily at each provisioning site), exactly like `Monitoring.use`.
|
|
61
|
+
|
|
62
|
+
Registering the runtime interceptor itself stays separate and is the extension's
|
|
63
|
+
job, through `QueryDb_Callback.registerQueryInterceptor` — typically from a
|
|
64
|
+
`RuntimeExtension` cold-start hook. This call only provisions the path that lets
|
|
65
|
+
that hook be consulted on a read.
|
|
66
|
+
*/
|
|
67
|
+
let use = () => enabled := true
|
|
68
|
+
|
|
69
|
+
/** Whether interception was asked for. Consulted by the provisioning sites. */
|
|
70
|
+
let isEnabled = () => enabled.contents
|
|
71
|
+
|
|
72
|
+
/** Tests only — the switch is process-global, so a test that flips it must put it
|
|
73
|
+
back. */
|
|
74
|
+
let reset = () => enabled := false
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
let enabled = {
|
|
5
|
+
contents: false
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function use() {
|
|
9
|
+
enabled.contents = true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isEnabled() {
|
|
13
|
+
return enabled.contents;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function reset() {
|
|
17
|
+
enabled.contents = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
enabled,
|
|
22
|
+
use,
|
|
23
|
+
isEnabled,
|
|
24
|
+
reset,
|
|
25
|
+
}
|
|
26
|
+
/* No side effect */
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/***
|
|
2
|
+
Carrying `ResourceAttribution` across a `Pulumi.Output` boundary.
|
|
3
|
+
|
|
4
|
+
Separate from `ResourceAttribution` itself on purpose: that module is read by
|
|
5
|
+
code that ends up in a Lambda bundle, and importing the deploy-time engine there
|
|
6
|
+
is a cold-start failure. Everything Pulumi-shaped lives here, where only
|
|
7
|
+
deploy-time builders reach it — the same reason `AdapterDeploytime` sits apart
|
|
8
|
+
from `Adapter`.
|
|
9
|
+
|
|
10
|
+
Both helpers wrap `apply` / `flatMap` rather than the callback alone. Wrapping
|
|
11
|
+
the callback would read more neatly, but it severs the type flow from the Output
|
|
12
|
+
to the callback's parameters, and inference then fails on records the Output was
|
|
13
|
+
the only thing constraining.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** `Pulumi.Output.apply` for a callback that CREATES RESOURCES.
|
|
17
|
+
|
|
18
|
+
An apply callback runs after the enclosing builder's construct has returned,
|
|
19
|
+
so the ambient attribution context — published for the duration of that
|
|
20
|
+
construct and unpublished at the end of it — is already empty by then, and
|
|
21
|
+
anything the callback provisions is attributed to nobody. That is not a gap
|
|
22
|
+
but a wrong answer: an empty plugin means platform-scope substrate.
|
|
23
|
+
|
|
24
|
+
This captures the context where the apply is *registered*, which is
|
|
25
|
+
synchronous inside construct, and reinstates it around the callback. Reach
|
|
26
|
+
for it wherever an apply body reaches a resource; plain `Pulumi.Output.apply`
|
|
27
|
+
stays right for a callback that only shapes data. */
|
|
28
|
+
let applyAttributed = (output, callback) => {
|
|
29
|
+
let captured = ResourceAttribution.current.contents
|
|
30
|
+
output->Pulumi.Output.apply(resolved =>
|
|
31
|
+
captured->ResourceAttribution.within(() => callback(resolved))
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `Pulumi.Output.flatMap` counterpart, for the same reason — a builder that
|
|
36
|
+
resolves one resource's outputs in order to build the next one defers just as
|
|
37
|
+
far. */
|
|
38
|
+
let flatMapAttributed = (output, callback) => {
|
|
39
|
+
let captured = ResourceAttribution.current.contents
|
|
40
|
+
output->Pulumi.Output.flatMap(resolved =>
|
|
41
|
+
captured->ResourceAttribution.within(() => callback(resolved))
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
4
|
+
import * as ResourceAttribution$ReventlessCore from "../ResourceAttribution.res.mjs";
|
|
5
|
+
|
|
6
|
+
function applyAttributed(output, callback) {
|
|
7
|
+
let captured = ResourceAttribution$ReventlessCore.current.contents;
|
|
8
|
+
return output.apply(resolved => ResourceAttribution$ReventlessCore.within(captured, () => callback(resolved)));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function flatMapAttributed(output, callback) {
|
|
12
|
+
let captured = ResourceAttribution$ReventlessCore.current.contents;
|
|
13
|
+
return Output$Pulumi.flatMap(output, resolved => ResourceAttribution$ReventlessCore.within(captured, () => callback(resolved)));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
applyAttributed,
|
|
18
|
+
flatMapAttributed,
|
|
19
|
+
}
|
|
20
|
+
/* Output-Pulumi Not a pure module */
|
|
@@ -42,7 +42,13 @@ let createAggregatesWithoutEventMappers = (
|
|
|
42
42
|
let publishJsons =
|
|
43
43
|
SpecificAggregate.operations(aggregate)->Pulumi.Output.apply(({publishJsons}) => publishJsons)
|
|
44
44
|
publishToAggregates->Dict.set(SpecificAggregate.Spec.name, publishJsons)
|
|
45
|
-
|
|
45
|
+
// `finish` runs from an apply in `finishAggregates`, long after this
|
|
46
|
+
// construct returns — wrap it now, while the plugin is still the ambient
|
|
47
|
+
// one, so whatever it provisions is attributed to that plugin.
|
|
48
|
+
aggregateFinishFns->Dict.set(
|
|
49
|
+
SpecificAggregate.Spec.name,
|
|
50
|
+
ResourceAttribution.deferred(SpecificAggregate.finish),
|
|
51
|
+
)
|
|
46
52
|
aggOutputs
|
|
47
53
|
})
|
|
48
54
|
->Array.map(aggregate => {(aggregate.name, aggregate)})
|
|
@@ -106,7 +112,10 @@ let taskSideEffectFinishFns: array<unit => unit> = []
|
|
|
106
112
|
|
|
107
113
|
let registerTaskSideEffectHandler = (~gate, ~finish) => {
|
|
108
114
|
let _ = taskSideEffectGates->Array.push(gate)
|
|
109
|
-
|
|
115
|
+
// These arrays are module-level and shared by every plugin, so the context has
|
|
116
|
+
// to travel with each entry: by the time `finishTasks` runs them there is no
|
|
117
|
+
// one plugin that would be right for all of them.
|
|
118
|
+
let _ = taskSideEffectFinishFns->Array.push(ResourceAttribution.deferred(finish))
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
// Provision side-effect handler runtimes once every task's handler is ready. Same
|
|
@@ -179,7 +188,16 @@ let createReadModels = (
|
|
|
179
188
|
rmOperations->Pulumi.Output.apply(({enqueueEvent}) => enqueueEvent),
|
|
180
189
|
)
|
|
181
190
|
|
|
182
|
-
|
|
191
|
+
// Deferred to `finishReadModels`, which runs it from an apply — wrap it here
|
|
192
|
+
// so the read model's runtime is attributed to the plugin building it.
|
|
193
|
+
(
|
|
194
|
+
SpecificReadModel.Spec.name,
|
|
195
|
+
{
|
|
196
|
+
outputs: rmOutputs,
|
|
197
|
+
operations: rmOperations,
|
|
198
|
+
finish: ResourceAttribution.deferred(SpecificReadModel.finish),
|
|
199
|
+
},
|
|
200
|
+
)
|
|
183
201
|
})
|
|
184
202
|
readModels->finishReadModels
|
|
185
203
|
readModels->extractReadModelsOutputs
|
|
@@ -4,6 +4,7 @@ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
5
|
import * as Pulumi from "@pulumi/pulumi";
|
|
6
6
|
import * as QueryDb$ReventlessCore from "./QueryDb/QueryDb.res.mjs";
|
|
7
|
+
import * as ResourceAttribution$ReventlessCore from "../ResourceAttribution.res.mjs";
|
|
7
8
|
|
|
8
9
|
let addEventMapperFns = {};
|
|
9
10
|
|
|
@@ -22,7 +23,7 @@ function createAggregatesWithoutEventMappers(aggregates, api, componentRuntime,
|
|
|
22
23
|
aggregateResources[SpecificAggregate.Spec.name] = resources;
|
|
23
24
|
let publishJsons = SpecificAggregate.operations(aggregate).apply(param => param.publishJsons);
|
|
24
25
|
publishToAggregates[SpecificAggregate.Spec.name] = publishJsons;
|
|
25
|
-
aggregateFinishFns[SpecificAggregate.Spec.name] = SpecificAggregate.finish;
|
|
26
|
+
aggregateFinishFns[SpecificAggregate.Spec.name] = ResourceAttribution$ReventlessCore.deferred(SpecificAggregate.finish);
|
|
26
27
|
return aggOutputs;
|
|
27
28
|
}).map(aggregate => [
|
|
28
29
|
aggregate.name,
|
|
@@ -54,7 +55,7 @@ let taskSideEffectFinishFns = [];
|
|
|
54
55
|
|
|
55
56
|
function registerTaskSideEffectHandler(gate, finish) {
|
|
56
57
|
taskSideEffectGates.push(gate);
|
|
57
|
-
taskSideEffectFinishFns.push(finish);
|
|
58
|
+
taskSideEffectFinishFns.push(ResourceAttribution$ReventlessCore.deferred(finish));
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function finishTasks() {
|
|
@@ -102,7 +103,7 @@ function createReadModels(readModels, api, apiRole, componentRuntime, allEventTo
|
|
|
102
103
|
{
|
|
103
104
|
outputs: rmOutputs,
|
|
104
105
|
operations: rmOperations,
|
|
105
|
-
finish: SpecificReadModel.finish
|
|
106
|
+
finish: ResourceAttribution$ReventlessCore.deferred(SpecificReadModel.finish)
|
|
106
107
|
}
|
|
107
108
|
];
|
|
108
109
|
});
|
|
@@ -63,7 +63,10 @@ module Make = (
|
|
|
63
63
|
|
|
64
64
|
let (commandTopic, eventTopic, outgoingJsonEventsHandler) =
|
|
65
65
|
commandTopicResources
|
|
66
|
-
|
|
66
|
+
// The extension point's runtime and its event topic are both built in
|
|
67
|
+
// here, once the command topic's resources resolve — i.e. after the
|
|
68
|
+
// plugin's construct has returned. Reinstate its attribution.
|
|
69
|
+
->ResourceAttribution_Deploytime.flatMapAttributed(commandTopicResources => {
|
|
67
70
|
module ExtensionPointCallback = ExtensionPoint_Callback.Make(
|
|
68
71
|
{
|
|
69
72
|
let publishToAggregates = publishToAggregates
|
|
@@ -15,6 +15,7 @@ import * as EventTopic_Builder$ReventlessCore from "../EventTopic/EventTopic_Bui
|
|
|
15
15
|
import * as CommandTopic_Builder$ReventlessCore from "../CommandTopic/CommandTopic_Builder.res.mjs";
|
|
16
16
|
import * as ExtensionPoint_Callback$ReventlessCore from "./ExtensionPoint_Callback.res.mjs";
|
|
17
17
|
import * as ExtensionPoint_Operations$ReventlessCore from "./ExtensionPoint_Operations.res.mjs";
|
|
18
|
+
import * as ResourceAttribution_Deploytime$ReventlessCore from "../../adapter/ResourceAttribution_Deploytime.res.mjs";
|
|
18
19
|
|
|
19
20
|
function Make(Spec) {
|
|
20
21
|
return Mappings => (RuntimeEnvironment => (CommandTopicChannel => (EventTopicAdapter => (ExtensionPointRuntimeBuilder => (Defaults => {
|
|
@@ -42,7 +43,7 @@ function Make(Spec) {
|
|
|
42
43
|
}, opts);
|
|
43
44
|
let aggregateNames = Stdlib_Array.filterMap(Mappings.mappings, Mapping => Stdlib_Option.map(Mapping.mapOutgoingEvent, param => Mapping.delegateName));
|
|
44
45
|
let commandTopicResources = Adapter$ReventlessCore.resourcesToResolvedOutput(commandTopic.channel.resources);
|
|
45
|
-
let match = Output$Pulumi.unzip3(
|
|
46
|
+
let match = Output$Pulumi.unzip3(ResourceAttribution_Deploytime$ReventlessCore.flatMapAttributed(commandTopicResources, commandTopicResources => {
|
|
46
47
|
let ExtensionPointCallback = ExtensionPoint_Callback$ReventlessCore.Make({
|
|
47
48
|
publishToAggregates: publishToAggregates,
|
|
48
49
|
commandTopicResources: commandTopicResources,
|
|
@@ -648,7 +648,11 @@ module Make = (
|
|
|
648
648
|
queryEngine,
|
|
649
649
|
)
|
|
650
650
|
->Pulumi.Output.all6
|
|
651
|
-
|
|
651
|
+
// `applyAttributed`, not `apply`: this callback provisions real resources —
|
|
652
|
+
// extension points, tasks, the heartbeat, resolvers — and it runs after
|
|
653
|
+
// this construct has returned, when the ambient attribution context is
|
|
654
|
+
// already empty. It reinstates the plugin's context for the duration.
|
|
655
|
+
->ResourceAttribution_Deploytime.applyAttributed(((
|
|
652
656
|
(interstackAdminExtensionPoints, localAdminResolvedEP, _),
|
|
653
657
|
aggregateResources,
|
|
654
658
|
publishToAggregates,
|
|
@@ -36,6 +36,7 @@ import * as ResourceAttribution$ReventlessCore from "../../ResourceAttribution.r
|
|
|
36
36
|
import * as Plugin_EventQuerySchema$ReventlessCore from "./Plugin_EventQuerySchema.res.mjs";
|
|
37
37
|
import * as PluginExtensionPointSpec$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/PluginExtensionPointSpec.res.mjs";
|
|
38
38
|
import * as Plugin_SubscriptionSchema$ReventlessCore from "./Plugin_SubscriptionSchema.res.mjs";
|
|
39
|
+
import * as ResourceAttribution_Deploytime$ReventlessCore from "../../adapter/ResourceAttribution_Deploytime.res.mjs";
|
|
39
40
|
|
|
40
41
|
function Make(Spec) {
|
|
41
42
|
return ApiSpec => (FragmentProvider => (RuntimeEnvironment => (EventCollectorChannel => (QueryEngineAdapter => (PluginExtensionPointRemoteChannel => (HeartbeatRunner => (PluginRuntimeBuilder => (DcbEventLogStorage => (DcbEventTopicPublisher => (DcbCommandTopicChannel => (DcbCommandTopicChannelAsync => {
|
|
@@ -428,7 +429,7 @@ function Make(Spec) {
|
|
|
428
429
|
let schemaPushed = pushSchema !== undefined ? pushSchema(extra$1, version, apiSchemaFragment) : Pulumi.output();
|
|
429
430
|
let apiSchemaFragmentPayload = Plugin_Helpers$ReventlessCore.offloadPayload(apiSchemaFragment, Plugin$Reventless.apiSchemaFragmentSchema, "pluginApiFragments");
|
|
430
431
|
let structurePayload = Stdlib_Option.map(pluginStructure, s => Plugin_Helpers$ReventlessCore.offloadPayload(s, Plugin$Reventless.pluginStructureSchema, "pluginStructures"));
|
|
431
|
-
let builderOutputs = Pulumi.all([
|
|
432
|
+
let builderOutputs = ResourceAttribution_Deploytime$ReventlessCore.applyAttributed(Pulumi.all([
|
|
432
433
|
Pulumi.all([
|
|
433
434
|
interstackAdminExtensionPoints,
|
|
434
435
|
localAdminResolvedEP,
|
|
@@ -439,7 +440,7 @@ function Make(Spec) {
|
|
|
439
440
|
Pulumi.all(Plugin_Helpers$ReventlessCore.publishToReadModels),
|
|
440
441
|
scheduler,
|
|
441
442
|
queryEngine
|
|
442
|
-
])
|
|
443
|
+
]), param => {
|
|
443
444
|
let queryEngine = param[5];
|
|
444
445
|
let scheduler = param[4];
|
|
445
446
|
let publishToReadModels = param[3];
|
|
@@ -383,7 +383,10 @@ let createConnectPluginExtension = (
|
|
|
383
383
|
~opts,
|
|
384
384
|
) =>
|
|
385
385
|
pluginDefinition
|
|
386
|
-
|
|
386
|
+
// Provisions the plugin's connect-extension and its event-collector runtime,
|
|
387
|
+
// so it needs the plugin's attribution reinstated — the apply runs after
|
|
388
|
+
// construct has emptied it.
|
|
389
|
+
->ResourceAttribution_Deploytime.applyAttributed(pluginDefinition => {
|
|
387
390
|
module ConnectPluginExtension = PluginConnectExtension_Builder.Make({
|
|
388
391
|
let pluginDefinition = pluginDefinition
|
|
389
392
|
let uiFragments = uiFragments
|
|
@@ -548,7 +551,9 @@ module MakeEventCollectorHelper = (
|
|
|
548
551
|
resources,
|
|
549
552
|
)
|
|
550
553
|
->Pulumi.Output.all5
|
|
551
|
-
|
|
554
|
+
// Builds the plugin's own event-collector runtime, so it needs the plugin's
|
|
555
|
+
// attribution reinstated — this callback runs after construct emptied it.
|
|
556
|
+
->ResourceAttribution_Deploytime.applyAttributed(((
|
|
552
557
|
pluginDefinition,
|
|
553
558
|
connectExtData,
|
|
554
559
|
extensionsHandlers,
|
|
@@ -58,6 +58,7 @@ import * as OutboundTranslationSlice$ReventlessInterop from "@reventlessdev/reve
|
|
|
58
58
|
import * as PluginExtensionPoint_Plugin$ReventlessCore from "../connect/PluginExtensionPoint_Plugin.res.mjs";
|
|
59
59
|
import * as PluginConnectExtension_Builder$ReventlessCore from "../connect/PluginConnectExtension_Builder.res.mjs";
|
|
60
60
|
import * as PluginConnectExtension_Mapping$ReventlessCore from "../connect/PluginConnectExtension_Mapping.res.mjs";
|
|
61
|
+
import * as ResourceAttribution_Deploytime$ReventlessCore from "../../adapter/ResourceAttribution_Deploytime.res.mjs";
|
|
61
62
|
|
|
62
63
|
let log = Logger$ReventlessCore.fromEnv();
|
|
63
64
|
|
|
@@ -224,7 +225,7 @@ function extractExtensionDefinitions(extensionsOutputs) {
|
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
function createConnectPluginExtension(pluginDefinition, uiFragments, publishToPluginExtensionPoint, publishToAggregates, readModelNamesForSourceName, publishToReadModels, queryEngine, opts) {
|
|
227
|
-
return Output$Pulumi.unzip(
|
|
228
|
+
return Output$Pulumi.unzip(ResourceAttribution_Deploytime$ReventlessCore.applyAttributed(pluginDefinition, pluginDefinition => {
|
|
228
229
|
let ConnectPluginExtension = PluginConnectExtension_Builder$ReventlessCore.Make({
|
|
229
230
|
pluginDefinition: pluginDefinition,
|
|
230
231
|
uiFragments: uiFragments
|
|
@@ -292,13 +293,13 @@ function MakeEventCollectorHelper(RuntimeEnvironment) {
|
|
|
292
293
|
param[0],
|
|
293
294
|
param[1]
|
|
294
295
|
]) : Pulumi.output(undefined);
|
|
295
|
-
return Pulumi.all([
|
|
296
|
+
return ResourceAttribution_Deploytime$ReventlessCore.applyAttributed(Pulumi.all([
|
|
296
297
|
pluginDefinition,
|
|
297
298
|
connectExtData,
|
|
298
299
|
Pulumi.all(extensionsHandlers),
|
|
299
300
|
Pulumi.all(extensionPointsHandlers),
|
|
300
301
|
resources
|
|
301
|
-
])
|
|
302
|
+
]), param => {
|
|
302
303
|
let extensionsHandlers = param[2];
|
|
303
304
|
let connectExtData = param[1];
|
|
304
305
|
let pluginDefinition = param[0];
|
|
@@ -68,3 +68,118 @@ describe("ResourceAttribution — ambient plugin context", () => {
|
|
|
68
68
|
expect(Attribution.current.contents.plugin)->toEqual(None)
|
|
69
69
|
})
|
|
70
70
|
})
|
|
71
|
+
|
|
72
|
+
// The bracket only covers what runs synchronously inside a builder's construct.
|
|
73
|
+
// Builders defer part of their work into a `Pulumi.Output.apply`, which runs
|
|
74
|
+
// after `restore` has emptied the context — so a resource created there is
|
|
75
|
+
// attributed to nobody. These pin the mechanism that carries the context across
|
|
76
|
+
// that gap. Each empties the context first, which is exactly the state a
|
|
77
|
+
// deferred callback finds itself in.
|
|
78
|
+
|
|
79
|
+
describe("ResourceAttribution — carrying the context into deferred work", () => {
|
|
80
|
+
testSync("a deferred callback runs under the context it was wrapped in", () => {
|
|
81
|
+
let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
|
|
82
|
+
let observed = ref(None)
|
|
83
|
+
let finish = Attribution.deferred(() => observed := Attribution.current.contents.plugin)
|
|
84
|
+
Attribution.restore(previous)
|
|
85
|
+
|
|
86
|
+
// Construct has returned; nothing is attributed. This is when the apply fires.
|
|
87
|
+
expect(Attribution.current.contents.plugin)->toEqual(None)
|
|
88
|
+
finish()
|
|
89
|
+
expect(observed.contents)->toEqual(Some("Catalog"))
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
testSync("it puts the context back, so the next callback is not attributed to it", () => {
|
|
93
|
+
let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
|
|
94
|
+
let finish = Attribution.deferred(() => ())
|
|
95
|
+
Attribution.restore(previous)
|
|
96
|
+
|
|
97
|
+
finish()
|
|
98
|
+
expect(Attribution.current.contents.plugin)->toEqual(None)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// The registries these callbacks land in are module-level and shared, so one
|
|
102
|
+
// apply runs several plugins' finish functions back to back. Attributing them
|
|
103
|
+
// all to whichever ran first is the failure this guards.
|
|
104
|
+
testSync("two plugins deferred from one apply are each attributed to themselves", () => {
|
|
105
|
+
let outer = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
|
|
106
|
+
let catalogSaw = ref(None)
|
|
107
|
+
let catalogFinish = Attribution.deferred(() =>
|
|
108
|
+
catalogSaw := Attribution.current.contents.plugin
|
|
109
|
+
)
|
|
110
|
+
Attribution.restore(outer)
|
|
111
|
+
|
|
112
|
+
let previous = Attribution.enter(~platform="online-shop", ~plugin="Ordering")
|
|
113
|
+
let orderingSaw = ref(None)
|
|
114
|
+
let orderingFinish = Attribution.deferred(() =>
|
|
115
|
+
orderingSaw := Attribution.current.contents.plugin
|
|
116
|
+
)
|
|
117
|
+
Attribution.restore(previous)
|
|
118
|
+
|
|
119
|
+
catalogFinish()
|
|
120
|
+
orderingFinish()
|
|
121
|
+
expect((catalogSaw.contents, orderingSaw.contents))->toEqual((
|
|
122
|
+
Some("Catalog"),
|
|
123
|
+
Some("Ordering"),
|
|
124
|
+
))
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
testSync("a throwing callback still puts the context back", () => {
|
|
128
|
+
let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
|
|
129
|
+
let finish: unit => unit = Attribution.deferred(() =>
|
|
130
|
+
JsError.throwWithMessage("finish blew up")
|
|
131
|
+
)
|
|
132
|
+
Attribution.restore(previous)
|
|
133
|
+
|
|
134
|
+
let threw = try {
|
|
135
|
+
finish()
|
|
136
|
+
false
|
|
137
|
+
} catch {
|
|
138
|
+
| _ => true
|
|
139
|
+
}
|
|
140
|
+
expect(threw)->toBe(true)
|
|
141
|
+
expect(Attribution.current.contents.plugin)->toEqual(None)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
testSync("within re-enters a captured context for a callback that takes arguments", () => {
|
|
145
|
+
let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
|
|
146
|
+
let captured = Attribution.current.contents
|
|
147
|
+
Attribution.restore(previous)
|
|
148
|
+
|
|
149
|
+
let observed = captured->Attribution.within(() => Attribution.current.contents.plugin)
|
|
150
|
+
expect(observed)->toEqual(Some("Catalog"))
|
|
151
|
+
expect(Attribution.current.contents.plugin)->toEqual(None)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
testSync("enterCaptured can express the empty context, which enter cannot", () => {
|
|
155
|
+
let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
|
|
156
|
+
let restoredToEmpty = Attribution.enterCaptured({platform: None, plugin: None})
|
|
157
|
+
expect(Attribution.current.contents.plugin)->toEqual(None)
|
|
158
|
+
expect(restoredToEmpty.plugin)->toEqual(Some("Catalog"))
|
|
159
|
+
Attribution.restore(previous)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// The combinator being right is not the same as it being wired in. This one
|
|
164
|
+
// exercises the production choke point: a task side-effect handler registered
|
|
165
|
+
// during construct, then run the way `finishTasks` runs it — from an apply,
|
|
166
|
+
// with nothing attributed.
|
|
167
|
+
describe("Builder_Helpers.registerTaskSideEffectHandler — attribution survives the deferral", () => {
|
|
168
|
+
testSync("the registered finish sees the plugin that registered it", () => {
|
|
169
|
+
let before = Builder_Helpers.taskSideEffectFinishFns->Array.length
|
|
170
|
+
let observed = ref(None)
|
|
171
|
+
|
|
172
|
+
let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
|
|
173
|
+
Builder_Helpers.registerTaskSideEffectHandler(
|
|
174
|
+
~gate=Pulumi.Output.make(),
|
|
175
|
+
~finish=() => observed := Attribution.current.contents.plugin,
|
|
176
|
+
)
|
|
177
|
+
Attribution.restore(previous)
|
|
178
|
+
|
|
179
|
+
switch Builder_Helpers.taskSideEffectFinishFns->Array.get(before) {
|
|
180
|
+
| Some(finish) => finish()
|
|
181
|
+
| None => JsError.throwWithMessage("handler was not registered")
|
|
182
|
+
}
|
|
183
|
+
expect(observed.contents)->toEqual(Some("Catalog"))
|
|
184
|
+
})
|
|
185
|
+
})
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
4
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
3
5
|
import * as ComponentType$ReventlessCore from "../src/ComponentType.res.mjs";
|
|
6
|
+
import * as Builder_Helpers$ReventlessCore from "../src/components/Builder_Helpers.res.mjs";
|
|
4
7
|
import * as ResourceAttribution$ReventlessCore from "../src/ResourceAttribution.res.mjs";
|
|
5
8
|
|
|
6
9
|
globalThis.describe("ResourceAttribution.Scope — wire spellings", () => {
|
|
@@ -67,6 +70,109 @@ globalThis.describe("ResourceAttribution — ambient plugin context", () => {
|
|
|
67
70
|
});
|
|
68
71
|
});
|
|
69
72
|
|
|
73
|
+
globalThis.describe("ResourceAttribution — carrying the context into deferred work", () => {
|
|
74
|
+
globalThis.test("a deferred callback runs under the context it was wrapped in", () => {
|
|
75
|
+
let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
|
|
76
|
+
let observed = {
|
|
77
|
+
contents: undefined
|
|
78
|
+
};
|
|
79
|
+
let finish = ResourceAttribution$ReventlessCore.deferred(() => {
|
|
80
|
+
observed.contents = ResourceAttribution$ReventlessCore.current.contents.plugin;
|
|
81
|
+
});
|
|
82
|
+
ResourceAttribution$ReventlessCore.restore(previous);
|
|
83
|
+
globalThis.expect(ResourceAttribution$ReventlessCore.current.contents.plugin).toEqual(undefined);
|
|
84
|
+
finish();
|
|
85
|
+
globalThis.expect(observed.contents).toEqual("Catalog");
|
|
86
|
+
});
|
|
87
|
+
globalThis.test("it puts the context back, so the next callback is not attributed to it", () => {
|
|
88
|
+
let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
|
|
89
|
+
let finish = ResourceAttribution$ReventlessCore.deferred(() => {});
|
|
90
|
+
ResourceAttribution$ReventlessCore.restore(previous);
|
|
91
|
+
finish();
|
|
92
|
+
globalThis.expect(ResourceAttribution$ReventlessCore.current.contents.plugin).toEqual(undefined);
|
|
93
|
+
});
|
|
94
|
+
globalThis.test("two plugins deferred from one apply are each attributed to themselves", () => {
|
|
95
|
+
let outer = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
|
|
96
|
+
let catalogSaw = {
|
|
97
|
+
contents: undefined
|
|
98
|
+
};
|
|
99
|
+
let catalogFinish = ResourceAttribution$ReventlessCore.deferred(() => {
|
|
100
|
+
catalogSaw.contents = ResourceAttribution$ReventlessCore.current.contents.plugin;
|
|
101
|
+
});
|
|
102
|
+
ResourceAttribution$ReventlessCore.restore(outer);
|
|
103
|
+
let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Ordering");
|
|
104
|
+
let orderingSaw = {
|
|
105
|
+
contents: undefined
|
|
106
|
+
};
|
|
107
|
+
let orderingFinish = ResourceAttribution$ReventlessCore.deferred(() => {
|
|
108
|
+
orderingSaw.contents = ResourceAttribution$ReventlessCore.current.contents.plugin;
|
|
109
|
+
});
|
|
110
|
+
ResourceAttribution$ReventlessCore.restore(previous);
|
|
111
|
+
catalogFinish();
|
|
112
|
+
orderingFinish();
|
|
113
|
+
globalThis.expect([
|
|
114
|
+
catalogSaw.contents,
|
|
115
|
+
orderingSaw.contents
|
|
116
|
+
]).toEqual([
|
|
117
|
+
"Catalog",
|
|
118
|
+
"Ordering"
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
globalThis.test("a throwing callback still puts the context back", () => {
|
|
122
|
+
let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
|
|
123
|
+
let finish = ResourceAttribution$ReventlessCore.deferred(() => Stdlib_JsError.throwWithMessage("finish blew up"));
|
|
124
|
+
ResourceAttribution$ReventlessCore.restore(previous);
|
|
125
|
+
let threw;
|
|
126
|
+
try {
|
|
127
|
+
finish();
|
|
128
|
+
threw = false;
|
|
129
|
+
} catch (exn) {
|
|
130
|
+
threw = true;
|
|
131
|
+
}
|
|
132
|
+
globalThis.expect(threw).toBe(true);
|
|
133
|
+
globalThis.expect(ResourceAttribution$ReventlessCore.current.contents.plugin).toEqual(undefined);
|
|
134
|
+
});
|
|
135
|
+
globalThis.test("within re-enters a captured context for a callback that takes arguments", () => {
|
|
136
|
+
let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
|
|
137
|
+
let captured = ResourceAttribution$ReventlessCore.current.contents;
|
|
138
|
+
ResourceAttribution$ReventlessCore.restore(previous);
|
|
139
|
+
let observed = ResourceAttribution$ReventlessCore.within(captured, () => ResourceAttribution$ReventlessCore.current.contents.plugin);
|
|
140
|
+
globalThis.expect(observed).toEqual("Catalog");
|
|
141
|
+
globalThis.expect(ResourceAttribution$ReventlessCore.current.contents.plugin).toEqual(undefined);
|
|
142
|
+
});
|
|
143
|
+
globalThis.test("enterCaptured can express the empty context, which enter cannot", () => {
|
|
144
|
+
let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
|
|
145
|
+
let restoredToEmpty = ResourceAttribution$ReventlessCore.enterCaptured({
|
|
146
|
+
platform: undefined,
|
|
147
|
+
plugin: undefined
|
|
148
|
+
});
|
|
149
|
+
globalThis.expect(ResourceAttribution$ReventlessCore.current.contents.plugin).toEqual(undefined);
|
|
150
|
+
globalThis.expect(restoredToEmpty.plugin).toEqual("Catalog");
|
|
151
|
+
ResourceAttribution$ReventlessCore.restore(previous);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
globalThis.describe("Builder_Helpers.registerTaskSideEffectHandler — attribution survives the deferral", () => {
|
|
156
|
+
globalThis.test("the registered finish sees the plugin that registered it", () => {
|
|
157
|
+
let before = Builder_Helpers$ReventlessCore.taskSideEffectFinishFns.length;
|
|
158
|
+
let observed = {
|
|
159
|
+
contents: undefined
|
|
160
|
+
};
|
|
161
|
+
let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
|
|
162
|
+
Builder_Helpers$ReventlessCore.registerTaskSideEffectHandler(Pulumi.output(), () => {
|
|
163
|
+
observed.contents = ResourceAttribution$ReventlessCore.current.contents.plugin;
|
|
164
|
+
});
|
|
165
|
+
ResourceAttribution$ReventlessCore.restore(previous);
|
|
166
|
+
let finish = Builder_Helpers$ReventlessCore.taskSideEffectFinishFns[before];
|
|
167
|
+
if (finish !== undefined) {
|
|
168
|
+
finish();
|
|
169
|
+
} else {
|
|
170
|
+
Stdlib_JsError.throwWithMessage("handler was not registered");
|
|
171
|
+
}
|
|
172
|
+
globalThis.expect(observed.contents).toEqual("Catalog");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
70
176
|
let Attribution;
|
|
71
177
|
|
|
72
178
|
export {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// The seam is a single process-global switch, so what is worth pinning is the
|
|
4
|
+
// default (off) and the fact that switching it on does not also register a
|
|
5
|
+
// runtime interceptor — the two are deliberately separate decisions, and
|
|
6
|
+
// conflating them would provision an interceptor path for a hook nobody set.
|
|
7
|
+
|
|
8
|
+
module Interception = QueryInterception
|
|
9
|
+
|
|
10
|
+
describe("QueryInterception", () => {
|
|
11
|
+
testSync("off by default — no registration, nothing provisioned", () => {
|
|
12
|
+
Interception.reset()
|
|
13
|
+
expect(Interception.isEnabled())->toBe(false)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
testSync("use switches it on for the deployment", () => {
|
|
17
|
+
Interception.reset()
|
|
18
|
+
Interception.use()
|
|
19
|
+
expect(Interception.isEnabled())->toBe(true)
|
|
20
|
+
Interception.reset()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
testSync("use is idempotent — an extension may call it more than once", () => {
|
|
24
|
+
Interception.reset()
|
|
25
|
+
Interception.use()
|
|
26
|
+
Interception.use()
|
|
27
|
+
expect(Interception.isEnabled())->toBe(true)
|
|
28
|
+
Interception.reset()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
testSync("reset puts it back, so one test cannot leak into the next", () => {
|
|
32
|
+
Interception.use()
|
|
33
|
+
Interception.reset()
|
|
34
|
+
expect(Interception.isEnabled())->toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Provisioning the path and registering the hook are separate on purpose: the
|
|
38
|
+
// framework provisions, the extension decides what runs. Asking for
|
|
39
|
+
// interception must not fabricate an interceptor.
|
|
40
|
+
testSync("switching provisioning on does not register a runtime interceptor", () => {
|
|
41
|
+
QueryDb_Callback.clearQueryInterceptor()
|
|
42
|
+
Interception.use()
|
|
43
|
+
expect(QueryDb_Callback.queryInterceptorHook.contents->Option.isNone)->toBe(true)
|
|
44
|
+
Interception.reset()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
testSync("and registering a runtime interceptor does not switch provisioning on", () => {
|
|
48
|
+
Interception.reset()
|
|
49
|
+
QueryDb_Callback.registerQueryInterceptor((~identity as _, ~readModelName as _, ~args as _) =>
|
|
50
|
+
Promise.resolve(QueryDb_Callback.Allow)
|
|
51
|
+
)
|
|
52
|
+
expect(Interception.isEnabled())->toBe(false)
|
|
53
|
+
QueryDb_Callback.clearQueryInterceptor()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as QueryDb_Callback$ReventlessCore from "../../src/components/QueryDb/QueryDb_Callback.res.mjs";
|
|
5
|
+
import * as QueryInterception$ReventlessCore from "../../src/adapter/QueryInterception/QueryInterception.res.mjs";
|
|
6
|
+
|
|
7
|
+
globalThis.describe("QueryInterception", () => {
|
|
8
|
+
globalThis.test("off by default — no registration, nothing provisioned", () => {
|
|
9
|
+
QueryInterception$ReventlessCore.reset();
|
|
10
|
+
globalThis.expect(QueryInterception$ReventlessCore.isEnabled()).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
globalThis.test("use switches it on for the deployment", () => {
|
|
13
|
+
QueryInterception$ReventlessCore.reset();
|
|
14
|
+
QueryInterception$ReventlessCore.use();
|
|
15
|
+
globalThis.expect(QueryInterception$ReventlessCore.isEnabled()).toBe(true);
|
|
16
|
+
QueryInterception$ReventlessCore.reset();
|
|
17
|
+
});
|
|
18
|
+
globalThis.test("use is idempotent — an extension may call it more than once", () => {
|
|
19
|
+
QueryInterception$ReventlessCore.reset();
|
|
20
|
+
QueryInterception$ReventlessCore.use();
|
|
21
|
+
QueryInterception$ReventlessCore.use();
|
|
22
|
+
globalThis.expect(QueryInterception$ReventlessCore.isEnabled()).toBe(true);
|
|
23
|
+
QueryInterception$ReventlessCore.reset();
|
|
24
|
+
});
|
|
25
|
+
globalThis.test("reset puts it back, so one test cannot leak into the next", () => {
|
|
26
|
+
QueryInterception$ReventlessCore.use();
|
|
27
|
+
QueryInterception$ReventlessCore.reset();
|
|
28
|
+
globalThis.expect(QueryInterception$ReventlessCore.isEnabled()).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
globalThis.test("switching provisioning on does not register a runtime interceptor", () => {
|
|
31
|
+
QueryDb_Callback$ReventlessCore.clearQueryInterceptor();
|
|
32
|
+
QueryInterception$ReventlessCore.use();
|
|
33
|
+
globalThis.expect(Stdlib_Option.isNone(QueryDb_Callback$ReventlessCore.queryInterceptorHook.contents)).toBe(true);
|
|
34
|
+
QueryInterception$ReventlessCore.reset();
|
|
35
|
+
});
|
|
36
|
+
globalThis.test("and registering a runtime interceptor does not switch provisioning on", () => {
|
|
37
|
+
QueryInterception$ReventlessCore.reset();
|
|
38
|
+
QueryDb_Callback$ReventlessCore.registerQueryInterceptor((param, param$1, param$2) => Promise.resolve("Allow"));
|
|
39
|
+
globalThis.expect(QueryInterception$ReventlessCore.isEnabled()).toBe(false);
|
|
40
|
+
QueryDb_Callback$ReventlessCore.clearQueryInterceptor();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let Interception;
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
Interception,
|
|
48
|
+
}
|
|
49
|
+
/* Not a pure module */
|