@opetope/react 0.12.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 +144 -0
- package/README.md +38 -397
- package/package.json +5 -6
- package/README.ru.md +0 -401
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,149 @@
|
|
|
1
1
|
# @opetope/react
|
|
2
2
|
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 997137b: **Documentation only: four statements that had drifted from the tree are corrected, and the copies they drifted
|
|
8
|
+
out of are replaced by the one place each law is written (D460).** No source file changes, no public name moves,
|
|
9
|
+
no gate budget moves and no compiled example is edited. The root README gains a map, «Where each contract is
|
|
10
|
+
written once», naming for every subject the one normative section, the worked examples over it and nothing else.
|
|
11
|
+
|
|
12
|
+
- **The runtime word map promised two Resource names and the entry publishes nineteen.** `Resource` and
|
|
13
|
+
`ResourceState` were described as «the two words this entry publishes for a materialization», with the rest of
|
|
14
|
+
the family «named from `@opetope/core`». The entry re-exports the family whole — every `Resource*` word plus
|
|
15
|
+
`EventDelivery`, `LiveDelivery`, `PaginationOutcome` and `PaginationState` — and the comment above that block
|
|
16
|
+
names the decision that made it so. The row now says what the file does, which is what lets a feature author
|
|
17
|
+
write one import line for a declaration and for the `ResourceData<Data>` its `apply.change` annotates.
|
|
18
|
+
- **`@opetope/core` said it stays `private`, and its manifest carries no such field.** The manifest has
|
|
19
|
+
`publishConfig.access: public` and no `private`. The sentence keeps what was true — the API is experimental
|
|
20
|
+
while the design is under review — and says what that costs an upgrader: an alpha removes a superseded name
|
|
21
|
+
instead of keeping it beside its replacement, so an upgrade is read through the release notes. It makes no claim
|
|
22
|
+
about what is or is not on npm today.
|
|
23
|
+
- **Three pages still taught the pre-`LostWrite` law.** The runtime README said a dropped write is lost «in
|
|
24
|
+
silence rather than thrown at the caller or recorded by the reporter»; the primitives reference said «nothing is
|
|
25
|
+
reported»; and the spec's own model-layer section said «dropped without a reporter record». The law changed two
|
|
26
|
+
decisions ago and the normative sections that state it — §2.11 and §4 «Failures after abort» — were already
|
|
27
|
+
right: the first write an authority loses after ending inside a write into a cell its source reads, its own or
|
|
28
|
+
one of a command it invoked, reaches the reporter as `LostWrite`, once per run, and every other drop is silent.
|
|
29
|
+
All three now say that and defer to §4 for which is which. `update` is still `void`: nothing here promises a
|
|
30
|
+
throw at the caller. The primitives tables that listed a dropped write as always silent are corrected with them.
|
|
31
|
+
- **Two code tuples had already lost codes, and both are replaced by a link.** The authoring guide listed the
|
|
32
|
+
codes of every error class and was missing two of the six of `ContributionError`; the core README's boundary
|
|
33
|
+
list was missing the `CancellationError` its own word map names one paragraph above. Which class carries which
|
|
34
|
+
code, and which codes are cancellations rather than product answers, is the spec's «Errors»; which class is
|
|
35
|
+
thrown by whom is the how-it-works error table. The React README's outcome bullet, a third copy of the same law
|
|
36
|
+
plus the spec's «Command outcomes», goes the same way.
|
|
37
|
+
- **The rule that looked stale is not, and its reason was.** The authoring guide requires a result type on a model
|
|
38
|
+
factory, because an inline arrow handed straight to `openModel(Decl, ctx => …)` is not instantiated when its
|
|
39
|
+
body is checked. A probe over the current sources confirms the rule and refutes the explanation: a body that
|
|
40
|
+
names no input is inferred as `Command<void, void>`, taking the `void` default instead of the `never` the guide
|
|
41
|
+
claimed — so the declaration's input never arrives, a body that reads `input` is refused on the read with the
|
|
42
|
+
four-position instruction, and a body that reads nothing is refused against the record the factory returns. The
|
|
43
|
+
reason is rewritten to that; the rule stands.
|
|
44
|
+
- **The repetitions removed, and what was left in their place.** The primitives reference's shared-laws section
|
|
45
|
+
stated the fence-and-drain, writer, cancellation and reporter laws in full while declaring the spec normative
|
|
46
|
+
for all four; it now states what a primitive owes each law and links. The runtime README restated the reason
|
|
47
|
+
behind `opetope/no-snapshot-in-update`, which that rule's own README writes out, and restated the model
|
|
48
|
+
context's lanes and its `invoke`-from-an-effect verbatim from the core README, which declares them. Each task
|
|
49
|
+
page keeps its short explanation and its warnings.
|
|
50
|
+
|
|
51
|
+
- 8aec982: **A command's one waiting place is written as the record that names it: `concurrency: { pending: 'latest' }`
|
|
52
|
+
(D459).** The scalar `concurrency: 'latest'` named the input that wins and never named what it wins — «the latest»
|
|
53
|
+
reads as a place in a queue, as the body already running and as a cached answer, and only the first was true. The
|
|
54
|
+
shared form of the same fact never had that gap: `{ lane, pending: 'latest' }` says the place out loud, with the
|
|
55
|
+
`pending` an event and a live Resource already write for their own one waiting place. One fact was being written in
|
|
56
|
+
two vocabularies, and the shorter one left out the part that mattered. The word is removed rather than kept beside
|
|
57
|
+
the record, which makes this a breaking change to the authoring surface of a model command, of a model selection
|
|
58
|
+
and of a feature command.
|
|
59
|
+
|
|
60
|
+
- **Migrate by writing the place.** `concurrency: 'latest'` becomes `concurrency: { pending: 'latest' }` in
|
|
61
|
+
`ctx.command`, `ctx.select` and `own.command`. `concurrency: { lane, pending: 'latest' }` is unchanged: the lane
|
|
62
|
+
is the one parameter this record takes, and it is optional now instead of required. Nothing else about the option
|
|
63
|
+
moves — the same record, the same siblings, the same `dedupe` prohibition beside it.
|
|
64
|
+
- **No law moved with the spelling.** While call 1 runs, calls 2 and 3 take one waiting place: the body of 2 never
|
|
65
|
+
starts, its wait ends as `CommandError('cancelled', 'Command <id> replaced a waiting input.')`, and 3 runs when 1
|
|
66
|
+
finishes. A newer request never reaches the call in flight. The regression that holds this reads `signal.aborted`
|
|
67
|
+
inside the running body — after both newer requests were admitted and before the body returns — rather than
|
|
68
|
+
counting abort events, because the abort that follows a call's own end belongs to its cleanup and says nothing
|
|
69
|
+
about the policy. It was proved by substitution: with the kernel changed to cancel whatever is in flight, the
|
|
70
|
+
test fails on that line.
|
|
71
|
+
- **The retired word is answered with its replacement, not with a list of everything else.** The runtime throws
|
|
72
|
+
`Model command concurrency latest is now a record: write { pending: 'latest' }, or { lane, pending: 'latest' }.`
|
|
73
|
+
— and `Model select`, `Feature command` the same. Two neighbouring messages move with it: the choice sentence
|
|
74
|
+
loses the word — `concurrency must be queue, parallel, a lane of this surface or a record.` — and the record's own
|
|
75
|
+
refusal now says the lane is optional. A record that names `lane` and hands it `undefined` is still refused, by
|
|
76
|
+
the type under `exactOptionalPropertyTypes` and by the runtime, which reads the shape of the record by its keys.
|
|
77
|
+
- **The compiler names the replacement too, because the literal stays in the union.** The options record of a
|
|
78
|
+
command gained a third member — the retired word beside a key an author cannot write — so a refusal arrives as a
|
|
79
|
+
missing property carrying the instruction. Without it the union held no string at all, every word written there
|
|
80
|
+
widened to `string`, and one sentence answered a typo and a retired word alike. What the two now print, the same
|
|
81
|
+
on a model and on a feature: `{ concurrency: 'quee' }` is a `TS2820` that names the word and suggests `'queue'`,
|
|
82
|
+
which is better than it was before this change, and `{ concurrency: 'latest' }` is a `TS2345` whose elaboration
|
|
83
|
+
carries «`concurrency: "latest"` is now a record: write `{ pending: "latest" }`, or `{ lane, pending: "latest" }`».
|
|
84
|
+
A selection is the one surface where the compiler names the word written and not its replacement — its options
|
|
85
|
+
record is not a union — and there the runtime and the lint rule carry it.
|
|
86
|
+
- **`opetope/no-retired-vocabulary` reports it.** `concurrency: 'latest'` is a retired _value_ of a live key, the
|
|
87
|
+
shape `overflow: 'reject'` already had, so the rule now reads the values of a command's options record as well as
|
|
88
|
+
its keys and reports on the word that was written. A project that pins the rule's output by its own fixtures gains
|
|
89
|
+
one row.
|
|
90
|
+
- **What the gates read.** Instantiations are unchanged — 11 756, 87 710 and 160 268 against ceilings of 12 200,
|
|
91
|
+
99 062 and 181 237 — because a scalar became a record that already existed. The tightest size row, the headless
|
|
92
|
+
session consumer of `@opetope/devtools`, stays at 6.95 kB of 7.1 kB. The two runtime rows carry the new branch and
|
|
93
|
+
its message: the application graph consumer moves 48.56 → 48.66 kB of 49 kB and the public feature consumer
|
|
94
|
+
39.88 → 39.92 kB of 41 kB, measured by rebuilding the validator both ways. No budget moves.
|
|
95
|
+
|
|
96
|
+
- 4e65393: **The published archive of every package loses `README.ru.md`, and `@opetope/runtime` loses the Russian pages of
|
|
97
|
+
its `docs/` as well (D456).** The Russian half of the documentation is removed: nineteen `*.ru.md` files,
|
|
98
|
+
**2 027 997 bytes**, which is 39.0 % of what `ci:docs` counts as a document and 35.1 % of every `*.md` in the tree.
|
|
99
|
+
Documentation is written once, in English, from here on. No public name, no `exports` entry and no subpath moves —
|
|
100
|
+
a `.ru.md` was never a resolvable specifier, only a file read by its path — and no line of shipped JavaScript
|
|
101
|
+
changes, which is why this is a patch. The composition of what is published does change, and this is the line that
|
|
102
|
+
says so.
|
|
103
|
+
|
|
104
|
+
- **What goes.** `CONTRIBUTING.ru.md`, `README.ru.md`, the README of the minimal React example, the nine guides of
|
|
105
|
+
`docs/` — agent guide, cookbook, devtools, how-it-works, primitives, releases, both migration guides and the
|
|
106
|
+
spec — and the `README.ru.md` of all seven packages. `docs/decisions.md` stays Russian and keeps no pair, as it
|
|
107
|
+
never had one. The nine copies under `packages/runtime/docs/` are generated by packaging and leave on their own.
|
|
108
|
+
- **The rule that required a pair is cancelled, with its gate.** `CLAUDE.md` said «README and user guides have
|
|
109
|
+
English/Russian pairs, updated together» with four exceptions; it now says a document is written once, in
|
|
110
|
+
English, and gets no second copy. `ci:docs` no longer looks for a `*.ru.md` beside a document and no longer
|
|
111
|
+
compares the heading skeleton of a pair, and its report reads «links and example section order passed».
|
|
112
|
+
- **One gate is lost outright, and nothing replaces it.** The fixture generator held every English example against
|
|
113
|
+
its Russian twin as the same program — syntax without comments and without JSX text — which is how D441 caught a
|
|
114
|
+
real drift in a spec §2 block, a string literal that differed in one copy. The defect class that stops being
|
|
115
|
+
caught is a silent edit to a detail of an example that still compiles: a string literal, a number, the order of
|
|
116
|
+
two arguments of the same type. `ci:type` accepts any literal of the right type, `ci:docs` reads section order
|
|
117
|
+
and links rather than values, `ci:asserted-refusals` holds pragmas, and the generator's own `--check` compares a
|
|
118
|
+
fixture with the very document it was lifted from, so one `fixtures:generate` makes any such edit the new
|
|
119
|
+
baseline. The twin was the only place in this repository where one program was written twice and the two
|
|
120
|
+
writings were compared. It also never knew which copy was right — in the one case it fired, the wrong copy was
|
|
121
|
+
the Russian one — and it protected above all the copy that nothing compiled.
|
|
122
|
+
- **Where the archive change is written down, so it cannot drift from its gate.** The composition of an archive is
|
|
123
|
+
stated in six places and all six are rewritten: the `files` of the seven manifests; the required-file list and
|
|
124
|
+
the archive-member allowlist of the pack archive check; the fixture of that check's own unit test; the
|
|
125
|
+
allowlists of the two real pack smoke tests; and what packaging demands of each package before it packs. The
|
|
126
|
+
requirement that `@opetope/runtime` ship `docs/spec.ru.md` is gone; `docs/spec.md` is required exactly as before.
|
|
127
|
+
- **The numbers the gates print.** `ci:docs` reads 28 documents instead of 46 — nineteen leave and this changeset
|
|
128
|
+
is itself a document — with 4 pending changesets instead of 3 and 424 decision identifiers instead of 423. Its
|
|
129
|
+
feature examples halve, 64 to 32: that gate lints every fenced example that declares a feature, and it was
|
|
130
|
+
linting both copies of each one. Packaging prepares 10 documents instead of 19, and the merge-marker scan reads
|
|
131
|
+
1101 text files instead of 1117 — seventeen of the nineteen removed files live under a scanned root, and this
|
|
132
|
+
changeset is one file back. The document examples do not move at all: 38 spec §2 blocks and 132 blocks of 11
|
|
133
|
+
other documents, 2 fragments, 14 asserted refusals, exactly as before, because only the English block was ever
|
|
134
|
+
lifted into a fixture.
|
|
135
|
+
|
|
136
|
+
- Updated dependencies [997137b]
|
|
137
|
+
- Updated dependencies [ba75469]
|
|
138
|
+
- Updated dependencies [d09568e]
|
|
139
|
+
- Updated dependencies [8aa4d85]
|
|
140
|
+
- Updated dependencies [8aec982]
|
|
141
|
+
- Updated dependencies [be0613e]
|
|
142
|
+
- Updated dependencies [4e65393]
|
|
143
|
+
- Updated dependencies [e12b0b6]
|
|
144
|
+
- @opetope/core@0.12.1
|
|
145
|
+
- @opetope/runtime@0.12.1
|
|
146
|
+
|
|
3
147
|
## 0.12.0
|
|
4
148
|
|
|
5
149
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
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';
|
|
@@ -49,404 +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?.kind === '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 Command 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 command, and the order the command was declared with decides
|
|
74
|
-
what happens to it (D203). For an absolute value setter the model declares `concurrency: 'latest'` in
|
|
75
|
-
`context.command`, and the newest input then replaces the waiting one there (D185, D387). What the consumer keeps
|
|
76
|
-
is its own: an already-aborted input signal is refused as `cancelled` without reaching the command, a run of an
|
|
77
|
-
unmounted consumer is refused the same way, `inFlight` is true while any run this consumer started is unsettled,
|
|
78
|
-
and every run carries its own callbacks and signal.
|
|
79
|
-
|
|
80
|
-
To share equivalent pending or running work, the model declares `dedupe: true` or `dedupe: input => key`
|
|
81
|
-
on `context.command`. Sharing keeps the first input and each caller's independent cancellation; neither form may
|
|
82
|
-
be combined with `latest` (D277). These are Command options, not hook options.
|
|
83
|
-
|
|
84
|
-
For several commands a component writes `useCommand` per command, or one `useModel` selection, which already
|
|
85
|
-
answers ready hooks. `useCommands` is gone: a record over the one `CommandHook` was sugar that saved hook calls,
|
|
86
|
-
not a second mechanism, and the set that is really wanted is a selection in the model, where `ctx.select` gives it
|
|
87
|
-
an owner and a lifetime (D388).
|
|
88
|
-
|
|
89
|
-
Two aliases of one Command have independent local statuses, just like two `useCommand` consumers; they use the same
|
|
90
|
-
Command concurrency and mount command record (D197, D203). A selection introduces no shared busy state, queue or
|
|
91
|
-
transaction; controls that overwrite one value still need one semantic command.
|
|
92
|
-
|
|
93
|
-
## Selecting data and commands
|
|
94
|
-
|
|
95
|
-
`useModel(Declaration, (model, { read }) => ({ ... }))` combines explicit data selection and command consumers (D205, D214).
|
|
96
|
-
|
|
97
|
-
A model selection can use a named interface without an index signature (D217). Its result is a flat data record;
|
|
98
|
-
arrays, functions, constructors and built-in collection/date/promise objects are not selection records.
|
|
99
|
-
`read(source)` returns its snapshot; `read(source, project)` returns a projection. Authentic Commands selected as
|
|
100
|
-
record fields become the same `CommandHook` as `useCommand`, including independent alias statuses and stable `.run`.
|
|
101
|
-
A single selected command needs no nested hooks:
|
|
102
|
-
|
|
103
|
-
<!--example
|
|
104
|
-
import type { Command } from '@opetope/core';
|
|
105
|
-
import { defineModel } from '@opetope/core';
|
|
106
|
-
import { useModel } from '@opetope/react';
|
|
107
|
-
|
|
108
|
-
const AuthModel = defineModel<{ readonly logout: Command<void, void> }>('example.auth.model');
|
|
109
|
-
|
|
110
|
-
const LogOutButton = () => {
|
|
111
|
-
/* … */
|
|
112
|
-
};
|
|
113
|
-
-->
|
|
114
|
-
|
|
115
|
-
```tsx
|
|
116
|
-
const { logout } = useModel(AuthModel, auth => ({ logout: auth.logout }));
|
|
117
|
-
return (
|
|
118
|
-
<button disabled={logout.inFlight} onClick={() => void logout.run()}>
|
|
119
|
-
Log out
|
|
120
|
-
</button>
|
|
121
|
-
);
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
The one-argument form still returns the granted model; individual hooks remain available.
|
|
125
|
-
|
|
126
|
-
Only explicitly read sources are subscribed, once per distinct Readable in the selection. Returned data fields are
|
|
127
|
-
compared with `Object.is`; select scalar fields, spread a projected record into the selection, or return stable
|
|
128
|
-
references. A newly allocated nested object is a changed field. The callback is pure: no hooks, commands or side effects.
|
|
129
|
-
A read or selector error reaches the nearest React error boundary. Changing the selected sources or command keys
|
|
130
|
-
changes their subscriptions/consumers at commit; an abandoned render cannot replace committed authority.
|
|
131
|
-
|
|
132
|
-
The hook neither creates a model nor acquires a feature. It leases exactly the resources its selection names as
|
|
133
|
-
values — that field answers a `ResourceHook`, and one lease per Resource identity covers the mount even when two
|
|
134
|
-
keys name one Resource (D321, D359) — and nothing else: a
|
|
135
|
-
`read(resource.state)` leases nothing, and neither does the one-argument form. Combining hooks does not promise fewer
|
|
136
|
-
source subscriptions or faster renders; a raw `useModel` import now also includes the selection implementation.
|
|
137
|
-
There is no additional Command scheduler.
|
|
138
|
-
|
|
139
|
-
## Application host
|
|
140
|
-
|
|
141
|
-
The host opens a feature graph with `openApplication`. Ready features publish their UI contributions atomically;
|
|
142
|
-
the ordinary `Slot` mounts them into consumer-owned targets:
|
|
143
|
-
|
|
144
|
-
<!--example
|
|
145
|
-
import { defineSlot, Slot } from '@opetope/react';
|
|
146
|
-
|
|
147
|
-
const exampleFooterSlot = defineSlot('example.footer');
|
|
148
|
-
const exampleSettingsSlot = defineSlot('example.settings');
|
|
149
|
-
|
|
150
|
-
const ExampleShell = () => (
|
|
151
|
-
<>
|
|
152
|
-
/* … */
|
|
153
|
-
</>
|
|
154
|
-
);
|
|
155
|
-
-->
|
|
156
|
-
|
|
157
|
-
```tsx
|
|
158
|
-
<Slot target={exampleFooterSlot} />
|
|
159
|
-
<Slot target={exampleSettingsSlot} />
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
The application owns feature lifetimes; each feature's UI mounts through its contributions.
|
|
163
|
-
|
|
164
|
-
## Feature demand boundary
|
|
165
|
-
|
|
166
|
-
<!--example
|
|
167
|
-
import type { ReactNode } from 'react';
|
|
168
|
-
import type { Resource } from '@opetope/core';
|
|
169
|
-
import { defineSlot } from '@opetope/react';
|
|
170
|
-
import type { FeatureDemandSource } from '@opetope/react/integration';
|
|
171
|
-
|
|
172
|
-
type Tasks = Readonly<{ id: string }>;
|
|
173
|
-
type ConfirmActionProps = Readonly<{ itemId: string; onConfirmed: () => void }>;
|
|
174
|
-
|
|
175
|
-
declare const host: FeatureDemandSource<Readonly<{ exports: Readonly<{ tasks: Resource<Tasks, 'none'> }> }>>;
|
|
176
|
-
declare const itemId: string;
|
|
177
|
-
declare const onConfirmed: () => void;
|
|
178
|
-
declare const children: ReactNode;
|
|
179
|
-
declare const Spinner: () => null;
|
|
180
|
-
declare const Failure: (props: Readonly<{ error: unknown; onRetry: () => void }>) => null;
|
|
181
|
-
declare const TasksResourceLease: (props: Readonly<{ children: ReactNode; resource: Resource<Tasks, 'none'> }>) => null;
|
|
182
|
-
|
|
183
|
-
const confirmActionContentSlot = defineSlot<ConfirmActionProps>('example.confirmAction.content');
|
|
184
|
-
-->
|
|
185
|
-
|
|
186
|
-
```tsx
|
|
187
|
-
import { Slot } from '@opetope/react';
|
|
188
|
-
import { FeatureBoundary, useFeatureRetry } from '@opetope/react/integration';
|
|
189
|
-
|
|
190
|
-
function Retry() {
|
|
191
|
-
return <button onClick={useFeatureRetry()}>Try again</button>;
|
|
192
|
-
}
|
|
55
|
+
## Documentation
|
|
193
56
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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.
|
|
197
60
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
error={({ error, retry }) => <Failure error={error} onRetry={retry} />}
|
|
202
|
-
fallback={<Spinner />}
|
|
203
|
-
>
|
|
204
|
-
{({ exports }) => <TasksResourceLease resource={exports.tasks}>{children}</TasksResourceLease>}
|
|
205
|
-
</FeatureBoundary>;
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
`FeatureBoundary` consumes a `FeatureDemandSource` supplied by host integration. It holds the demand for the feature,
|
|
209
|
-
shows the ready branch once that demand is ready and isolates `useFeatureRetry` inside the error subtree. A branch may be a node or a
|
|
210
|
-
render callback: the callback of `children` receives the ready instance typed by the demand, the callback of `error`
|
|
211
|
-
receives `{ error, retry }`, and only the branch that is shown runs. The callback needs no `useFeature` of its own,
|
|
212
|
-
so a ready consumer does not acquire the demand a second time; hooks still live in child components, not in the
|
|
213
|
-
callback. There is no separate hook for
|
|
214
|
-
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
|
|
215
|
-
root, no `ui` section and no second binding API any more (D85).
|
|
216
|
-
|
|
217
|
-
Both the boundary and the bare `useFeature` are exercised by the packaged consumer of `ci:pack`, which checks the
|
|
218
|
-
law they share: one lease per mount, kept across a settled `retry` and released on unmount. `useFeature` is the
|
|
219
|
-
hook half used where a host renders the readiness itself, and it is a candidate to leave this entry until a second
|
|
220
|
-
application is measured; `FeatureBoundary` is not a pair with `ContributionBoundary` and stays (D301).
|
|
221
|
-
|
|
222
|
-
## Contributions
|
|
223
|
-
|
|
224
|
-
```tsx
|
|
225
|
-
import { defineSlot, defineSwitchSlot, Slot } from '@opetope/react';
|
|
226
|
-
|
|
227
|
-
const HeaderEnd = defineSlot<{ readonly mode: 'desktop' | 'phone' }>('example.headerEnd');
|
|
228
|
-
const PageEnd = defineSwitchSlot<'home' | 'wallet', { readonly compact: boolean }>('example.pageEnd');
|
|
229
|
-
|
|
230
|
-
function Header({ mode }: { readonly mode: 'desktop' | 'phone' }) {
|
|
231
|
-
return <Slot props={{ mode }} target={HeaderEnd} />;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function HomePageEnd() {
|
|
235
|
-
return <Slot props={{ compact: true }} target={PageEnd('home')} />;
|
|
236
|
-
}
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
`defineSlot` creates an authentic target with an ordered set of contributions; `defineSwitchSlot` lazily creates and
|
|
240
|
-
caches a stable target per route. `Slot` reads the atomic snapshot and renders every `{ Component }` in the authority
|
|
241
|
-
frame of the instance that gave the contribution. The order and the stable key come from the contribution entries of the core, so
|
|
242
|
-
React introduces neither a second comparator nor a second lifetime registry. The packaged consumer of `ci:pack`
|
|
243
|
-
exercises that caching and routes one contribution through it (D301).
|
|
244
|
-
|
|
245
|
-
`useResource(resource)` answers `{ pagination, refresh, reset, retry, state }`: the `ResourceState` through
|
|
246
|
-
`useSyncExternalStore`, the lease `resource.acquire()` answers taken in a commit effect for as long as the component
|
|
247
|
-
is mounted, and the three verbs as ordinary `CommandHook` consumers (D359). An interrupted render therefore opens
|
|
248
|
-
nothing, and when the reference changes the state follows the new Resource while the effect cleanup releases the
|
|
249
|
-
previous lease. The number of React hooks does not depend on the Resource: a declaration without `pagination`
|
|
250
|
-
subscribes to a constant source and keeps a `loadNext` that answers `skipped`, while a declaration that carried the
|
|
251
|
-
capability answers `pagination` as `{ loadNext, state }` (D356). A field of a `useModel` selection whose value is a
|
|
252
|
-
Resource answers the same hook and takes the same lease, so a model declares no `Command<void, void>` around
|
|
253
|
-
`resource.refresh()` to give a button its `inFlight`. `useReadable(resource)` is a compile error, because a Resource
|
|
254
|
-
is not a `Readable`; `useReadable(resource.state)` and a `read(resource.state)` in a selector stay passive and lease
|
|
255
|
-
nothing — observing and holding are independent questions, so a selector that reads the state and a `useResource`
|
|
256
|
-
beside it subscribe twice (D349). The compile-checked example of specification §2.13 shows both sides of that
|
|
257
|
-
(D301, D321).
|
|
258
|
-
|
|
259
|
-
`refresh.run()`, `retry.run()` and `reset.run()` reach the Resource, which is owned by the model that declared
|
|
260
|
-
it: `inFlight` is true until the outcome of the operation settles, `outcome` carries that `ResourceOperationOutcome`
|
|
261
|
-
as the `value` of an `ok` status, and `run(undefined, { signal })` cancels the wait of this consumer and not the work
|
|
262
|
-
the model owns. Two consumers of one Resource keep separate statuses, and `run` keeps its identity while the Resource
|
|
263
|
-
does. A render that caught up with retirement renders the terminal record — `idle` with reason `retired` for a Resource
|
|
264
|
-
a model owns, `unleased` for a feature's facade, which is a reference that outlives the instance — and unmounts,
|
|
265
|
-
releasing its lease (D359, D364). Nothing here refuses a read because a lifetime ended: a source whose owner retired
|
|
266
|
-
stops and keeps answering its last value, so `useReadable`, `useSelector` with an inline selector and a selection's
|
|
267
|
-
`read` each read on and need no memory of their own. A refusal that remains is work that failed — a state that
|
|
268
|
-
failed, a broken pagination cursor — and it still reaches the render.
|
|
269
|
-
|
|
270
|
-
Direct component props, a contribution's `props` adapter and its model's props `Readable` share the same
|
|
271
|
-
mount-owned snapshot. Incoming slot props publish in layout before paint, so direct and adapted renders cannot
|
|
272
|
-
mix new props with an old model snapshot. Model factory failures clean up partially created kernels.
|
|
273
|
-
Models belong to commit: an abandoned render creates no model to clean up (D170, D188).
|
|
274
|
-
StrictMode effect replay and Suspense hide/reveal preserve the committed model bundle and state. Only actual
|
|
275
|
-
identity replacement or unmount releases it; a hidden unmount releases in a microtask after React has disconnected
|
|
276
|
-
the layout effects (D209).
|
|
277
|
-
|
|
278
|
-
Demand retry is scoped to the source identity. Replacing a boundary's demand source allows its new retry to run
|
|
279
|
-
even if the previous source's retry is still pending.
|
|
280
|
-
|
|
281
|
-
Every mount is an error boundary of its own contribution (D256). A render or commit that throws stops there, is
|
|
282
|
-
reported to the feature that published the contribution, and leaves the other mounts of the target untouched.
|
|
283
|
-
`ContributionBoundary` states once, above every slot, what a failed mount shows:
|
|
284
|
-
|
|
285
|
-
<!--example
|
|
286
|
-
import { defineSlot, Slot } from '@opetope/react';
|
|
287
|
-
|
|
288
|
-
declare const FailedContribution: (props: Readonly<{ error: unknown; onRetry: () => void }>) => null;
|
|
289
|
-
|
|
290
|
-
const applicationSurface = defineSlot('example.surface');
|
|
291
|
-
-->
|
|
292
|
-
|
|
293
|
-
```tsx
|
|
294
|
-
import { ContributionBoundary } from '@opetope/react/integration';
|
|
295
|
-
|
|
296
|
-
<ContributionBoundary error={({ error, retry }) => <FailedContribution error={error} onRetry={retry} />}>
|
|
297
|
-
<Slot target={applicationSurface} />
|
|
298
|
-
</ContributionBoundary>;
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
`error` takes a node or a callback of `{ contribution, error, feature, retry, target }`, the failure shape of
|
|
302
|
-
`FeatureBoundary` plus the identity of what failed: `contribution` is the published entry `<feature>.<provides key>`
|
|
303
|
-
and `target` is the slot, so one branch can answer by surface. `retry` remounts the contribution, so its models are
|
|
304
|
-
created again. The reporter of the publishing feature receives the same identity on a `ContributionError` whose
|
|
305
|
-
`cause` is the original error, with code `render-failed` or `error-content-failed`. Without the provider a failed mount renders nothing — containment
|
|
306
|
-
never depends on it. Error content that throws is contained the same way and reported, never escalated.
|
|
307
|
-
|
|
308
|
-
## Scenario tests and physical activity
|
|
309
|
-
|
|
310
|
-
`createScenario(application, options)` from `@opetope/react/testing` opens the real application and its existing
|
|
311
|
-
inspection session (D206, D215). Supply the normal `imports`/`conditions` and a test-owned
|
|
312
|
-
`host.mount(Component)` adapter returning an `unmount()` handle. The package adds no DOM renderer or test-runner
|
|
313
|
-
dependency. `scenario.mount(target, { props })` uses the published Slot contributions and returns
|
|
314
|
-
`{ host, updateProps, unmount }`; `host` is the renderer's original result. Typed targets require `options.props`,
|
|
315
|
-
while targets without props omit it, exactly as with `Slot` (D217). Fixture commands do not bypass authority.
|
|
316
|
-
|
|
317
|
-
As with `openApplication`, omit `conditions` when no enabled feature requires a host-bound condition. Conditions
|
|
318
|
-
computed through `source`/`select` bind automatically; external conditions still require their sources (D271).
|
|
319
|
-
|
|
320
|
-
The synchronous constructor exposes `ready`, so a test can inspect a pending lazy body before readiness.
|
|
321
|
-
`waitFor(snapshot => predicate, { label, timeoutMs, pollIntervalMs })` wakes on inspection changes and also polls
|
|
322
|
-
external UI predicates; `notify()` wakes it after a controlled fixture update. The default deadline is 1000ms,
|
|
323
|
-
with a 10ms predicate poll. A `ScenarioTimeoutError` carries the data-only snapshot, bounded history and observed
|
|
324
|
-
conditions, feature phases, body loads, lane blockers and resource lease facts. It does not infer repository
|
|
325
|
-
or network causes. `getSnapshot()` and `history()` use that same observation model; history defaults to 64 snapshots,
|
|
326
|
-
activity to 256 records. Capacities accept integers from 1 to 10000. Do not replace predicates with a fixed number of ticks.
|
|
327
|
-
|
|
328
|
-
`close()` fences application admission synchronously, unmounts all registered screens and joins their cleanup with
|
|
329
|
-
physical application drain. Its deadline does not cancel cleanup: a later `close()` can await the same drain.
|
|
330
|
-
A readiness deadline likewise leaves the application available for inspection and explicit cleanup.
|
|
331
|
-
`ownership()` reports only registered runtime ownership, with `unknown` for missing, stale or truncated evidence;
|
|
332
|
-
a workspace stale snapshot is complete only after the scenario witnessed successful physical cleanup. This permits
|
|
333
|
-
a scoped zero-count assertion, without proving absence of arbitrary host, UI or GC leaks. Successful cleanup clears
|
|
334
|
-
application imports and internal renderer references. A failed cleanup promise can retain original errors and retry
|
|
335
|
-
capabilities; a caller that keeps `mounted.host` also keeps its own renderer result.
|
|
336
|
-
|
|
337
|
-
The inspection schemas are `opetope.devtools-graph/5` and `opetope.devtools-frame/5`, with optional
|
|
338
|
-
`opetope.runtime-activity/4` snapshots (D266, D275, D344, D358). Within one session,
|
|
339
|
-
a frame without `activity` preserves the previous activity; a full snapshot/reset without it clears that observation
|
|
340
|
-
(D216). Activity-bearing frames replace the previous activity in full.
|
|
341
|
-
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,
|
|
342
|
-
actual feature generation, physical calls, exact current lane blockers, and for every registered Resource its
|
|
343
|
-
`kind`, `lifetime`, `epoch`, `state`, `activity` with its `operation`, `leases`, `subscribers`, `pagination` and the
|
|
344
|
-
`epoch` of each physical load — there is no `attempt` and no numeric generation, because a Resource runs one logical
|
|
345
|
-
attempt and recovery belongs to the transport (D358). Host demand and UI models are unknown. `freshness`
|
|
346
|
-
and `truncated` distinguish a complete live view from a partial or detached one. A closed session is stale;
|
|
347
|
-
`closed: true` says that the close of the application finished, whether its physical drain succeeded or refused, and
|
|
348
|
-
the snapshot of a closed application names no feature and no Resource (D427). No control authority or product payload is added.
|
|
349
|
-
Activity output is bounded by record capacity. Snapshot collection still visits registered owners, executors and
|
|
350
|
-
resources, so capacity does not bound traversal cost. Collection stops once truncation is proven;
|
|
351
|
-
idle executors may still require traversal to establish completeness. Normal call dispatch allocates no diagnostic record with
|
|
352
|
-
observation disabled. Graph frames remain bounded by the existing ring capacity.
|
|
353
|
-
|
|
354
|
-
## Testing
|
|
355
|
-
|
|
356
|
-
`@opetope/react/testing` exports component fixtures and application scenarios, and re-exports
|
|
357
|
-
`@opetope/runtime/testing` — which in turn re-exports `@opetope/core/testing` — so a React test names one entry
|
|
358
|
-
(D285):
|
|
359
|
-
|
|
360
|
-
- `renderSlot(target, { contribution, models, props })` mounts the published target or one fixture contribution;
|
|
361
|
-
- `command(run)` produces an authentic `Command` for a model fixture, plus the contribution binding a mount reads;
|
|
362
|
-
- `runCommand(target, input?, options?)` executes an existing authentic `Command` without mounting React (D261);
|
|
363
|
-
- `testReadable(initial)`, `testResourceEpoch()`, `openModel`, `settled`, `waitFor`, `yieldTurn` and `eventually`
|
|
364
|
-
come from the entries below, for the part of a test that has no UI in it — `testResourceEpoch()` is what a
|
|
365
|
-
component fixture writing a `ResourceState` by hand puts in its `epoch` (D362).
|
|
366
|
-
|
|
367
|
-
A test that renders nothing should import `@opetope/runtime/testing` directly: `renderSlot` and the binding half of
|
|
368
|
-
`command` are all that needs React here. The Command a fixture mints is the `command` of `@opetope/core/testing`
|
|
369
|
-
(D300), and this word is the one name of the re-export chain that does not pass through: the React `command`
|
|
370
|
-
shadows it with the stronger one.
|
|
371
|
-
|
|
372
|
-
The `renderSlot` harness returns `Slot` and `updateProps`: there are no roots and no fixtures for them, UI enters the
|
|
373
|
-
application as contributions (D85). It is not a test spelling of `<Slot>` — it mounts a fixture and takes a
|
|
374
|
-
`reporter`, because a component test has no feature to report to — and the packaged consumer of `ci:pack` mounts it
|
|
375
|
-
from the real archive (D301).
|
|
376
|
-
|
|
377
|
-
A test that previously mounted `useCommand` only to invoke a Command can use `runCommand` directly. The result is the
|
|
378
|
-
Command's output, while a UI test still checks the `CommandOutcome` returned by the mounted consumer:
|
|
379
|
-
|
|
380
|
-
```ts
|
|
381
|
-
import assert from 'node:assert/strict';
|
|
382
|
-
import { defineModel } from '@opetope/core';
|
|
383
|
-
import type { Command, ModelCommandContext } from '@opetope/core';
|
|
384
|
-
import { defineFeature, openFeature } from '@opetope/runtime';
|
|
385
|
-
import { runCommand } from '@opetope/react/testing';
|
|
386
|
-
|
|
387
|
-
const Arithmetic = defineModel<{ readonly double: Command<number, number> }>('example.testing.model');
|
|
388
|
-
const arithmetic = defineFeature('example.testing.arithmetic', {
|
|
389
|
-
own: ({ model }) => ({
|
|
390
|
-
arithmetic: model(Arithmetic, context => ({
|
|
391
|
-
double: context.command(({ input }: ModelCommandContext<number>) => input * 2),
|
|
392
|
-
})),
|
|
393
|
-
}),
|
|
394
|
-
exports: ({ own }) => ({ double: own.arithmetic.double }),
|
|
395
|
-
});
|
|
396
|
-
const instance = openFeature(arithmetic, { imports: {}, reporter: () => undefined });
|
|
397
|
-
try {
|
|
398
|
-
const ready = await instance.ready;
|
|
399
|
-
const result = await runCommand(ready.exports.double, 3);
|
|
400
|
-
assert.equal(result, 6);
|
|
401
|
-
} finally {
|
|
402
|
-
await instance.close();
|
|
403
|
-
}
|
|
404
|
-
```
|
|
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.
|
|
405
64
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
owner, lane and retirement rules; the test closes the instance it opened, including after failed readiness.
|
|
409
|
-
A void Command can use `runCommand(target)`. Options always occupy the third argument:
|
|
410
|
-
`runCommand(target, input, { signal })`, or `runCommand(voidTarget, undefined, { signal })`.
|
|
411
|
-
`RunCommandOptions` 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).
|
|
412
67
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
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).
|
|
416
70
|
|
|
417
|
-
|
|
71
|
+
<a id="application-host"></a>
|
|
72
|
+
[See Application host](../runtime/docs/reference/react.md).
|
|
418
73
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
| `@opetope/react` | `useModel`, `useCommand`, `useReadable`, `useSelector`, `useResource`, `requiresModels`, `defineSlot`, `defineSwitchSlot`, `Slot`, `ContributionError`; types `SlotTarget`, `SwitchSlotTarget`, `SlotContribution`, `CommandHook`, `CommandOutcome`, `ResourceHook`, `PaginatedResourceHook`, `ResourcePaginationHook` |
|
|
422
|
-
| `@opetope/react/integration` | `FeatureBoundary`, `ContributionBoundary`, `useFeature`, `useFeatureRetry`, `FeatureBoundaryError` for the integration layer; types `FeatureBoundaryProps`, `ContributionBoundaryProps`, `ContributionErrorContent`, `ContributionFailure`, `FeatureDemandSource`, `FeatureDemandState`, `FeatureDemandResult`, `FeatureDemandLease` |
|
|
423
|
-
| `@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).
|
|
424
76
|
|
|
425
|
-
|
|
77
|
+
<a id="contributions"></a>
|
|
78
|
+
[See Contributions](../runtime/docs/reference/react.md).
|
|
426
79
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
`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).
|
|
430
82
|
|
|
431
|
-
|
|
83
|
+
<a id="testing"></a>
|
|
84
|
+
[See Testing](../runtime/docs/reference/react.md).
|
|
432
85
|
|
|
433
|
-
-
|
|
434
|
-
|
|
435
|
-
- an abandoned concurrent render holds no frame;
|
|
436
|
-
- `useCommand` does not subscribe to the invoker: the observable state of a call lies in a `Readable` of the model;
|
|
437
|
-
- the `cancelled` outcome does not move `outcome`, and it is only the `CommandError` codes `cancelled`
|
|
438
|
-
and `closed`; a call to a weak port with no provider (`unavailable`) and a rejected publication
|
|
439
|
-
(`publication-rejected`) arrive as the `failed` outcome and settle in `outcome` (D138, D292);
|
|
440
|
-
- a contribution that fails to render or to commit is contained by its own mount and reported to its feature;
|
|
441
|
-
- `useSelector` reads its `equals` where the selected value is known, so a named non-generic comparator compiles
|
|
442
|
-
beside a selection whose parameter is inferred, as it does in Core (D348);
|
|
443
|
-
- `useSelector` keeps the selected reference when something else changed; its identity case is `useReadable` and its
|
|
444
|
-
model case is a `useModel` selection, so it is proven by the memory gate and the packaged consumer rather than by a
|
|
445
|
-
specification example, and it is a candidate to leave this entry until a second application is measured (D301);
|
|
446
|
-
- 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).
|
|
447
88
|
|
|
448
|
-
|
|
89
|
+
<a id="laws"></a>
|
|
90
|
+
[See Laws](../runtime/docs/reference/react.md).
|
|
449
91
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
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).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opetope/react",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=20.19.0"
|
|
6
6
|
},
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
17
|
"README.md",
|
|
18
|
-
"README.ru.md",
|
|
19
18
|
"LICENSE",
|
|
20
19
|
"CHANGELOG.md"
|
|
21
20
|
],
|
|
@@ -50,16 +49,16 @@
|
|
|
50
49
|
}
|
|
51
50
|
],
|
|
52
51
|
"devDependencies": {
|
|
53
|
-
"@opetope/core": "0.12.
|
|
54
|
-
"@opetope/runtime": "0.12.
|
|
52
|
+
"@opetope/core": "0.12.1",
|
|
53
|
+
"@opetope/runtime": "0.12.1",
|
|
55
54
|
"@testing-library/react": "16.3.3",
|
|
56
55
|
"@types/react": "19.2.18",
|
|
57
56
|
"react": "19.2.8",
|
|
58
57
|
"react-dom": "19.2.8"
|
|
59
58
|
},
|
|
60
59
|
"peerDependencies": {
|
|
61
|
-
"@opetope/core": "0.12.
|
|
62
|
-
"@opetope/runtime": "0.12.
|
|
60
|
+
"@opetope/core": "0.12.1",
|
|
61
|
+
"@opetope/runtime": "0.12.1",
|
|
63
62
|
"react": ">=19.0.0 <20"
|
|
64
63
|
},
|
|
65
64
|
"sideEffects": false,
|
package/README.ru.md
DELETED
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
# `@opetope/react`
|
|
2
|
-
|
|
3
|
-
React-привязка для моделей и UI-вкладов Opetope. Словарь пакета задаёт §3 [спецификации](../runtime/docs/spec.ru.md).
|
|
4
|
-
|
|
5
|
-
## Установка
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
npm install @opetope/core @opetope/runtime @opetope/react 'react@^19.0.0' 'react-dom@^19.0.0'
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Используйте согласованные версии Opetope. Для release candidate добавьте `@next` каждому пакету `@opetope/*` в команде.
|
|
12
|
-
API поставляется только в ESM; требуется Node 20.19+. Команды разработки ниже относятся к contributor checkout.
|
|
13
|
-
|
|
14
|
-
Нормативные руководства EN/RU поставляются в `@opetope/runtime`: после его установки откройте
|
|
15
|
-
`node_modules/@opetope/runtime/docs/spec.md` или `spec.ru.md`; рецепты находятся в `cookbook.md` и `cookbook.ru.md`.
|
|
16
|
-
Для чтения установленных руководств доступ к GitHub не нужен.
|
|
17
|
-
|
|
18
|
-
## Hello UI
|
|
19
|
-
|
|
20
|
-
```tsx
|
|
21
|
-
import { defineModel } from '@opetope/core';
|
|
22
|
-
import type { Command, Readable } from '@opetope/core';
|
|
23
|
-
import { defineSlot, requiresModels, Slot, useModel } from '@opetope/react';
|
|
24
|
-
|
|
25
|
-
const CounterModel = defineModel<{
|
|
26
|
-
readonly count: Readable<number>;
|
|
27
|
-
readonly increment: Command<void, void>;
|
|
28
|
-
}>('example.counter.model');
|
|
29
|
-
const CounterSlot = defineSlot<{ readonly label: string }>('example.counter.slot');
|
|
30
|
-
|
|
31
|
-
const CounterButton = requiresModels([CounterModel])(({ label }: { readonly label: string }) => {
|
|
32
|
-
const {
|
|
33
|
-
count,
|
|
34
|
-
increment: { run, inFlight, outcome },
|
|
35
|
-
} = useModel(CounterModel, (model, { read }) => ({
|
|
36
|
-
count: read(model.count),
|
|
37
|
-
increment: model.increment,
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<button disabled={inFlight} onClick={() => void run()}>
|
|
42
|
-
{outcome?.kind === 'failed' ? 'Retry' : `${label}: ${count}`}
|
|
43
|
-
</button>
|
|
44
|
-
);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
function CounterArea() {
|
|
48
|
-
return <Slot props={{ label: 'Count' }} target={CounterSlot} />;
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Компонент не получает ни сервис, ни класс, ни ref рантайма. Монтирование вклада неявно выдаёт модели `own` его фичи;
|
|
53
|
-
компонент или хук, читающий per-mount модель, объявляет её через `requiresModels`. `useModel` читает ровно этот кадр
|
|
54
|
-
авторитета. `useCommand` отдаёт `{ run, inFlight, outcome }`
|
|
55
|
-
и всегда resolve-ит исход, поэтому штатная отмена не становится unhandled rejection (D116, D292).
|
|
56
|
-
|
|
57
|
-
`outcome` — последний завершившийся не-отменённый исход этого потребителя: `ok` встаёт на место прежнего `failed`,
|
|
58
|
-
`failed` — на место прежнего `ok`, исход `cancelled` не двигает ничего, и старт прогона ничего не чистит, поэтому
|
|
59
|
-
отказ читается, пока идёт повтор. До первого завершения он `undefined`. Читают его по дискриминанту —
|
|
60
|
-
`outcome?.kind === 'failed' ? outcome.error : null`.
|
|
61
|
-
|
|
62
|
-
Команда без входа привязывается к событию стрелкой: `onClick={() => void logout.run()}`. Именно стрелка не пускает
|
|
63
|
-
React-событие в Command и не оставляет промис висеть; `onClick={logout.run}` не компилируется, потому что `MouseEvent`
|
|
64
|
-
не ложится в `void`. Для данных, исхода, callbacks или сигнала отдельного вызова используйте `run(input, options?)`.
|
|
65
|
-
Там, где конфиг линтера запрещает стрелку прямо в пропсе (`react-perf/jsx-no-new-function-as-prop`,
|
|
66
|
-
`react/jsx-no-bind`), её поднимают через `useCallback(() => void logout.run(), [logout.run])`: зависимость — `run`,
|
|
67
|
-
а не объект хука (D290).
|
|
68
|
-
|
|
69
|
-
`run` стабилен, пока invoker тот же; возвращённый объект — снимок того рендера, в котором его прочитали, и меняется
|
|
70
|
-
вместе с `inFlight` или `outcome`, поэтому эффект или memo зависят от `run`, а не от хука, — правило
|
|
71
|
-
`opetope/no-command-in-deps` держит это за автора (D290). Сохранённый `run` заменённого или
|
|
72
|
-
размонтированного потребителя закрыт для новых вызовов и не перенаправляет работу на замену.
|
|
73
|
-
|
|
74
|
-
Хук ничего не планирует. Каждый `run` доходит до команды, и решает порядок, с которым команда объявлена (D203).
|
|
75
|
-
Для setter абсолютного значения модель объявляет `concurrency: 'latest'` в `context.command`, и новый вход
|
|
76
|
-
заменяет ожидающий именно там (D185, D387). За потребителем остаётся своё: уже отменённый входной signal
|
|
77
|
-
отвергается как `cancelled`, не доходя до команды, `run` размонтированного потребителя отвергается так же,
|
|
78
|
-
`inFlight` истинен, пока не завершился хоть один начатый им прогон, и у каждого прогона свои callbacks и signal.
|
|
79
|
-
|
|
80
|
-
Чтобы объединить эквивалентную ожидающую или исполняющуюся работу, модель объявляет `dedupe: true` либо
|
|
81
|
-
`dedupe: input => key` у `context.command`. Сохраняются первый вход и независимая отмена каждого вызывающего;
|
|
82
|
-
обе формы запрещены с `latest` (D277). Это опции Command, а не хука.
|
|
83
|
-
|
|
84
|
-
Для нескольких команд компонент пишет `useCommand` на команду либо одну выборку `useModel`, которая и так
|
|
85
|
-
отдаёт готовые хуки. `useCommands` удалён: запись поверх единственного `CommandHook` была сахаром, экономившим
|
|
86
|
-
вызовы хуков, а не вторым механизмом, и нужный набор — это выбор в модели, где у него есть владелец и время
|
|
87
|
-
жизни, которые даёт `ctx.select` (D388).
|
|
88
|
-
|
|
89
|
-
Два псевдонима одного Command имеют независимые локальные статусы, как два потребителя `useCommand`; они
|
|
90
|
-
используют один порядок Command и одну command-запись монтирования (D197, D203). Выборка не вводит общий busy,
|
|
91
|
-
очередь или транзакцию; контролам, перезаписывающим одно значение, по-прежнему нужна одна смысловая команда.
|
|
92
|
-
|
|
93
|
-
## Выбор данных и команд
|
|
94
|
-
|
|
95
|
-
`useModel(Declaration, (model, { read }) => ({ ... }))` объединяет явный выбор данных и command consumers (D205, D214).
|
|
96
|
-
|
|
97
|
-
Результат выбора модели может быть именованным interface без index signature (D217). Результат — плоская запись данных;
|
|
98
|
-
arrays, functions, constructors и встроенные объекты коллекций, дат и promises не являются selection records.
|
|
99
|
-
`read(source)` возвращает снимок; `read(source, project)` — проекцию. Authentic Commands, выбранные полями record,
|
|
100
|
-
превращаются в тот же `CommandHook`, что у `useCommand`, с независимыми статусами aliases и стабильным `.run`.
|
|
101
|
-
Одну команду можно выбрать без вложенных hooks:
|
|
102
|
-
|
|
103
|
-
```tsx
|
|
104
|
-
const { logout } = useModel(AuthModel, auth => ({ logout: auth.logout }));
|
|
105
|
-
return (
|
|
106
|
-
<button disabled={logout.inFlight} onClick={() => void logout.run()}>
|
|
107
|
-
Log out
|
|
108
|
-
</button>
|
|
109
|
-
);
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Форма с одним аргументом по-прежнему возвращает выданную модель; отдельные hooks сохраняются.
|
|
113
|
-
|
|
114
|
-
Подписка создаётся только на явно прочитанные источники, одна на каждый различный Readable внутри selection.
|
|
115
|
-
Поля данных сравниваются через `Object.is`: выбирайте скаляры, раскрывайте проекцию record в selection или
|
|
116
|
-
возвращайте стабильные ссылки. Новый вложенный объект считается изменившимся полем. Callback чистый:
|
|
117
|
-
без hooks, команд и side effects. Ошибка read/selector попадает в ближайший React error boundary.
|
|
118
|
-
Изменение источников и ключей команд применяется в commit; abandoned render не меняет действующие полномочия.
|
|
119
|
-
|
|
120
|
-
Hook не создаёт модель и не приобретает feature. Он арендует ровно те ресурсы, которые выборка назвала
|
|
121
|
-
значениями: такое поле отвечает `ResourceHook`, и одна аренда на identity Resource покрывает всё монтирование, даже
|
|
122
|
-
когда один Resource назвали два ключа (D321, D359). Больше ничего:
|
|
123
|
-
`read(resource.state)` не арендует, и однопараметрическая форма тоже. Объединение hooks не обещает меньше подписок или
|
|
124
|
-
более быстрый render; импорт обычного `useModel` теперь также включает реализацию selection. Дополнительного
|
|
125
|
-
scheduler для Command нет.
|
|
126
|
-
|
|
127
|
-
## Application host
|
|
128
|
-
|
|
129
|
-
Хост открывает граф фич через `openApplication`. Готовые фичи атомарно публикуют UI-вклады;
|
|
130
|
-
обычный `Slot` монтирует их в consumer-owned целях:
|
|
131
|
-
|
|
132
|
-
```tsx
|
|
133
|
-
<Slot target={exampleFooterSlot} />
|
|
134
|
-
<Slot target={exampleSettingsSlot} />
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Приложение владеет временем жизни фич; UI каждой фичи монтируется через её вклады.
|
|
138
|
-
|
|
139
|
-
## Граница спроса на фичу
|
|
140
|
-
|
|
141
|
-
```tsx
|
|
142
|
-
import { Slot } from '@opetope/react';
|
|
143
|
-
import { FeatureBoundary, useFeatureRetry } from '@opetope/react/integration';
|
|
144
|
-
|
|
145
|
-
function Retry() {
|
|
146
|
-
return <button onClick={useFeatureRetry()}>Try again</button>;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
<FeatureBoundary demand={host} error={<Retry />} fallback={<Spinner />}>
|
|
150
|
-
<Slot props={{ itemId, onConfirmed }} target={confirmActionContentSlot} />
|
|
151
|
-
</FeatureBoundary>;
|
|
152
|
-
|
|
153
|
-
// или с render-колбэками, когда нужен сам готовый экземпляр (D177)
|
|
154
|
-
<FeatureBoundary
|
|
155
|
-
demand={host}
|
|
156
|
-
error={({ error, retry }) => <Failure error={error} onRetry={retry} />}
|
|
157
|
-
fallback={<Spinner />}
|
|
158
|
-
>
|
|
159
|
-
{({ exports }) => <TasksResourceLease resource={exports.tasks}>{children}</TasksResourceLease>}
|
|
160
|
-
</FeatureBoundary>;
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
`FeatureBoundary` принимает `FeatureDemandSource` от интеграции с хостом. Он держит спрос на фичу,
|
|
164
|
-
показывает готовую ветвь после готовности спроса и изолирует `useFeatureRetry` в поддереве ошибки. Ветвь может быть узлом или
|
|
165
|
-
render-колбэком: колбэк `children` получает готовый экземпляр, типизированный по `demand`, колбэк `error` получает
|
|
166
|
-
`{ error, retry }`, и выполняется только показанная ветвь. Своего `useFeature` колбэку не нужно, поэтому готовый
|
|
167
|
-
потребитель не берёт спрос второй раз; хуки по-прежнему живут в дочерних компонентах, а не в колбэке. Отдельного хука для чтения ошибки
|
|
168
|
-
нет: хост сам передал её в `error`, поэтому знает её без второго слова (D142). UI входит в фичу через `Slot`: ни
|
|
169
|
-
корня, ни секции `ui`, ни второго API привязки больше нет (D85).
|
|
170
|
-
|
|
171
|
-
И boundary, и голый `useFeature` проверяет упакованный потребитель `ci:pack` — на общем для них законе: одна аренда
|
|
172
|
-
на монтирование, сохраняется через севший `retry` и отпускается на размонтировании. `useFeature` это хуковая
|
|
173
|
-
половина для случая, когда готовность рендерит сам хост, и он кандидат на вывод с этого входа до второго
|
|
174
|
-
измеренного приложения; `FeatureBoundary` не пара `ContributionBoundary` и остаётся (D301).
|
|
175
|
-
|
|
176
|
-
## Contributions
|
|
177
|
-
|
|
178
|
-
```tsx
|
|
179
|
-
import { defineSlot, defineSwitchSlot, Slot } from '@opetope/react';
|
|
180
|
-
|
|
181
|
-
const HeaderEnd = defineSlot<{ readonly mode: 'desktop' | 'phone' }>('example.headerEnd');
|
|
182
|
-
const PageEnd = defineSwitchSlot<'home' | 'wallet', { readonly compact: boolean }>('example.pageEnd');
|
|
183
|
-
|
|
184
|
-
function Header({ mode }: { readonly mode: 'desktop' | 'phone' }) {
|
|
185
|
-
return <Slot props={{ mode }} target={HeaderEnd} />;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function HomePageEnd() {
|
|
189
|
-
return <Slot props={{ compact: true }} target={PageEnd('home')} />;
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
`defineSlot` создаёт подлинную цель с упорядоченным множеством вкладов; `defineSwitchSlot` лениво создаёт и
|
|
194
|
-
кеширует стабильную цель на каждый маршрут. `Slot` читает атомарный снимок и рендерит каждый `{ Component }` в кадре
|
|
195
|
-
авторитета того экземпляра, который вклад дал. Порядок и стабильный ключ приходят из записей вкладов ядра, поэтому
|
|
196
|
-
React не вводит второй компаратор и второй реестр времени жизни. Упакованный потребитель `ci:pack` проверяет это
|
|
197
|
-
кеширование и проводит через него один вклад (D301).
|
|
198
|
-
|
|
199
|
-
`useResource(resource)` отвечает `{ pagination, refresh, reset, retry, state }`: `ResourceState` через
|
|
200
|
-
`useSyncExternalStore`, арендой, которую отдаёт `resource.acquire()`, взятой в commit-эффекте на всё время
|
|
201
|
-
монтирования компонента, и тремя глаголами как обычными потребителями `CommandHook` (D359). Поэтому прерванный
|
|
202
|
-
render не открывает ничего, а при смене ссылки состояние следует новому Resource, и cleanup эффекта освобождает
|
|
203
|
-
предыдущую аренду. Число React-хуков от Resource не зависит: объявление без `pagination` подписывается на
|
|
204
|
-
постоянный источник и держит `loadNext`, который отвечает `skipped`, а объявление, несшее эту capability, отвечает
|
|
205
|
-
`pagination` записью `{ loadNext, state }` (D356). Поле выборки `useModel`, значением которого является Resource,
|
|
206
|
-
отвечает тем же хуком и берёт ту же аренду, поэтому модель не объявляет `Command<void, void>` вокруг
|
|
207
|
-
`resource.refresh()` ради `inFlight` в кнопке. `useReadable(resource)` — это ошибка компиляции, потому что Resource
|
|
208
|
-
не `Readable`; `useReadable(resource.state)` и `read(resource.state)` в селекторе остаются пассивными и не арендуют
|
|
209
|
-
ничего: наблюдать и держать — независимые вопросы, поэтому селектор, читающий состояние, и стоящий рядом
|
|
210
|
-
`useResource` подписываются дважды (D349). Обе стороны показывает компилируемый пример спецификации §2.13
|
|
211
|
-
(D301, D321).
|
|
212
|
-
|
|
213
|
-
`refresh.run()`, `retry.run()` и `reset.run()` доходят до Resource, которым владеет объявившая его модель:
|
|
214
|
-
`inFlight` истинен, пока не осел исход операции, `outcome` несёт этот `ResourceOperationOutcome` значением `value`
|
|
215
|
-
при статусе `ok`, а `run(undefined, { signal })` отменяет ожидание этого потребителя, а не работу, которой владеет
|
|
216
|
-
модель. Два потребителя одного Resource держат раздельные статусы, а `run` сохраняет ссылку, пока та же
|
|
217
|
-
идентичность Resource. Render, догнавший retirement, рисует терминальную запись — `idle` с причиной `retired` у Resource, которым
|
|
218
|
-
владеет модель, и `unleased` у фасада фичи, потому что эта ссылка переживает экземпляр, — и размонтируется, отпустив
|
|
219
|
-
аренду (D359, D364). Ничто здесь не отказывает в чтении из-за того, что кончилась чья-то жизнь: источник, у которого
|
|
220
|
-
ушёл владелец, останавливается и продолжает отвечать последним значением, поэтому `useReadable`, `useSelector` с
|
|
221
|
-
inline-селектором и `read` выборки просто читают дальше, и собственная память им не нужна. Оставшийся отказ — это
|
|
222
|
-
упавшая работа: упавшее состояние, сломанный курсор пагинации, — и он по-прежнему доходит до рендера.
|
|
223
|
-
|
|
224
|
-
Прямые пропсы компонента, `props`-адаптер вклада и props-`Readable` его модели используют один снимок
|
|
225
|
-
монтирования. Входящие пропсы слота публикуются в layout до paint, поэтому прямой и адаптированный рендеры
|
|
226
|
-
не смешивают новые пропсы со старым снимком модели. Отказ фабрики модели убирает частично созданный kernel.
|
|
227
|
-
Модели принадлежат коммиту: брошенный рендер не создаёт модель, которую пришлось бы убирать (D170, D188).
|
|
228
|
-
Повтор эффектов StrictMode и скрытие/раскрытие Suspense сохраняют закоммиченный набор моделей и состояние.
|
|
229
|
-
Только реальная замена идентичности или размонтирование освобождает его; при скрытом размонтировании это делает
|
|
230
|
-
микрозадача после того, как React отключил layout-эффекты (D209).
|
|
231
|
-
|
|
232
|
-
Retry спроса привязан к источнику. После замены источника boundary новый retry может начаться, даже если retry
|
|
233
|
-
старого ещё не завершён.
|
|
234
|
-
|
|
235
|
-
Каждое монтирование — граница ошибок своего вклада (D256). Сбой рендера или коммита останавливается на нём, уходит в
|
|
236
|
-
reporter опубликовавшей вклад фичи и не трогает остальные монтирования цели. `ContributionBoundary` один раз, над
|
|
237
|
-
всеми слотами, говорит, что показывает упавшее монтирование:
|
|
238
|
-
|
|
239
|
-
```tsx
|
|
240
|
-
import { ContributionBoundary } from '@opetope/react/integration';
|
|
241
|
-
|
|
242
|
-
<ContributionBoundary error={({ error, retry }) => <FailedContribution error={error} onRetry={retry} />}>
|
|
243
|
-
<Slot target={applicationSurface} />
|
|
244
|
-
</ContributionBoundary>;
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
`error` принимает узел или колбэк `{ contribution, error, feature, retry, target }` — форму отказа `FeatureBoundary`
|
|
248
|
-
плюс идентичность упавшего: `contribution` — опубликованная запись `<фича>.<ключ provides>`, `target` — слот, поэтому
|
|
249
|
-
одна ветвь может отвечать по поверхности. `retry` перемонтирует вклад, поэтому его модели создаются заново. Reporter
|
|
250
|
-
опубликовавшей фичи получает ту же идентичность на `ContributionError`, у которого в `cause` исходная ошибка, с кодом
|
|
251
|
-
`render-failed` или `error-content-failed`. Без провайдера упавшее монтирование рендерит пустоту:
|
|
252
|
-
изоляция от него не зависит. Упавшее содержимое ошибки изолируется так же и сообщается, а не эскалируется.
|
|
253
|
-
|
|
254
|
-
## Сценарные тесты и физическая активность
|
|
255
|
-
|
|
256
|
-
`createScenario(application, options)` из `@opetope/react/testing` открывает настоящее приложение и его существующую
|
|
257
|
-
inspection session (D206, D215). Передайте обычные `imports`/`conditions` и принадлежащий тесту адаптер
|
|
258
|
-
`host.mount(Component)`, возвращающий handle с `unmount()`. Пакет не добавляет зависимость от DOM renderer или test runner.
|
|
259
|
-
`scenario.mount(target, { props })` использует опубликованные Slot contributions и возвращает
|
|
260
|
-
`{ host, updateProps, unmount }`; `host` — исходный результат renderer. Typed targets требуют `options.props`,
|
|
261
|
-
а targets без props опускают его, как в `Slot` (D217). Fixtures не обходят authority команд.
|
|
262
|
-
|
|
263
|
-
Как и в `openApplication`, `conditions` можно опустить, если включённые фичи не требуют условий от хоста.
|
|
264
|
-
Условия с `source`/`select` связываются автоматически; внешним условиям по-прежнему нужны источники (D271).
|
|
265
|
-
|
|
266
|
-
Синхронный конструктор возвращает `ready`, поэтому pending lazy body можно исследовать до готовности.
|
|
267
|
-
`waitFor(snapshot => predicate, { label, timeoutMs, pollIntervalMs })` просыпается от inspection и дополнительно
|
|
268
|
-
опрашивает predicates внешнего UI; `notify()` будит его после изменения управляемой fixture. По умолчанию deadline
|
|
269
|
-
равен 1000ms, polling predicate — 10ms. `ScenarioTimeoutError` содержит data-only snapshot, ограниченную историю и
|
|
270
|
-
наблюдаемые conditions, фазы feature, body load, lane blockers и аренды ресурсов. Причины внутри repository
|
|
271
|
-
или сети не выводятся из догадок. `getSnapshot()` и `history()` используют ту же модель наблюдения; по умолчанию
|
|
272
|
-
хранятся 64 снимка, activity ограничена 256 записями. Capacities — целые от 1 до 10000.
|
|
273
|
-
Не заменяйте predicates фиксированным числом ticks.
|
|
274
|
-
|
|
275
|
-
`close()` синхронно ставит fence admission приложения, размонтирует зарегистрированные экраны и ждёт их cleanup
|
|
276
|
-
вместе с physical application drain. Deadline не отменяет cleanup: последующий `close()` может дождаться того же drain.
|
|
277
|
-
Deadline готовности также оставляет приложение доступным для inspection и явного закрытия.
|
|
278
|
-
`ownership()` описывает только зарегистрированное владение runtime; при отсутствующих, stale или усечённых данных
|
|
279
|
-
возвращается `unknown`. Терминальный stale-снимок считается полным лишь после подтверждённого сценарием успешного
|
|
280
|
-
физического cleanup. Это допускает проверку нулевых счётчиков в данном scope, но не доказывает отсутствие произвольных
|
|
281
|
-
host/UI/GC-утечек. Успешный cleanup очищает imports приложения и внутренние ссылки на renderer. Promise отказавшего
|
|
282
|
-
cleanup может удерживать исходные ошибки и retry capabilities; сохранённый пользователем `mounted.host` удерживает его renderer result.
|
|
283
|
-
|
|
284
|
-
Inspection использует схемы `opetope.devtools-graph/5` и `opetope.devtools-frame/5`, а также optional snapshots
|
|
285
|
-
`opetope.runtime-activity/4` (D266, D275, D344, D358). В пределах одной session
|
|
286
|
-
frame без `activity` сохраняет предыдущую activity; полный snapshot/reset без этого поля очищает наблюдение (D216).
|
|
287
|
-
Frame с `activity` заменяет предыдущую activity целиком.
|
|
288
|
-
Используйте согласованные версии runtime/devtools: предыдущие ревизии графа, кадров и activity отвергаются; presence слабого ребра требует graph `/5` и frame `/4`. Activity указывает execution,
|
|
289
|
-
фактическое поколение feature, физические вызовы, точных текущих lane blockers, а для каждого зарегистрированного
|
|
290
|
-
Resource — его `kind`, `lifetime`, `epoch`, `state`, `activity` вместе с `operation`, `leases`, `subscribers`,
|
|
291
|
-
`pagination` и `epoch` каждой физической загрузки: ни `attempt`, ни числового generation здесь нет, потому что
|
|
292
|
-
Resource делает одну логическую попытку, а восстановление принадлежит транспорту (D358). Спрос хоста и UI-модели
|
|
293
|
-
неизвестны. `freshness`
|
|
294
|
-
и `truncated` отличают полное live-наблюдение от усечённого или отключённого. Закрытая session имеет stale-снимок;
|
|
295
|
-
`closed: true` говорит, что закрытие приложения завершилось, успешным был его физический drain или отказавшим, и
|
|
296
|
-
снимок закрытого приложения не называет ни одной фичи и ни одного Resource (D427). Control authority и продуктовые payload не добавляются.
|
|
297
|
-
Размер activity ограничен capacity записей. Сбор снимка обходит зарегистрированных owners, executors и resources,
|
|
298
|
-
поэтому capacity не ограничивает стоимость обхода. Сбор останавливается после доказанного truncation;
|
|
299
|
-
idle executors могут требовать обхода, чтобы подтвердить полноту данных. Обычный call dispatch не создаёт диагностических записей при
|
|
300
|
-
выключенном наблюдении. Frames ограничены существующей ring capacity.
|
|
301
|
-
|
|
302
|
-
## Testing
|
|
303
|
-
|
|
304
|
-
`@opetope/react/testing` экспортирует fixtures компонентов и сценарии приложения, а также реэкспортирует
|
|
305
|
-
`@opetope/runtime/testing`, который, в свою очередь, реэкспортирует `@opetope/core/testing`, поэтому React-тест
|
|
306
|
-
называет один вход (D285):
|
|
307
|
-
|
|
308
|
-
- `renderSlot(target, { contribution, models, props })` монтирует опубликованную цель либо один вклад-фикстуру;
|
|
309
|
-
- `command(run)` выдаёт подлинный `Command` для фикстуры модели плюс привязку вклада, которую читает монтирование;
|
|
310
|
-
- `runCommand(target, input?, options?)` запускает существующий подлинный `Command` без монтирования React (D261);
|
|
311
|
-
- `testReadable(initial)`, `testResourceEpoch()`, `openModel`, `settled`, `waitFor`, `yieldTurn` и `eventually`
|
|
312
|
-
приходят из входов ниже — для той части теста, в которой UI нет; `testResourceEpoch()` это то, что фикстура
|
|
313
|
-
компонента, собирающая `ResourceState` руками, кладёт в её `epoch` (D362).
|
|
314
|
-
|
|
315
|
-
Тест, который ничего не рендерит, должен импортировать `@opetope/runtime/testing` напрямую: React здесь нужен
|
|
316
|
-
только `renderSlot` и привязочной половине `command`. Сам Command фикстуры чеканит `command` из
|
|
317
|
-
`@opetope/core/testing` (D300), и это единственное имя цепочки реэкспортов, которое насквозь не проходит:
|
|
318
|
-
React-`command` заслоняет его своим, более сильным.
|
|
319
|
-
|
|
320
|
-
Харнесс `renderSlot` отдаёт `Slot` и `updateProps`: корней и их фикстур больше нет, UI входит в приложение
|
|
321
|
-
вкладами (D85). Это не тестовое написание `<Slot>`: он монтирует фикстуру и принимает `reporter`, потому что у
|
|
322
|
-
компонентного теста нет фичи, которой можно сообщить, — и упакованный потребитель `ci:pack` монтирует его из
|
|
323
|
-
настоящего архива (D301).
|
|
324
|
-
|
|
325
|
-
Тест, который раньше монтировал `useCommand` только ради вызова Command, может использовать `runCommand` напрямую.
|
|
326
|
-
Результат — выход Command; тест UI по-прежнему проверяет `CommandOutcome`, возвращённый смонтированным consumer:
|
|
327
|
-
|
|
328
|
-
```ts
|
|
329
|
-
import assert from 'node:assert/strict';
|
|
330
|
-
import { defineModel } from '@opetope/core';
|
|
331
|
-
import type { Command, ModelCommandContext } from '@opetope/core';
|
|
332
|
-
import { defineFeature, openFeature } from '@opetope/runtime';
|
|
333
|
-
import { runCommand } from '@opetope/react/testing';
|
|
334
|
-
|
|
335
|
-
const Arithmetic = defineModel<{ readonly double: Command<number, number> }>('example.testing.model');
|
|
336
|
-
const arithmetic = defineFeature('example.testing.arithmetic', {
|
|
337
|
-
own: ({ model }) => ({
|
|
338
|
-
arithmetic: model(Arithmetic, context => ({
|
|
339
|
-
double: context.command(({ input }: ModelCommandContext<number>) => input * 2),
|
|
340
|
-
})),
|
|
341
|
-
}),
|
|
342
|
-
exports: ({ own }) => ({ double: own.arithmetic.double }),
|
|
343
|
-
});
|
|
344
|
-
const instance = openFeature(arithmetic, { imports: {}, reporter: () => undefined });
|
|
345
|
-
try {
|
|
346
|
-
const ready = await instance.ready;
|
|
347
|
-
const result = await runCommand(ready.exports.double, 3);
|
|
348
|
-
assert.equal(result, 6);
|
|
349
|
-
} finally {
|
|
350
|
-
await instance.close();
|
|
351
|
-
}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
`runCommand` возвращает `Promise<Output>` и сохраняет исходные ошибки и причины отклонения при отмене Command. Он не добавляет
|
|
355
|
-
UI-статус, обёртку outcome, политику выполнения, создание модели или отдельное время жизни для cleanup. У Command остаются
|
|
356
|
-
его владелец, lane и правила retirement; тест закрывает открытый экземпляр, в том числе при отказе readiness.
|
|
357
|
-
Для void Command достаточно `runCommand(target)`. Options всегда передаются третьим аргументом:
|
|
358
|
-
`runCommand(target, input, { signal })` или `runCommand(voidTarget, undefined, { signal })`.
|
|
359
|
-
`RunCommandOptions` содержит только необязательный `signal`; уже отменённый сигнал отклоняет вызов до входа в его тело.
|
|
360
|
-
|
|
361
|
-
`run`, `runCommand` и вложенный `invoke` используют одно правило входа (D273): `void` и `undefined` можно опустить;
|
|
362
|
-
остальные входы, включая `T | undefined`, требуют аргумент. `never` нельзя вызвать без входа.
|
|
363
|
-
Обобщённые функции могут передавать цель и её явный вход с сохранением типов.
|
|
364
|
-
|
|
365
|
-
## Карта слов и входы
|
|
366
|
-
|
|
367
|
-
| Вход | Что содержит |
|
|
368
|
-
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
369
|
-
| `@opetope/react` | `useModel`, `useCommand`, `useReadable`, `useSelector`, `useResource`, `requiresModels`, `defineSlot`, `defineSwitchSlot`, `Slot`, `ContributionError`; типы `SlotTarget`, `SwitchSlotTarget`, `SlotContribution`, `CommandHook`, `CommandOutcome`, `ResourceHook`, `PaginatedResourceHook`, `ResourcePaginationHook` |
|
|
370
|
-
| `@opetope/react/integration` | `FeatureBoundary`, `ContributionBoundary`, `useFeature`, `useFeatureRetry`, `FeatureBoundaryError` для integration-слоя; типы `FeatureBoundaryProps`, `ContributionBoundaryProps`, `ContributionErrorContent`, `ContributionFailure`, `FeatureDemandSource`, `FeatureDemandState`, `FeatureDemandResult`, `FeatureDemandLease` |
|
|
371
|
-
| `@opetope/react/testing` | `renderSlot`, `command`, `createScenario`, `ScenarioTimeoutError` и типы fixtures/scenarios плюс всё, что публикуют `@opetope/runtime/testing` и `@opetope/core/testing`; только для тестов |
|
|
372
|
-
|
|
373
|
-
`Command<Input, Output>` импортируется из `@opetope/core`, а UI-результат `CommandOutcome<Output>` — из `@opetope/react`. Прежний синоним `Command` и переэкспорт `CommandOutcome` из `@opetope/react/integration` удалены (D270).
|
|
374
|
-
|
|
375
|
-
`Model` — типизированный ключ записи из `Readable`, `Command` и фабрик readable, которую используют UI-компоненты.
|
|
376
|
-
Конструкторы integration-входа не переэкспортируются из безопасного входа и не могут вернуться в
|
|
377
|
-
`ui/models/data/contracts` через локальный barrel фичи.
|
|
378
|
-
|
|
379
|
-
## Законы
|
|
380
|
-
|
|
381
|
-
- `useModel(declaration)` читает только модели, объявленные вкладом;
|
|
382
|
-
- один закоммиченный вклад владеет своим кадром моделей до размонтирования или retire;
|
|
383
|
-
- брошенный concurrent-рендер кадр не удерживает;
|
|
384
|
-
- `useCommand` не подписывается на invoker: наблюдаемое состояние вызова лежит в `Readable` модели;
|
|
385
|
-
- исход `cancelled` не двигает `outcome`, и это только коды `CommandError` `cancelled`
|
|
386
|
-
и `closed`; вызов слабого порта без провайдера (`unavailable`) и отклонённая публикация
|
|
387
|
-
(`publication-rejected`) приходят исходом `failed` и оседают в `outcome` (D138, D292);
|
|
388
|
-
- вклад, упавший в рендере или коммите, изолируется своим монтированием и сообщается своей фиче;
|
|
389
|
-
- `useSelector` читает свой `equals` там, где выбранное значение уже известно, поэтому именованная необобщённая
|
|
390
|
-
функция компилируется рядом с выборкой, параметр которой выводится, — как в Core (D348);
|
|
391
|
-
- `useSelector` сохраняет выбранную ссылку, когда изменилось что-то другое; его тождественный случай это
|
|
392
|
-
`useReadable`, а случай поля модели — выборка `useModel`, поэтому доказывают его гейт памяти и упакованный
|
|
393
|
-
потребитель, а не пример спецификации, и он кандидат на вывод с этого входа до второго измеренного
|
|
394
|
-
приложения (D301);
|
|
395
|
-
- закрытие и размонтирование синхронно фенсят новые вызовы и освобождают ссылки кадра.
|
|
396
|
-
|
|
397
|
-
## Compatibility contract
|
|
398
|
-
|
|
399
|
-
ESM пакета собирается для Chrome 82+, Firefox 110+, Safari/iOS 15+, Android 82+ и Node 20.19+ и требует peer
|
|
400
|
-
`react >=19.0.0 <20` (D251). React и ReactDOM это peers хоста, а не встроенные полифилы. Сырые модули исполняет smoke импорта
|
|
401
|
-
по HTTP с локальным бандлом peer-ов; исторические сборки браузеров проверяет только внешняя ферма.
|