@opetope/react 0.11.0 → 0.12.1
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 +412 -17
- package/README.md +41 -360
- package/dist/command-hook-controller.d.ts +4 -4
- package/dist/command-hook.d.ts +3 -3
- package/dist/command.d.ts +8 -8
- package/dist/contribution-frame-CCKnOxZR.js +2 -0
- package/dist/contribution-frame-CCKnOxZR.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model-binding.d.ts +4 -4
- package/dist/model-hook.d.ts +2 -2
- package/dist/resource-command.d.ts +11 -4
- package/dist/scenario-slot.d.ts +6 -3
- package/dist/scenario-types.d.ts +2 -2
- package/dist/scenario.d.ts +1 -2
- package/dist/{commands-hook.d.ts → selection-hooks.d.ts} +4 -11
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/package.json +5 -6
- package/README.ru.md +0 -414
- package/dist/contribution-frame-DkNmjChZ.js +0 -2
- package/dist/contribution-frame-DkNmjChZ.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
# `@opetope/react`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React hooks and slots for Opetope models. A contribution grants the models its UI may read; hooks subscribe to selected data and expose commands with local status.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npm install @opetope/core @opetope/runtime @opetope/react 'react@^19
|
|
8
|
+
npm install --save-exact @opetope/core @opetope/runtime @opetope/react 'react@^19' 'react-dom@^19'
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Use matching Opetope versions
|
|
12
|
-
|
|
11
|
+
Use matching exact Opetope versions; for release candidates, install every Opetope package from `@next`.
|
|
12
|
+
Packages are ESM-only and support Node 20.19+. React integrations support React and React DOM 19.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
`node_modules/@opetope/runtime/docs/spec.md` or `spec.ru.md`; recipes are in `cookbook.md` and `cookbook.ru.md`.
|
|
16
|
-
No GitHub access is needed to read those installed guides.
|
|
14
|
+
## Example
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
This focused example shows the package boundary. [Start](../runtime/docs/start.md) includes a complete
|
|
17
|
+
feature, host, React entry and cleanup in one visible module.
|
|
18
|
+
|
|
19
|
+
<!--example id="doc-readme-react-0"
|
|
20
|
+
/* … */
|
|
21
|
+
-->
|
|
19
22
|
|
|
20
23
|
```tsx
|
|
21
24
|
import { defineModel } from '@opetope/core';
|
|
22
|
-
import type {
|
|
25
|
+
import type { Command, Readable } from '@opetope/core';
|
|
23
26
|
import { defineSlot, requiresModels, Slot, useModel } from '@opetope/react';
|
|
24
27
|
|
|
25
28
|
const CounterModel = defineModel<{
|
|
26
29
|
readonly count: Readable<number>;
|
|
27
|
-
readonly increment:
|
|
30
|
+
readonly increment: Command<void, void>;
|
|
28
31
|
}>('example.counter.model');
|
|
29
32
|
const CounterSlot = defineSlot<{ readonly label: string }>('example.counter.slot');
|
|
30
33
|
|
|
@@ -39,7 +42,7 @@ const CounterButton = requiresModels([CounterModel])(({ label }: { readonly labe
|
|
|
39
42
|
|
|
40
43
|
return (
|
|
41
44
|
<button disabled={inFlight} onClick={() => void run()}>
|
|
42
|
-
{outcome?.
|
|
45
|
+
{outcome?.kind === 'failed' ? 'Retry' : `${label}: ${count}`}
|
|
43
46
|
</button>
|
|
44
47
|
);
|
|
45
48
|
});
|
|
@@ -49,364 +52,42 @@ function CounterArea() {
|
|
|
49
52
|
}
|
|
50
53
|
```
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
model declares it with `requiresModels`. `useModel` reads exactly that authority frame. `useCommand` returns
|
|
54
|
-
`{ run, inFlight, outcome }` and always resolves the outcome, so a normal cancellation never becomes an unhandled
|
|
55
|
-
rejection (D116, D292).
|
|
56
|
-
|
|
57
|
-
`outcome` is the last settled non-cancelled outcome of this consumer: `ok` takes the place of a previous `failed`
|
|
58
|
-
and a `failed` the place of a previous `ok`, a `cancelled` outcome moves nothing, and starting a run clears nothing,
|
|
59
|
-
so a failure stays readable while the retry is in flight. Before the first settle it is `undefined`. Read it through
|
|
60
|
-
its discriminant — `outcome?.status === 'failed' ? outcome.error : null`.
|
|
61
|
-
|
|
62
|
-
A command without input binds to an event through an arrow: `onClick={() => void logout.run()}`. The arrow is what
|
|
63
|
-
keeps the React event out of the Call and the promise from hanging; `onClick={logout.run}` does not compile, because
|
|
64
|
-
a `MouseEvent` is not a `void` input. Use `run(input, options?)` for data, an outcome, callbacks or a per-run signal.
|
|
65
|
-
Where a lint config bans an inline arrow prop (`react-perf/jsx-no-new-function-as-prop`, `react/jsx-no-bind`), hoist
|
|
66
|
-
it with `useCallback(() => void logout.run(), [logout.run])`: the dependency is `run`, never the hook object (D290).
|
|
67
|
-
|
|
68
|
-
`run` is stable while the invoker stays the same; the returned object is a snapshot of the render it was read in and
|
|
69
|
-
changes as `inFlight` or `outcome` changes, so an effect or a memo depends on `run` and never on the hook — the rule
|
|
70
|
-
`opetope/no-command-in-deps` holds this for the author (D290). A `run` captured from a replaced or unmounted
|
|
71
|
-
consumer is fenced; it does not start work on the replacement.
|
|
72
|
-
|
|
73
|
-
The hook schedules nothing. Every `run` reaches the call, and the policy the call was created with decides what
|
|
74
|
-
happens to it (D203). For an absolute value setter the model declares `policy: 'latest'` in `context.call`, and the
|
|
75
|
-
newest input then replaces the waiting one there (D185). What the consumer keeps is its own: an already-aborted
|
|
76
|
-
input signal is refused as `cancelled` without reaching the call, a run of an unmounted consumer is refused the same
|
|
77
|
-
way, `inFlight` is true while any run this consumer started is unsettled, and every run carries its own callbacks
|
|
78
|
-
and signal.
|
|
79
|
-
|
|
80
|
-
To share equivalent pending or running work, the model declares `dedupe: true` or `dedupe: input => key`
|
|
81
|
-
on `context.call`. Sharing keeps the first input and each caller's independent cancellation; neither form may
|
|
82
|
-
be combined with `latest` (D277). These are Call options, not hook options.
|
|
83
|
-
|
|
84
|
-
For several commands use an object selection (D174):
|
|
85
|
-
|
|
86
|
-
```tsx
|
|
87
|
-
const { save, remove } = useCommands({ save: model.save, remove: other.remove });
|
|
88
|
-
|
|
89
|
-
void save.run(input);
|
|
90
|
-
const saving = save.inFlight;
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Import `useCommands` from `@opetope/react`. Each selected field returns the same `CommandHook` as `useCommand`,
|
|
94
|
-
with its own input/output types and its own `inFlight` and `outcome`. Keys are local UI names; commands may come from
|
|
95
|
-
different granted models. There is no second argument: each call carries its own policy. Its own case is a key set
|
|
96
|
-
the component does not know in advance — hooks are not called in a loop; a fixed set is `useCommand` per word or one
|
|
97
|
-
`useModel` selection. It is proven by the packaged consumer of `ci:pack` and is a candidate to leave this entry
|
|
98
|
-
until a second application is measured (D301).
|
|
99
|
-
|
|
100
|
-
An inline object needs no `useMemo`. Reordering keys or updating a sibling preserves an unchanged field's object
|
|
101
|
-
and `.run`. Adding or removing keys does not change the number of React hooks. Removing a key or replacing its
|
|
102
|
-
command starts a fresh local status for that field; abandoned renders leave the
|
|
103
|
-
committed selection intact. Two aliases of one Call have independent local statuses, just like two `useCommand`
|
|
104
|
-
consumers; they use the same Call policy and mount command record (D197, D203). A group introduces no shared busy state, queue or transaction; controls that overwrite one value still
|
|
105
|
-
need one semantic command. Only own enumerable properties are selected, including symbol keys.
|
|
106
|
-
|
|
107
|
-
## Selecting data and commands
|
|
108
|
-
|
|
109
|
-
`useModel(Declaration, (model, { read }) => ({ ... }))` combines explicit data selection and command consumers (D205, D214).
|
|
110
|
-
|
|
111
|
-
A model selection can use a named interface without an index signature (D217). Its result is a flat data record;
|
|
112
|
-
arrays, functions, constructors and built-in collection/date/promise objects are not selection records.
|
|
113
|
-
`read(source)` returns its snapshot; `read(source, project)` returns a projection. Authentic Calls selected as record
|
|
114
|
-
fields become the same `CommandHook` as `useCommands`, including independent alias statuses and stable `.run`.
|
|
115
|
-
A single selected command needs no nested hooks:
|
|
116
|
-
|
|
117
|
-
```tsx
|
|
118
|
-
const { logout } = useModel(AuthModel, auth => ({ logout: auth.logout }));
|
|
119
|
-
return (
|
|
120
|
-
<button disabled={logout.inFlight} onClick={() => void logout.run()}>
|
|
121
|
-
Log out
|
|
122
|
-
</button>
|
|
123
|
-
);
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
The one-argument form still returns the granted model; individual hooks remain available.
|
|
127
|
-
|
|
128
|
-
Only explicitly read sources are subscribed, once per distinct Readable in the selection. Returned data fields are
|
|
129
|
-
compared with `Object.is`; select scalar fields, spread a projected record into the selection, or return stable
|
|
130
|
-
references. A newly allocated nested object is a changed field. The callback is pure: no hooks, commands or side effects.
|
|
131
|
-
A read or selector error reaches the nearest React error boundary. Changing the selected sources or command keys
|
|
132
|
-
changes their subscriptions/consumers at commit; an abandoned render cannot replace committed authority.
|
|
133
|
-
|
|
134
|
-
The hook neither creates a model nor acquires a feature. It leases exactly the resources its selection names as
|
|
135
|
-
values — that field answers a `ResourceHook`, and one lease per Resource identity covers the mount even when two
|
|
136
|
-
keys name one Resource (D321, D359) — and nothing else: a
|
|
137
|
-
`read(resource.state)` leases nothing, and neither does the one-argument form. Combining hooks does not promise fewer
|
|
138
|
-
source subscriptions or faster renders; a raw `useModel` import now also includes the selection implementation.
|
|
139
|
-
There is no additional Call scheduler.
|
|
140
|
-
|
|
141
|
-
## Application host
|
|
142
|
-
|
|
143
|
-
The host opens a feature graph with `openApplication`. Ready features publish their UI contributions atomically;
|
|
144
|
-
the ordinary `Slot` mounts them into consumer-owned targets:
|
|
145
|
-
|
|
146
|
-
```tsx
|
|
147
|
-
<Slot target={exampleFooterSlot} />
|
|
148
|
-
<Slot target={exampleSettingsSlot} />
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
The application owns feature lifetimes; each feature's UI mounts through its contributions.
|
|
152
|
-
|
|
153
|
-
## Feature demand boundary
|
|
154
|
-
|
|
155
|
-
```tsx
|
|
156
|
-
import { Slot } from '@opetope/react';
|
|
157
|
-
import { FeatureBoundary, useFeatureRetry } from '@opetope/react/integration';
|
|
158
|
-
|
|
159
|
-
function Retry() {
|
|
160
|
-
return <button onClick={useFeatureRetry()}>Try again</button>;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
<FeatureBoundary demand={host} error={<Retry />} fallback={<Spinner />}>
|
|
164
|
-
<Slot props={{ itemId, onConfirmed }} target={confirmActionContentSlot} />
|
|
165
|
-
</FeatureBoundary>;
|
|
166
|
-
|
|
167
|
-
// or with render callbacks, when the ready instance itself is needed (D177)
|
|
168
|
-
<FeatureBoundary
|
|
169
|
-
demand={host}
|
|
170
|
-
error={({ error, retry }) => <Failure error={error} onRetry={retry} />}
|
|
171
|
-
fallback={<Spinner />}
|
|
172
|
-
>
|
|
173
|
-
{({ exports }) => <TasksResourceLease resource={exports.tasks}>{children}</TasksResourceLease>}
|
|
174
|
-
</FeatureBoundary>;
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
`FeatureBoundary` consumes a `FeatureDemandSource` supplied by host integration. It holds the demand for the feature,
|
|
178
|
-
shows the ready branch once that demand is ready and isolates `useFeatureRetry` inside the error subtree. A branch may be a node or a
|
|
179
|
-
render callback: the callback of `children` receives the ready instance typed by the demand, the callback of `error`
|
|
180
|
-
receives `{ error, retry }`, and only the branch that is shown runs. The callback needs no `useFeature` of its own,
|
|
181
|
-
so a ready consumer does not acquire the demand a second time; hooks still live in child components, not in the
|
|
182
|
-
callback. There is no separate hook for
|
|
183
|
-
reading the error: the host passed it into `error` itself, so it knows it without a second word (D142). UI enters a feature through `Slot`: there is no
|
|
184
|
-
root, no `ui` section and no second binding API any more (D85).
|
|
185
|
-
|
|
186
|
-
Both the boundary and the bare `useFeature` are exercised by the packaged consumer of `ci:pack`, which checks the
|
|
187
|
-
law they share: one lease per mount, kept across a settled `retry` and released on unmount. `useFeature` is the
|
|
188
|
-
hook half used where a host renders the readiness itself, and it is a candidate to leave this entry until a second
|
|
189
|
-
application is measured; `FeatureBoundary` is not a pair with `ContributionBoundary` and stays (D301).
|
|
190
|
-
|
|
191
|
-
## Contributions
|
|
192
|
-
|
|
193
|
-
```tsx
|
|
194
|
-
import { defineSlot, defineSwitchSlot, Slot } from '@opetope/react';
|
|
195
|
-
|
|
196
|
-
const HeaderEnd = defineSlot<{ readonly mode: 'desktop' | 'phone' }>('example.headerEnd');
|
|
197
|
-
const PageEnd = defineSwitchSlot<'home' | 'wallet', { readonly compact: boolean }>('example.pageEnd');
|
|
198
|
-
|
|
199
|
-
function Header({ mode }: { readonly mode: 'desktop' | 'phone' }) {
|
|
200
|
-
return <Slot props={{ mode }} target={HeaderEnd} />;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function HomePageEnd() {
|
|
204
|
-
return <Slot props={{ compact: true }} target={PageEnd('home')} />;
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
`defineSlot` creates an authentic target with an ordered set of contributions; `defineSwitchSlot` lazily creates and
|
|
209
|
-
caches a stable target per route. `Slot` reads the atomic snapshot and renders every `{ Component }` in the authority
|
|
210
|
-
frame of the instance that gave the contribution. The order and the stable key come from the contribution entries of the core, so
|
|
211
|
-
React introduces neither a second comparator nor a second lifetime registry. The packaged consumer of `ci:pack`
|
|
212
|
-
exercises that caching and routes one contribution through it (D301).
|
|
213
|
-
|
|
214
|
-
`useResource(resource)` answers `{ invalidate, pagination, refresh, retry, state }`: the `ResourceState` through
|
|
215
|
-
`useSyncExternalStore`, the lease `resource.acquire()` answers taken in a commit effect for as long as the component
|
|
216
|
-
is mounted, and the three verbs as ordinary `CommandHook` consumers (D359). An interrupted render therefore opens
|
|
217
|
-
nothing, and when the reference changes the state follows the new Resource while the effect cleanup releases the
|
|
218
|
-
previous lease. The number of React hooks does not depend on the Resource: a declaration without `pagination`
|
|
219
|
-
subscribes to a constant source and keeps a `loadNext` that answers `skipped`, while a declaration that carried the
|
|
220
|
-
capability answers `pagination` as `{ loadNext, state }` (D356). A field of a `useModel` selection whose value is a
|
|
221
|
-
Resource answers the same hook and takes the same lease, so a model declares no `Call<void, void>` around
|
|
222
|
-
`resource.refresh()` to give a button its `inFlight`. `useReadable(resource)` is a compile error, because a Resource
|
|
223
|
-
is not a `Readable`; `useReadable(resource.state)` and a `read(resource.state)` in a selector stay passive and lease
|
|
224
|
-
nothing — observing and holding are independent questions, so a selector that reads the state and a `useResource`
|
|
225
|
-
beside it subscribe twice (D349). The compile-checked example of specification §2.13 shows both sides of that
|
|
226
|
-
(D301, D321).
|
|
227
|
-
|
|
228
|
-
`refresh.run()`, `retry.run()` and `invalidate.run()` reach the Resource, which is owned by the model that declared
|
|
229
|
-
it: `inFlight` is true until the outcome of the operation settles, `outcome` carries that `ResourceOperationOutcome`
|
|
230
|
-
as the `value` of an `ok` status, and `run(undefined, { signal })` cancels the wait of this consumer and not the work
|
|
231
|
-
the model owns. Two consumers of one Resource keep separate statuses, and `run` keeps its identity while the Resource
|
|
232
|
-
does. A render that caught up with retirement renders the terminal record — `idle` with reason `retired` for a Resource
|
|
233
|
-
a model owns, `unleased` for a feature's facade, which is a reference that outlives the instance — and unmounts,
|
|
234
|
-
releasing its lease (D359, D364). Nothing here refuses a read because a lifetime ended: a source whose owner retired
|
|
235
|
-
stops and keeps answering its last value, so `useReadable`, `useSelector` with an inline selector and a selection's
|
|
236
|
-
`read` each read on and need no memory of their own. A refusal that remains is work that failed — a state that
|
|
237
|
-
failed, a broken pagination cursor — and it still reaches the render.
|
|
238
|
-
|
|
239
|
-
Direct component props, a contribution's `props` adapter and its model's props `Readable` share the same
|
|
240
|
-
mount-owned snapshot. Incoming slot props publish in layout before paint, so direct and adapted renders cannot
|
|
241
|
-
mix new props with an old model snapshot. Model factory failures clean up partially created kernels.
|
|
242
|
-
Models belong to commit: an abandoned render creates no model to clean up (D170, D188).
|
|
243
|
-
StrictMode effect replay and Suspense hide/reveal preserve the committed model bundle and state. Only actual
|
|
244
|
-
identity replacement or unmount releases it; a hidden unmount releases in a microtask after React has disconnected
|
|
245
|
-
the layout effects (D209).
|
|
246
|
-
|
|
247
|
-
Demand retry is scoped to the source identity. Replacing a boundary's demand source allows its new retry to run
|
|
248
|
-
even if the previous source's retry is still pending.
|
|
55
|
+
## Documentation
|
|
249
56
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
`
|
|
253
|
-
|
|
254
|
-
```tsx
|
|
255
|
-
import { ContributionBoundary } from '@opetope/react/integration';
|
|
256
|
-
|
|
257
|
-
<ContributionBoundary error={({ error, retry }) => <FailedContribution error={error} onRetry={retry} />}>
|
|
258
|
-
<Slot target={applicationSurface} />
|
|
259
|
-
</ContributionBoundary>;
|
|
260
|
-
```
|
|
57
|
+
The [reference](../runtime/docs/reference/react.md) owns the detailed contract, options and failure semantics.
|
|
58
|
+
[Guides](../runtime/docs/guides/index.md) show individual tasks. Documentation is shipped with
|
|
59
|
+
`@opetope/runtime` in `docs/`, so an installed application can read it without access to this repository.
|
|
261
60
|
|
|
262
|
-
|
|
263
|
-
`
|
|
264
|
-
|
|
265
|
-
created again. The reporter of the publishing feature receives the same identity on a `ContributionError` whose
|
|
266
|
-
`cause` is the original error, with code `render-failed` or `error-content-failed`. Without the provider a failed mount renders nothing — containment
|
|
267
|
-
never depends on it. Error content that throws is contained the same way and reported, never escalated.
|
|
268
|
-
|
|
269
|
-
## Scenario tests and physical activity
|
|
270
|
-
|
|
271
|
-
`createScenario(application, options)` from `@opetope/react/testing` opens the real application and its existing
|
|
272
|
-
inspection session (D206, D215). Supply the normal `imports`/`conditions` and a test-owned
|
|
273
|
-
`host.mount(Component)` adapter returning an `unmount()` handle. The package adds no DOM renderer or test-runner
|
|
274
|
-
dependency. `scenario.mount(target, { props })` uses the published Slot contributions and returns
|
|
275
|
-
`{ host, updateProps, unmount }`; `host` is the renderer's original result. Typed targets require `options.props`,
|
|
276
|
-
while targets without props omit it, exactly as with `Slot` (D217). Fixture commands do not bypass authority.
|
|
277
|
-
|
|
278
|
-
As with `openApplication`, omit `conditions` when no enabled feature requires a host-bound condition. Conditions
|
|
279
|
-
computed through `from`/`select` bind automatically; external conditions still require their sources (D271).
|
|
280
|
-
|
|
281
|
-
The synchronous constructor exposes `ready`, so a test can inspect a pending lazy body before readiness.
|
|
282
|
-
`waitFor(snapshot => predicate, { label, timeoutMs, pollIntervalMs })` wakes on inspection changes and also polls
|
|
283
|
-
external UI predicates; `notify()` wakes it after a controlled fixture update. The default deadline is 1000ms,
|
|
284
|
-
with a 10ms predicate poll. A `ScenarioTimeoutError` carries the data-only snapshot, bounded history and observed
|
|
285
|
-
conditions, feature phases, body loads, lane blockers and resource lease facts. It does not infer repository
|
|
286
|
-
or network causes. `getSnapshot()` and `history()` use that same observation model; history defaults to 64 snapshots,
|
|
287
|
-
activity to 256 records. Capacities accept integers from 1 to 10000. Do not replace predicates with a fixed number of ticks.
|
|
288
|
-
|
|
289
|
-
`close()` fences application admission synchronously, unmounts all registered screens and joins their cleanup with
|
|
290
|
-
physical application drain. Its deadline does not cancel cleanup: a later `close()` can await the same drain.
|
|
291
|
-
A readiness deadline likewise leaves the application available for inspection and explicit cleanup.
|
|
292
|
-
`ownership()` reports only registered runtime ownership, with `unknown` for missing, stale or truncated evidence;
|
|
293
|
-
a workspace stale snapshot is complete only after the scenario witnessed successful physical cleanup. This permits
|
|
294
|
-
a scoped zero-count assertion, without proving absence of arbitrary host, UI or GC leaks. Successful cleanup clears
|
|
295
|
-
application imports and internal renderer references. A failed cleanup promise can retain original errors and retry
|
|
296
|
-
capabilities; a caller that keeps `mounted.host` also keeps its own renderer result.
|
|
297
|
-
|
|
298
|
-
The inspection schemas are `opetope.devtools-graph/5` and `opetope.devtools-frame/5`, with optional
|
|
299
|
-
`opetope.runtime-activity/4` snapshots (D266, D275, D344, D358). Within one session,
|
|
300
|
-
a frame without `activity` preserves the previous activity; a full snapshot/reset without it clears that observation
|
|
301
|
-
(D216). Activity-bearing frames replace the previous activity in full.
|
|
302
|
-
Use matching runtime/devtools versions: previous graph/frame and activity revisions are rejected; weak-edge presence requires graph `/5` and frame `/4`. Activity identifies the execution,
|
|
303
|
-
actual feature generation, physical Calls, exact current lane blockers, and for every registered Resource its
|
|
304
|
-
`kind`, `lifetime`, `epoch`, `state`, `activity` with its `operation`, `leases`, `subscribers`, `pagination` and the
|
|
305
|
-
`epoch` of each physical load — there is no `attempt` and no numeric generation, because a Resource runs one logical
|
|
306
|
-
attempt and recovery belongs to the transport (D358). Host demand and UI models are unknown. `freshness`
|
|
307
|
-
and `truncated` distinguish a complete live view from a partial or detached one. A closed session is stale;
|
|
308
|
-
`closed: true` requires successful physical application drain. No control authority or product payload is added.
|
|
309
|
-
Activity output is bounded by record capacity. Snapshot collection still visits registered owners, executors and
|
|
310
|
-
resources, so capacity does not bound traversal cost. Collection stops once truncation is proven;
|
|
311
|
-
idle executors may still require traversal to establish completeness. Normal call dispatch allocates no diagnostic record with
|
|
312
|
-
observation disabled. Graph frames remain bounded by the existing ring capacity.
|
|
313
|
-
|
|
314
|
-
## Testing
|
|
315
|
-
|
|
316
|
-
`@opetope/react/testing` exports component fixtures and application scenarios, and re-exports
|
|
317
|
-
`@opetope/runtime/testing` — which in turn re-exports `@opetope/core/testing` — so a React test names one entry
|
|
318
|
-
(D285):
|
|
319
|
-
|
|
320
|
-
- `renderSlot(target, { contribution, models, props })` mounts the published target or one fixture contribution;
|
|
321
|
-
- `command(run)` produces an authentic `Call` for a model fixture, plus the contribution binding a mount reads;
|
|
322
|
-
- `runCall(target, input?, options?)` executes an existing authentic `Call` without mounting React (D261);
|
|
323
|
-
- `testReadable(initial)`, `testResourceEpoch()`, `openModel`, `settled`, `waitFor`, `yieldTurn` and `eventually`
|
|
324
|
-
come from the entries below, for the part of a test that has no UI in it — `testResourceEpoch()` is what a
|
|
325
|
-
component fixture writing a `ResourceState` by hand puts in its `epoch` (D362).
|
|
326
|
-
|
|
327
|
-
A test that renders nothing should import `@opetope/runtime/testing` directly: `renderSlot` and the binding half of
|
|
328
|
-
`command` are all that needs React here. The Call a fixture mints is the `command` of `@opetope/core/testing`
|
|
329
|
-
(D300), and this word is the one name of the re-export chain that does not pass through: the React `command`
|
|
330
|
-
shadows it with the stronger one.
|
|
331
|
-
|
|
332
|
-
The `renderSlot` harness returns `Slot` and `updateProps`: there are no roots and no fixtures for them, UI enters the
|
|
333
|
-
application as contributions (D85). It is not a test spelling of `<Slot>` — it mounts a fixture and takes a
|
|
334
|
-
`reporter`, because a component test has no feature to report to — and the packaged consumer of `ci:pack` mounts it
|
|
335
|
-
from the real archive (D301).
|
|
336
|
-
|
|
337
|
-
A test that previously mounted `useCommand` only to invoke a Call can use `runCall` directly. The result is the
|
|
338
|
-
Call's output, while a UI test still checks the `CommandOutcome` returned by the mounted consumer:
|
|
339
|
-
|
|
340
|
-
```ts
|
|
341
|
-
import assert from 'node:assert/strict';
|
|
342
|
-
import { defineModel } from '@opetope/core';
|
|
343
|
-
import type { Call } from '@opetope/core';
|
|
344
|
-
import { defineFeature, openFeature } from '@opetope/runtime';
|
|
345
|
-
import { runCall } from '@opetope/react/testing';
|
|
346
|
-
|
|
347
|
-
const Arithmetic = defineModel<{ readonly double: Call<number, number> }>('example.testing.model');
|
|
348
|
-
const arithmetic = defineFeature('example.testing.arithmetic', {
|
|
349
|
-
own: ({ model }) => ({
|
|
350
|
-
arithmetic: model(Arithmetic, context => ({
|
|
351
|
-
double: context.call((input: number) => input * 2),
|
|
352
|
-
})),
|
|
353
|
-
}),
|
|
354
|
-
exports: ({ own }) => ({ double: own.arithmetic.double }),
|
|
355
|
-
});
|
|
356
|
-
const instance = openFeature(arithmetic, { imports: {}, reporter: () => undefined });
|
|
357
|
-
try {
|
|
358
|
-
const ready = await instance.ready;
|
|
359
|
-
const result = await runCall(ready.exports.double, 3);
|
|
360
|
-
assert.equal(result, 6);
|
|
361
|
-
} finally {
|
|
362
|
-
await instance.close();
|
|
363
|
-
}
|
|
364
|
-
```
|
|
61
|
+
For a contributor checkout, use `npm run ci:type --workspace @opetope/react` and
|
|
62
|
+
`npm run ci:test --workspace @opetope/react` where provided. The root `npm run check` performs full acceptance;
|
|
63
|
+
[contributor commands](../runtime/docs/maintainers/contributing.md) describe the build and package checks.
|
|
365
64
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
owner, lane and retirement rules; the test closes the instance it opened, including after failed readiness.
|
|
369
|
-
A void Call can use `runCall(target)`. Options always occupy the third argument:
|
|
370
|
-
`runCall(target, input, { signal })`, or `runCall(voidTarget, undefined, { signal })`.
|
|
371
|
-
`RunCallOptions` contains only the optional `signal`; a pre-aborted signal rejects before the body runs.
|
|
65
|
+
<a id="hello-ui"></a>
|
|
66
|
+
[See Hello UI](../runtime/docs/reference/react.md#react-hello-ui).
|
|
372
67
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
Generic helpers may forward the target and its explicit input without losing their types.
|
|
68
|
+
<a id="selecting-data-and-commands"></a>
|
|
69
|
+
[See Selecting data and commands](../runtime/docs/reference/react.md).
|
|
376
70
|
|
|
377
|
-
|
|
71
|
+
<a id="application-host"></a>
|
|
72
|
+
[See Application host](../runtime/docs/reference/react.md).
|
|
378
73
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
| `@opetope/react` | `useModel`, `useCommand`, `useCommands`, `useReadable`, `useSelector`, `useResource`, `requiresModels`, `defineSlot`, `defineSwitchSlot`, `Slot`, `ContributionError`; types `SlotTarget`, `SwitchSlotTarget`, `SlotContribution`, `CommandHook`, `CommandOutcome`, `ResourceHook`, `PaginatedResourceHook`, `ResourcePaginationHook` |
|
|
382
|
-
| `@opetope/react/integration` | `FeatureBoundary`, `ContributionBoundary`, `useFeature`, `useFeatureRetry`, `FeatureBoundaryError` for the integration layer; types `FeatureBoundaryProps`, `ContributionBoundaryProps`, `ContributionErrorContent`, `ContributionFailure`, `FeatureDemandSource`, `FeatureDemandState`, `FeatureDemandResult`, `FeatureDemandLease` |
|
|
383
|
-
| `@opetope/react/testing` | `renderSlot`, `command`, `createScenario`, `ScenarioTimeoutError` and fixture/scenario types, plus everything `@opetope/runtime/testing` and `@opetope/core/testing` publish; tests only |
|
|
74
|
+
<a id="feature-demand-boundary"></a>
|
|
75
|
+
[See Feature demand boundary](../runtime/docs/reference/react.md).
|
|
384
76
|
|
|
385
|
-
|
|
77
|
+
<a id="contributions"></a>
|
|
78
|
+
[See Contributions](../runtime/docs/reference/react.md).
|
|
386
79
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
`ui/models/data/contracts` through a local barrel of a feature.
|
|
80
|
+
<a id="scenario-tests-and-physical-activity"></a>
|
|
81
|
+
[See Scenario tests and physical activity](../runtime/docs/reference/react.md).
|
|
390
82
|
|
|
391
|
-
|
|
83
|
+
<a id="testing"></a>
|
|
84
|
+
[See Testing](../runtime/docs/reference/react.md).
|
|
392
85
|
|
|
393
|
-
-
|
|
394
|
-
|
|
395
|
-
- an abandoned concurrent render holds no frame;
|
|
396
|
-
- `useCommand` does not subscribe to the invoker: the observable state of a call lies in a `Readable` of the model;
|
|
397
|
-
- the `cancelled` outcome does not move `outcome`, and it is only the `CallError` codes `cancelled`
|
|
398
|
-
and `closed`; a call to a weak port with no provider (`unavailable`) and a rejected publication
|
|
399
|
-
(`publication-rejected`) arrive as the `failed` outcome and settle in `outcome` (D138, D292);
|
|
400
|
-
- a contribution that fails to render or to commit is contained by its own mount and reported to its feature;
|
|
401
|
-
- `useSelector` reads its `equals` where the selected value is known, so a named non-generic comparator compiles
|
|
402
|
-
beside a selection whose parameter is inferred, as it does in Core (D348);
|
|
403
|
-
- `useSelector` keeps the selected reference when something else changed; its identity case is `useReadable` and its
|
|
404
|
-
model case is a `useModel` selection, so it is proven by the memory gate and the packaged consumer rather than by a
|
|
405
|
-
specification example, and it is a candidate to leave this entry until a second application is measured (D301);
|
|
406
|
-
- closing and unmounting synchronously fence new calls and release the references of the frame.
|
|
86
|
+
<a id="word-map-and-entries"></a>
|
|
87
|
+
[See Word map and entries](../runtime/docs/reference/react.md).
|
|
407
88
|
|
|
408
|
-
|
|
89
|
+
<a id="laws"></a>
|
|
90
|
+
[See Laws](../runtime/docs/reference/react.md).
|
|
409
91
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
executed by an HTTP import smoke test with a local bundle of the peers; historical browser builds are checked only by an external farm.
|
|
92
|
+
<a id="compatibility-contract"></a>
|
|
93
|
+
[See Compatibility contract](../runtime/docs/reference/react.md).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CommandInputArgs, CallRunOptions } from '@opetope/core/internal';
|
|
2
2
|
import type { CommandOutcome } from './command.js';
|
|
3
3
|
type CommandRunOptions<Output> = Readonly<{
|
|
4
4
|
onFailure?: (error: unknown) => PromiseLike<void> | void;
|
|
@@ -7,15 +7,15 @@ type CommandRunOptions<Output> = Readonly<{
|
|
|
7
7
|
}>;
|
|
8
8
|
type CommandRun<Input, Output> = {
|
|
9
9
|
(input: Input, options?: CommandRunOptions<Output>): Promise<CommandOutcome<Output>>;
|
|
10
|
-
(...args: [...
|
|
10
|
+
(...args: [...CommandInputArgs<Input>, options?: CommandRunOptions<Output>]): Promise<CommandOutcome<Output>>;
|
|
11
11
|
};
|
|
12
12
|
type CommandInvoker = Readonly<{
|
|
13
13
|
run: (input: never, options?: CallRunOptions<unknown>) => Promise<unknown>;
|
|
14
14
|
}>;
|
|
15
15
|
type CommandNotification<Output> = (inFlight: boolean, outcome?: CommandOutcome<Output>) => void;
|
|
16
16
|
/**
|
|
17
|
-
* D203: the consumer keeps a status, not a schedule. Every `run` reaches the
|
|
18
|
-
*
|
|
17
|
+
* D203: the consumer keeps a status, not a schedule. Every `run` reaches the command, and what the command was
|
|
18
|
+
* declared with — `concurrency`, `memoize`, `dedupe` — decides what happens to it. What stays
|
|
19
19
|
* local is what only this consumer knows: whether it is mounted, its own input signal, and the outcome it shows.
|
|
20
20
|
*/
|
|
21
21
|
declare class CommandHookController<Input, Output> {
|
package/dist/command-hook.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Command } from '@opetope/core';
|
|
2
2
|
import type { CommandOutcome } from './command.js';
|
|
3
3
|
import { CommandHookController } from './command-hook-controller.js';
|
|
4
4
|
import type { CommandRun } from './command-hook-controller.js';
|
|
5
5
|
/** D292: a cancellation is not an answer of the product, so it never settles into the outcome this hook shows. */
|
|
6
6
|
type SettledOutcome<Output> = Exclude<CommandOutcome<Output>, {
|
|
7
|
-
readonly
|
|
7
|
+
readonly kind: 'cancelled';
|
|
8
8
|
}>;
|
|
9
9
|
interface CommandHookStatus<Output> {
|
|
10
10
|
readonly inFlight: boolean;
|
|
@@ -19,6 +19,6 @@ type CommandHook<Input, Output> = CommandHookStatus<Output> & {
|
|
|
19
19
|
* it, and the synchronisation of a slot that was already busy when this mount arrived.
|
|
20
20
|
*/
|
|
21
21
|
declare function useCommandStatus<Input, Output>(currentSlot: CommandHookController<Input, Output>): CommandHook<Input, Output>;
|
|
22
|
-
declare function useCommand<Input, Output>(command:
|
|
22
|
+
declare function useCommand<Input, Output>(command: Command<Input, Output>): CommandHook<Input, Output>;
|
|
23
23
|
export { useCommand, useCommandStatus };
|
|
24
24
|
export type { CommandHook };
|
package/dist/command.d.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
type CommandBinding<Input, Output> =
|
|
1
|
+
import type { Command } from '@opetope/core';
|
|
2
|
+
type CommandBinding<Input, Output> = Command<Input, Output>;
|
|
3
3
|
type CommandOutcome<Output> = {
|
|
4
4
|
readonly error: unknown;
|
|
5
|
-
readonly
|
|
5
|
+
readonly kind: 'failed';
|
|
6
6
|
} | {
|
|
7
7
|
readonly reason: unknown;
|
|
8
|
-
readonly
|
|
8
|
+
readonly kind: 'cancelled';
|
|
9
9
|
} | {
|
|
10
|
-
readonly
|
|
10
|
+
readonly kind: 'ok';
|
|
11
11
|
readonly value: Output;
|
|
12
12
|
};
|
|
13
13
|
type AnyCommandBinding = CommandBinding<never, unknown>;
|
|
14
14
|
type CommandBindingDefinition<Input, Output> = {
|
|
15
|
-
readonly target:
|
|
15
|
+
readonly target: Command<Input, Output>;
|
|
16
16
|
};
|
|
17
|
-
declare function bindCommand<Input, Output>(target:
|
|
18
|
-
declare function getCommandBindingDefinition<Input, Output>(binding:
|
|
17
|
+
declare function bindCommand<Input, Output>(target: Command<Input, Output>): CommandBinding<Input, Output>;
|
|
18
|
+
declare function getCommandBindingDefinition<Input, Output>(binding: Command<Input, Output>): CommandBindingDefinition<Input, Output>;
|
|
19
19
|
declare function isCommandBinding(value: unknown): value is AnyCommandBinding;
|
|
20
20
|
export { bindCommand, getCommandBindingDefinition, isCommandBinding };
|
|
21
21
|
export type { CommandOutcome };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{jsx as m}from"react/jsx-runtime";import{useContext as T,createContext as V,useCallback as S,useRef as O,useMemo as p,useSyncExternalStore as k,useEffect as D,useInsertionEffect as A,useLayoutEffect as w,memo as N,useState as W}from"react";import{reportDetachedError as C,compatibleAbortReason as K,isCallTarget as _,isModel as G,isReadable as j,createLifetimeController as H,createCallController as J,getContributionOwner as Q,createState as U,createAggregateError as X}from"@opetope/core/internal";import{isResourceValue as Y,reportRuntimeFailure as Z,requireContributionModelPlans as ee,openContributionModels as te}from"@opetope/runtime/internal";import{C as u,u as ne,b as re}from"./contribution-isolation-Bzfbt4iM.js";import{CommandError as ie}from"@opetope/core";const x=V(void 0);function oe(){const t=T(x);if(t===void 0)throw new u("missing","Opetope hook was called outside a contribution mount.");return t}function y(t){return Y(t)}function se(t,e){return new Promise((n,r)=>{const i=()=>{r(new ie("cancelled","Resource operation wait was cancelled.",K(e)))};if(e.aborted){i();return}e.addEventListener("abort",i,{once:!0}),t.then(o=>{e.removeEventListener("abort",i),n(o)},o=>{e.removeEventListener("abort",i),r(o)})})}function ae(t,e){var r;const n=(r=e==null?void 0:e.onSuccess)==null?void 0:r.call(e,t);return n!==void 0&&Promise.resolve(n).catch(C),t}function v(t){return{run:(e,n)=>{let r;try{r=Promise.resolve(t())}catch(o){return Promise.reject(o)}const i=n==null?void 0:n.signal;return(i===void 0?r:se(r,i)).then(o=>ae(o,n))}}}const ce=Object.freeze({kind:"skipped",reason:"resource-not-ready"});function de(t){const e=t.pagination;if(!(typeof e!="object"||e===null))return e}function E(t){return{loadNext:v(()=>{const e=t.pagination;return e===void 0?Promise.resolve(ce):e.loadNext()}),refresh:v(()=>t.refresh()),reset:v(()=>t.reset()),retry:v(()=>t.retry())}}function ue(t,e){const n=e.get(t);if(n!==void 0)return n;const r=E(t);return e.set(t,r),r}const le=t=>t;function F(t,e,n){const r=e,i=(n==null?void 0:n.equals)??Object.is,o=S(d=>t.subscribe(d),[t]),c=O(void 0);let s=c.current;s===void 0&&(s={hasValue:!1,value:void 0},c.current=s);const a=p(()=>{let d=!1,h,f;return()=>{const b=t.getSnapshot();if(d&&Object.is(h,b))return f;const g=r(b);if(!d){let R=g;return s.hasValue&&i(s.value,g)&&(R=s.value),d=!0,h=b,f=R,f}return i(f,g)?(h=b,f):(h=b,f=g,f)}},[s,i,t,r]),l=k(o,a,a);return D(()=>{s.hasValue=!0,s.value=l},[s,l]),l}function z(t){return F(t,le)}const M=new WeakMap;function I(t){if(arguments.length>1)throw new u("binding-invalid","Command follows the target it binds; binding options are forbidden.");if(!_(t))throw new u("binding-invalid","Command target is not authentic.");return M.set(t,{target:t}),t}function fe(t){const e=M.get(t);if(e===void 0)throw new u("binding-invalid","Command binding is not authentic.");return e}function $(t){return typeof t=="object"&&t!==null&&M.has(t)}const he=Symbol("opetope.model-binding"),me=Symbol("opetope.model-binding-identity"),be=Object.freeze({}),L=new WeakMap;function pe(t,e,n){if(typeof n!="string")throw new u("binding-invalid",`Model ${t.id} accepts only string fields.`);const r=Object.getOwnPropertyDescriptor(e,n);if((r==null?void 0:r.enumerable)!==!0||!("value"in r))throw new u("binding-invalid",`Model ${t.id}.${n} requires an enumerable data binding.`);return r.value}function ge(t,e){const n=[],r=Reflect.ownKeys(e);if(r.length===0)throw new u("binding-invalid",`Model ${t.id} requires at least one field.`);for(const i of r){const o=pe(t,e,i);if(!(j(o)||y(o)||typeof o=="function")){if(!$(o))throw new u("binding-invalid",`Model ${t.id}.${String(i)} requires a Readable, a Resource or a bound command.`);n.push(o)}}return Object.freeze(n)}function q(t,e){if(!G(t))throw new u("binding-invalid","Model declaration is not authentic.");if(typeof e!="object"||e===null)throw new u("binding-invalid",`Model ${t.id} requires a record binding.`);const n=ge(t,e),r=Object.freeze({[he]:be,[me]:!0});return L.set(r,{commands:n,declaration:t,model:e}),r}function we(t){const e=L.get(t);if(e===void 0)throw new u("binding-invalid","Model binding is not authentic.");return e}let Ce=0;function ve(t){const e=new Map,n=[],r=new Set;for(const i of t){const o=we(i);if(e.has(o.declaration))throw new u("duplicate",`Model ${o.declaration.id} is bound twice in one mount.`);e.set(o.declaration,o.model);for(const c of o.commands)r.has(c)||(r.add(c),n.push(c))}return{commands:Object.freeze(n),frame:{commandRecords:new Map,models:e}}}class ye{frame;authority;boundCommands;closeTicket=0;isActive;lifetime;onClose;released=!1;rendererAttached=!1;renderListeners=new Set;state="detached";constructor(e,n){const r=++Ce;this.lifetime=H({id:`contribution.${String(r)}`}),this.isActive=n==null?void 0:n.isActive,this.onClose=n==null?void 0:n.onClose;const{commands:i,frame:o}=ve(e);this.frame=o,this.boundCommands=i,this.buildCommandRecords(r)}attach(){if(this.state==="closing")return this.closeTicket+=1,this.state="active",this.rendererAttached=!0,!0;if(this.state==="closed")return!1;if(this.state==="active")throw new u("duplicate","One contribution mount cannot mount twice.");const e=this.createAuthority(this.lifetime.open());try{for(const n of this.frame.commandRecords.values())n.controller.activate()}catch(n){throw this.authority=e,this.state="active",this.close(),n}return this.authority=e,this.rendererAttached=!0,this.state="active",!0}close(){if(this.state==="closed")return;this.state="closed",this.closeTicket+=1,this.lifetime.close(),this.authority=void 0;for(const n of this.frame.commandRecords.values())n.controller.close();const e=this.onClose;this.onClose=void 0,e==null||e(),this.publishRenderable(),this.rendererAttached||this.releaseFrame()}detach(){if(!this.rendererAttached)return;if(this.rendererAttached=!1,this.state==="closed"){this.releaseFrame();return}if(this.state!=="active")return;this.state="closing";const e=++this.closeTicket;queueMicrotask(()=>{this.state==="closing"&&e===this.closeTicket&&this.close()})}diagnostics(){const e=[...this.frame.commandRecords.values()].map(n=>n.controller.diagnostics());return Object.freeze({active:this.state==="active",calls:Object.freeze(e),commandRecords:this.frame.commandRecords.size,models:this.frame.models.size})}getRenderableSnapshot=()=>this.state!=="closed"&&(this.isActive===void 0||this.isActive());releaseClosedFrame(){this.state==="closed"&&this.releaseFrame()}subscribeRenderable=e=>{if(this.state==="closed")return()=>{};const n={listener:e};this.renderListeners.add(n);let r=!0;return()=>{r&&(r=!1,this.renderListeners.delete(n))}};buildCommandRecords(e){let n=0;for(const r of this.boundCommands){fe(r);const i=J(r,{captureAuthority:this.captureAuthority,id:`contributionCommand.${String(e)}.${String(++n)}`,scheduling:{policy:"parallel"}});this.frame.commandRecords.set(r,{controller:i,invoker:Object.freeze({run:i.run})})}}captureAuthority=()=>{const e=this.authority;if(this.state!=="active"||e===void 0)throw new u("inactive","Contribution mount is not active.");return e.assertCurrent(),e};createAuthority(e){return Object.freeze({assertCurrent:()=>{if(this.state!=="active")throw new u("inactive","Contribution mount is not active.");e.assertCurrent()},isCurrent:()=>this.state==="active"&&e.isCurrent()})}publishRenderable(){for(const{listener:e}of[...this.renderListeners])try{e()}catch(n){C(n)}}releaseFrame(){this.released||(this.released=!0,this.frame.commandRecords.clear(),this.frame.models.clear(),this.boundCommands=[],this.renderListeners.clear())}}function Me({children:t,mount:e}){const n=k(e.subscribeRenderable,e.getRenderableSnapshot,e.getRenderableSnapshot);return A(()=>{if(e.attach())return()=>e.detach()},[e]),w(()=>{n||e.releaseClosedFrame()},[e,n]),n?m(x,{value:e.frame,children:t}):null}function B(t){const e=t.bundle;t.bundle=void 0,e==null||e.frame.close()}function Re(t,e){if(typeof e!="object"||e===null)return q(t,e);const n=i=>j(i)||y(i)||typeof i=="function"||$(i),r=Object.entries(e).map(([i,o])=>[i,n(o)?o:I(o)]);return q(t,Object.fromEntries(r))}function Se(t){return Q(t)}const Oe={models:[]};function P(t,e){try{t()}catch(n){throw X([e,n],"Contribution mount setup and cleanup failed.",{cause:e})}throw e}function ke(t,e,n,r){const i=U(e),{models:o,reporter:c}=r??Oe;try{const s=ee(t.models),a=s.length===0?void 0:te(s,i,c);return{bundle:a,models:[...o,...(a==null?void 0:a.models)??[],...n],props:i}}catch(s){return P(i.close,s)}}function Ae(t,e,n,r){const{bundle:i,models:o,props:c}=ke(t,e,n,r);let s=!1;const a=()=>{if(!s){s=!0;try{i==null||i.close().catch(C)}finally{c.close()}}};try{return{frame:new ye(o.map(([d,h])=>Re(d,h)),{isActive:()=>!s,onClose:a}),isOpen:()=>!s,props:c}}catch(l){return P(a,l)}}function je(t,e){return m(t,{...e})}function xe({bundle:t,contribution:e}){const n=z(t.props),r=p(()=>e.props===void 0?n:e.props(n),[n,e]);return m(Me,{mount:t.frame,children:je(e.Component,r)})}function Ee({authority:t,contribution:e,fixtures:n,slotProps:r}){const i=p(()=>({authority:t,bundle:void 0,contribution:e,fixtures:n,retired:!1}),[t,e,n]),[o,c]=W(void 0),s=(o==null?void 0:o.owner)===i?o.bundle:void 0,a=O(r);return w(()=>{a.current=r}),A(()=>()=>{i.retired=!0,queueMicrotask(()=>{try{B(i)}catch(l){C(l)}})},[i]),w(()=>{const l=i.bundle??(i.bundle=Ae(i.contribution,a.current,i.fixtures??[],i.authority));return c(d=>(d==null?void 0:d.owner)===i?d:{bundle:l,owner:i}),()=>{i.retired&&B(i)}},[i]),w(()=>{(s==null?void 0:s.isOpen())===!0&&s.props.set(r)},[s,r]),s===void 0?null:m(xe,{bundle:s,contribution:e})}function Fe(t){const{authority:e,contribution:n,contributionId:r,fixtures:i,targetId:o}=t,c=ne(),s=p(()=>({authority:e,contribution:n,fixtures:i}),[e,n,i]),a=e==null?void 0:e.feature,l=p(()=>({contribution:r,feature:a,target:o}),[r,a,o]),d=e==null?void 0:e.reporter,h=S(f=>Z(f,d),[d]);return m(re,{content:c,identity:s,report:h,source:l,children:m(Ee,{...t})})}const ze=N(Fe);export{ze as M,z as a,Se as b,E as c,F as d,I as e,y as i,de as p,ue as r,oe as u};
|
|
2
|
+
//# sourceMappingURL=contribution-frame-CCKnOxZR.js.map
|