@opetope/react 0.10.1 → 0.12.0
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 +359 -17
- package/README.md +114 -71
- package/README.ru.md +60 -70
- 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/model-selection-snapshot.d.ts +20 -3
- 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 -5
- package/dist/contribution-frame-QRJSP2W3.js +0 -2
- package/dist/contribution-frame-QRJSP2W3.js.map +0 -1
package/README.md
CHANGED
|
@@ -19,12 +19,12 @@ No GitHub access is needed to read those installed guides.
|
|
|
19
19
|
|
|
20
20
|
```tsx
|
|
21
21
|
import { defineModel } from '@opetope/core';
|
|
22
|
-
import type {
|
|
22
|
+
import type { Command, Readable } from '@opetope/core';
|
|
23
23
|
import { defineSlot, requiresModels, Slot, useModel } from '@opetope/react';
|
|
24
24
|
|
|
25
25
|
const CounterModel = defineModel<{
|
|
26
26
|
readonly count: Readable<number>;
|
|
27
|
-
readonly increment:
|
|
27
|
+
readonly increment: Command<void, void>;
|
|
28
28
|
}>('example.counter.model');
|
|
29
29
|
const CounterSlot = defineSlot<{ readonly label: string }>('example.counter.slot');
|
|
30
30
|
|
|
@@ -39,7 +39,7 @@ const CounterButton = requiresModels([CounterModel])(({ label }: { readonly labe
|
|
|
39
39
|
|
|
40
40
|
return (
|
|
41
41
|
<button disabled={inFlight} onClick={() => void run()}>
|
|
42
|
-
{outcome?.
|
|
42
|
+
{outcome?.kind === 'failed' ? 'Retry' : `${label}: ${count}`}
|
|
43
43
|
</button>
|
|
44
44
|
);
|
|
45
45
|
});
|
|
@@ -57,10 +57,10 @@ rejection (D116, D292).
|
|
|
57
57
|
`outcome` is the last settled non-cancelled outcome of this consumer: `ok` takes the place of a previous `failed`
|
|
58
58
|
and a `failed` the place of a previous `ok`, a `cancelled` outcome moves nothing, and starting a run clears nothing,
|
|
59
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?.
|
|
60
|
+
its discriminant — `outcome?.kind === 'failed' ? outcome.error : null`.
|
|
61
61
|
|
|
62
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
|
|
63
|
+
keeps the React event out of the Command and the promise from hanging; `onClick={logout.run}` does not compile, because
|
|
64
64
|
a `MouseEvent` is not a `void` input. Use `run(input, options?)` for data, an outcome, callbacks or a per-run signal.
|
|
65
65
|
Where a lint config bans an inline arrow prop (`react-perf/jsx-no-new-function-as-prop`, `react/jsx-no-bind`), hoist
|
|
66
66
|
it with `useCallback(() => void logout.run(), [logout.run])`: the dependency is `run`, never the hook object (D290).
|
|
@@ -70,39 +70,25 @@ changes as `inFlight` or `outcome` changes, so an effect or a memo depends on `r
|
|
|
70
70
|
`opetope/no-command-in-deps` holds this for the author (D290). A `run` captured from a replaced or unmounted
|
|
71
71
|
consumer is fenced; it does not start work on the replacement.
|
|
72
72
|
|
|
73
|
-
The hook schedules nothing. Every `run` reaches the
|
|
74
|
-
happens to it (D203). For an absolute value setter the model declares `
|
|
75
|
-
newest input then replaces the waiting one there (D185). What the consumer keeps
|
|
76
|
-
input signal is refused as `cancelled` without reaching the
|
|
77
|
-
way, `inFlight` is true while any run this consumer started is unsettled,
|
|
78
|
-
and signal.
|
|
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
79
|
|
|
80
80
|
To share equivalent pending or running work, the model declares `dedupe: true` or `dedupe: input => key`
|
|
81
|
-
on `context.
|
|
82
|
-
be combined with `latest` (D277). These are
|
|
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
83
|
|
|
84
|
-
For several commands
|
|
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).
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
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.
|
|
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.
|
|
106
92
|
|
|
107
93
|
## Selecting data and commands
|
|
108
94
|
|
|
@@ -110,10 +96,22 @@ need one semantic command. Only own enumerable properties are selected, includin
|
|
|
110
96
|
|
|
111
97
|
A model selection can use a named interface without an index signature (D217). Its result is a flat data record;
|
|
112
98
|
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
|
|
114
|
-
fields become the same `CommandHook` as `
|
|
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`.
|
|
115
101
|
A single selected command needs no nested hooks:
|
|
116
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
|
+
|
|
117
115
|
```tsx
|
|
118
116
|
const { logout } = useModel(AuthModel, auth => ({ logout: auth.logout }));
|
|
119
117
|
return (
|
|
@@ -136,13 +134,26 @@ values — that field answers a `ResourceHook`, and one lease per Resource ident
|
|
|
136
134
|
keys name one Resource (D321, D359) — and nothing else: a
|
|
137
135
|
`read(resource.state)` leases nothing, and neither does the one-argument form. Combining hooks does not promise fewer
|
|
138
136
|
source subscriptions or faster renders; a raw `useModel` import now also includes the selection implementation.
|
|
139
|
-
There is no additional
|
|
137
|
+
There is no additional Command scheduler.
|
|
140
138
|
|
|
141
139
|
## Application host
|
|
142
140
|
|
|
143
141
|
The host opens a feature graph with `openApplication`. Ready features publish their UI contributions atomically;
|
|
144
142
|
the ordinary `Slot` mounts them into consumer-owned targets:
|
|
145
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
|
+
|
|
146
157
|
```tsx
|
|
147
158
|
<Slot target={exampleFooterSlot} />
|
|
148
159
|
<Slot target={exampleSettingsSlot} />
|
|
@@ -152,6 +163,26 @@ The application owns feature lifetimes; each feature's UI mounts through its con
|
|
|
152
163
|
|
|
153
164
|
## Feature demand boundary
|
|
154
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
|
+
|
|
155
186
|
```tsx
|
|
156
187
|
import { Slot } from '@opetope/react';
|
|
157
188
|
import { FeatureBoundary, useFeatureRetry } from '@opetope/react/integration';
|
|
@@ -211,27 +242,30 @@ frame of the instance that gave the contribution. The order and the stable key c
|
|
|
211
242
|
React introduces neither a second comparator nor a second lifetime registry. The packaged consumer of `ci:pack`
|
|
212
243
|
exercises that caching and routes one contribution through it (D301).
|
|
213
244
|
|
|
214
|
-
`useResource(resource)` answers `{
|
|
245
|
+
`useResource(resource)` answers `{ pagination, refresh, reset, retry, state }`: the `ResourceState` through
|
|
215
246
|
`useSyncExternalStore`, the lease `resource.acquire()` answers taken in a commit effect for as long as the component
|
|
216
247
|
is mounted, and the three verbs as ordinary `CommandHook` consumers (D359). An interrupted render therefore opens
|
|
217
248
|
nothing, and when the reference changes the state follows the new Resource while the effect cleanup releases the
|
|
218
249
|
previous lease. The number of React hooks does not depend on the Resource: a declaration without `pagination`
|
|
219
250
|
subscribes to a constant source and keeps a `loadNext` that answers `skipped`, while a declaration that carried the
|
|
220
251
|
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 `
|
|
252
|
+
Resource answers the same hook and takes the same lease, so a model declares no `Command<void, void>` around
|
|
222
253
|
`resource.refresh()` to give a button its `inFlight`. `useReadable(resource)` is a compile error, because a Resource
|
|
223
254
|
is not a `Readable`; `useReadable(resource.state)` and a `read(resource.state)` in a selector stay passive and lease
|
|
224
255
|
nothing — observing and holding are independent questions, so a selector that reads the state and a `useResource`
|
|
225
256
|
beside it subscribe twice (D349). The compile-checked example of specification §2.13 shows both sides of that
|
|
226
257
|
(D301, D321).
|
|
227
258
|
|
|
228
|
-
`refresh.run()`, `retry.run()` and `
|
|
259
|
+
`refresh.run()`, `retry.run()` and `reset.run()` reach the Resource, which is owned by the model that declared
|
|
229
260
|
it: `inFlight` is true until the outcome of the operation settles, `outcome` carries that `ResourceOperationOutcome`
|
|
230
261
|
as the `value` of an `ok` status, and `run(undefined, { signal })` cancels the wait of this consumer and not the work
|
|
231
262
|
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
|
|
233
|
-
|
|
234
|
-
|
|
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.
|
|
235
269
|
|
|
236
270
|
Direct component props, a contribution's `props` adapter and its model's props `Readable` share the same
|
|
237
271
|
mount-owned snapshot. Incoming slot props publish in layout before paint, so direct and adapted renders cannot
|
|
@@ -248,6 +282,14 @@ Every mount is an error boundary of its own contribution (D256). A render or com
|
|
|
248
282
|
reported to the feature that published the contribution, and leaves the other mounts of the target untouched.
|
|
249
283
|
`ContributionBoundary` states once, above every slot, what a failed mount shows:
|
|
250
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
|
+
|
|
251
293
|
```tsx
|
|
252
294
|
import { ContributionBoundary } from '@opetope/react/integration';
|
|
253
295
|
|
|
@@ -273,7 +315,7 @@ dependency. `scenario.mount(target, { props })` uses the published Slot contribu
|
|
|
273
315
|
while targets without props omit it, exactly as with `Slot` (D217). Fixture commands do not bypass authority.
|
|
274
316
|
|
|
275
317
|
As with `openApplication`, omit `conditions` when no enabled feature requires a host-bound condition. Conditions
|
|
276
|
-
computed through `
|
|
318
|
+
computed through `source`/`select` bind automatically; external conditions still require their sources (D271).
|
|
277
319
|
|
|
278
320
|
The synchronous constructor exposes `ready`, so a test can inspect a pending lazy body before readiness.
|
|
279
321
|
`waitFor(snapshot => predicate, { label, timeoutMs, pollIntervalMs })` wakes on inspection changes and also polls
|
|
@@ -292,17 +334,18 @@ a scoped zero-count assertion, without proving absence of arbitrary host, UI or
|
|
|
292
334
|
application imports and internal renderer references. A failed cleanup promise can retain original errors and retry
|
|
293
335
|
capabilities; a caller that keeps `mounted.host` also keeps its own renderer result.
|
|
294
336
|
|
|
295
|
-
The inspection schemas are `opetope.devtools-graph/5` and `opetope.devtools-frame/
|
|
337
|
+
The inspection schemas are `opetope.devtools-graph/5` and `opetope.devtools-frame/5`, with optional
|
|
296
338
|
`opetope.runtime-activity/4` snapshots (D266, D275, D344, D358). Within one session,
|
|
297
339
|
a frame without `activity` preserves the previous activity; a full snapshot/reset without it clears that observation
|
|
298
340
|
(D216). Activity-bearing frames replace the previous activity in full.
|
|
299
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,
|
|
300
|
-
actual feature generation, physical
|
|
342
|
+
actual feature generation, physical calls, exact current lane blockers, and for every registered Resource its
|
|
301
343
|
`kind`, `lifetime`, `epoch`, `state`, `activity` with its `operation`, `leases`, `subscribers`, `pagination` and the
|
|
302
344
|
`epoch` of each physical load — there is no `attempt` and no numeric generation, because a Resource runs one logical
|
|
303
345
|
attempt and recovery belongs to the transport (D358). Host demand and UI models are unknown. `freshness`
|
|
304
346
|
and `truncated` distinguish a complete live view from a partial or detached one. A closed session is stale;
|
|
305
|
-
`closed: true`
|
|
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.
|
|
306
349
|
Activity output is bounded by record capacity. Snapshot collection still visits registered owners, executors and
|
|
307
350
|
resources, so capacity does not bound traversal cost. Collection stops once truncation is proven;
|
|
308
351
|
idle executors may still require traversal to establish completeness. Normal call dispatch allocates no diagnostic record with
|
|
@@ -315,14 +358,14 @@ observation disabled. Graph frames remain bounded by the existing ring capacity.
|
|
|
315
358
|
(D285):
|
|
316
359
|
|
|
317
360
|
- `renderSlot(target, { contribution, models, props })` mounts the published target or one fixture contribution;
|
|
318
|
-
- `command(run)` produces an authentic `
|
|
319
|
-
- `
|
|
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);
|
|
320
363
|
- `testReadable(initial)`, `testResourceEpoch()`, `openModel`, `settled`, `waitFor`, `yieldTurn` and `eventually`
|
|
321
364
|
come from the entries below, for the part of a test that has no UI in it — `testResourceEpoch()` is what a
|
|
322
365
|
component fixture writing a `ResourceState` by hand puts in its `epoch` (D362).
|
|
323
366
|
|
|
324
367
|
A test that renders nothing should import `@opetope/runtime/testing` directly: `renderSlot` and the binding half of
|
|
325
|
-
`command` are all that needs React here. The
|
|
368
|
+
`command` are all that needs React here. The Command a fixture mints is the `command` of `@opetope/core/testing`
|
|
326
369
|
(D300), and this word is the one name of the re-export chain that does not pass through: the React `command`
|
|
327
370
|
shadows it with the stronger one.
|
|
328
371
|
|
|
@@ -331,21 +374,21 @@ application as contributions (D85). It is not a test spelling of `<Slot>` — it
|
|
|
331
374
|
`reporter`, because a component test has no feature to report to — and the packaged consumer of `ci:pack` mounts it
|
|
332
375
|
from the real archive (D301).
|
|
333
376
|
|
|
334
|
-
A test that previously mounted `useCommand` only to invoke a
|
|
335
|
-
|
|
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:
|
|
336
379
|
|
|
337
380
|
```ts
|
|
338
381
|
import assert from 'node:assert/strict';
|
|
339
382
|
import { defineModel } from '@opetope/core';
|
|
340
|
-
import type {
|
|
383
|
+
import type { Command, ModelCommandContext } from '@opetope/core';
|
|
341
384
|
import { defineFeature, openFeature } from '@opetope/runtime';
|
|
342
|
-
import {
|
|
385
|
+
import { runCommand } from '@opetope/react/testing';
|
|
343
386
|
|
|
344
|
-
const Arithmetic = defineModel<{ readonly double:
|
|
387
|
+
const Arithmetic = defineModel<{ readonly double: Command<number, number> }>('example.testing.model');
|
|
345
388
|
const arithmetic = defineFeature('example.testing.arithmetic', {
|
|
346
389
|
own: ({ model }) => ({
|
|
347
390
|
arithmetic: model(Arithmetic, context => ({
|
|
348
|
-
double: context.
|
|
391
|
+
double: context.command(({ input }: ModelCommandContext<number>) => input * 2),
|
|
349
392
|
})),
|
|
350
393
|
}),
|
|
351
394
|
exports: ({ own }) => ({ double: own.arithmetic.double }),
|
|
@@ -353,35 +396,35 @@ const arithmetic = defineFeature('example.testing.arithmetic', {
|
|
|
353
396
|
const instance = openFeature(arithmetic, { imports: {}, reporter: () => undefined });
|
|
354
397
|
try {
|
|
355
398
|
const ready = await instance.ready;
|
|
356
|
-
const result = await
|
|
399
|
+
const result = await runCommand(ready.exports.double, 3);
|
|
357
400
|
assert.equal(result, 6);
|
|
358
401
|
} finally {
|
|
359
402
|
await instance.close();
|
|
360
403
|
}
|
|
361
404
|
```
|
|
362
405
|
|
|
363
|
-
`
|
|
364
|
-
UI status or outcome wrapper, scheduling policy, model creation or cleanup lifetime. The existing
|
|
406
|
+
`runCommand` returns `Promise<Output>` and preserves the Command's original errors and cancellation rejections. It adds no
|
|
407
|
+
UI status or outcome wrapper, scheduling policy, model creation or cleanup lifetime. The existing Command retains its
|
|
365
408
|
owner, lane and retirement rules; the test closes the instance it opened, including after failed readiness.
|
|
366
|
-
A void
|
|
367
|
-
`
|
|
368
|
-
`
|
|
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.
|
|
369
412
|
|
|
370
|
-
`run`, `
|
|
413
|
+
`run`, `runCommand` and nested `invoke` use the same no-input rule (D273): `void` and `undefined` allow omission;
|
|
371
414
|
other inputs, including `T | undefined`, require an argument. `never` cannot be invoked without input.
|
|
372
415
|
Generic helpers may forward the target and its explicit input without losing their types.
|
|
373
416
|
|
|
374
417
|
## Word map and entries
|
|
375
418
|
|
|
376
|
-
| Entry | What it holds
|
|
377
|
-
| ---------------------------- |
|
|
378
|
-
| `@opetope/react` | `useModel`, `useCommand`, `
|
|
379
|
-
| `@opetope/react/integration` | `FeatureBoundary`, `ContributionBoundary`, `useFeature`, `useFeatureRetry`, `FeatureBoundaryError` for the integration layer; types `FeatureBoundaryProps`, `ContributionBoundaryProps`, `ContributionErrorContent`, `ContributionFailure`, `FeatureDemandSource`, `FeatureDemandState`, `FeatureDemandResult`, `FeatureDemandLease`
|
|
380
|
-
| `@opetope/react/testing` | `renderSlot`, `command`, `createScenario`, `ScenarioTimeoutError` and fixture/scenario types, plus everything `@opetope/runtime/testing` and `@opetope/core/testing` publish; tests only
|
|
419
|
+
| Entry | What it holds |
|
|
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 |
|
|
381
424
|
|
|
382
|
-
`
|
|
425
|
+
`Command<Input, Output>` is imported from `@opetope/core`; UI `CommandOutcome<Output>` is imported from `@opetope/react`. The former `Command` synonym and the `CommandOutcome` re-export from `@opetope/react/integration` are removed (D270).
|
|
383
426
|
|
|
384
|
-
`Model` is a typed key for a record of `Readable`, `
|
|
427
|
+
`Model` is a typed key for a record of `Readable`, `Command` and readable factories that UI components consume.
|
|
385
428
|
The constructors of the integration entry are not re-exported from the safe entry and cannot come back into
|
|
386
429
|
`ui/models/data/contracts` through a local barrel of a feature.
|
|
387
430
|
|
|
@@ -391,7 +434,7 @@ The constructors of the integration entry are not re-exported from the safe entr
|
|
|
391
434
|
- one committed contribution owns its model frame until unmount or retire;
|
|
392
435
|
- an abandoned concurrent render holds no frame;
|
|
393
436
|
- `useCommand` does not subscribe to the invoker: the observable state of a call lies in a `Readable` of the model;
|
|
394
|
-
- the `cancelled` outcome does not move `outcome`, and it is only the `
|
|
437
|
+
- the `cancelled` outcome does not move `outcome`, and it is only the `CommandError` codes `cancelled`
|
|
395
438
|
and `closed`; a call to a weak port with no provider (`unavailable`) and a rejected publication
|
|
396
439
|
(`publication-rejected`) arrive as the `failed` outcome and settle in `outcome` (D138, D292);
|
|
397
440
|
- a contribution that fails to render or to commit is contained by its own mount and reported to its feature;
|