@opetope/react 0.2.0 → 0.4.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 +46 -0
- package/README.md +25 -5
- package/README.ru.md +25 -5
- package/dist/contribution-frame-B1PnpOaP.js +2 -0
- package/dist/contribution-frame-B1PnpOaP.js.map +1 -0
- package/dist/contribution-frame.d.ts +14 -6
- package/dist/contribution-isolation-Bzfbt4iM.js +2 -0
- package/dist/contribution-isolation-Bzfbt4iM.js.map +1 -0
- package/dist/contribution-isolation.d.ts +64 -0
- package/dist/errors.d.ts +20 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integration.d.ts +2 -0
- package/dist/integration.js +1 -1
- package/dist/integration.js.map +1 -1
- package/dist/scenario-slot.d.ts +5 -0
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/package.json +5 -5
- package/dist/contribution-frame-1td5XTES.js +0 -2
- package/dist/contribution-frame-1td5XTES.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# @opetope/react
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 23ca271: Let a contained failure name itself (D257).
|
|
8
|
+
|
|
9
|
+
A `ContributionFailure` now carries `contribution`, the published entry `<feature>.<provides key>`, `target`, the slot
|
|
10
|
+
it rendered in, and `feature`, the publishing feature or `undefined` for a test fixture. The error content of
|
|
11
|
+
`ContributionBoundary` receives the three next to the raw error and its retry, so one branch declared above every slot
|
|
12
|
+
can answer by surface without naming every contribution.
|
|
13
|
+
|
|
14
|
+
One application reporter serves every feature, so the diagnostic carries the same identity: the mount reports a
|
|
15
|
+
`ContributionError` whose `cause` is the original error, with the new codes `render-failed` for the contribution and
|
|
16
|
+
`error-content-failed` for error content that failed in its place. The authority of a generation carries the id of its
|
|
17
|
+
feature to make that possible.
|
|
18
|
+
|
|
19
|
+
A contained failure that was a cancellation stays one: the wrapper carries the same brand, so `isCancellation` answers
|
|
20
|
+
as it did before containment.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [23ca271]
|
|
25
|
+
- @opetope/runtime@0.4.0
|
|
26
|
+
- @opetope/core@0.4.0
|
|
27
|
+
|
|
28
|
+
## 0.3.0
|
|
29
|
+
|
|
30
|
+
### Minor Changes
|
|
31
|
+
|
|
32
|
+
- b15f15d: Contain a failed contribution in its own mount (D256).
|
|
33
|
+
|
|
34
|
+
A contribution whose render or commit throws no longer reaches the host's error boundary: the mount catches it,
|
|
35
|
+
reports it to the feature that published the contribution, and keeps the other mounts of the target rendering.
|
|
36
|
+
`ContributionBoundary` from `@opetope/react/integration` declares once, above every slot, what a failed mount shows;
|
|
37
|
+
its `error` takes a node or a `({ error, retry }) => ReactNode` callback, and `retry` remounts the contribution so its
|
|
38
|
+
models are created again. Error content that throws is contained by a second boundary instead of escalating.
|
|
39
|
+
|
|
40
|
+
The authority of a generation now carries the reporter of its feature, which is also the reporter the UI models of a
|
|
41
|
+
mount open with, and `renderSlot` accepts a `reporter` so a component test can assert a contained failure.
|
|
42
|
+
|
|
43
|
+
### Patch Changes
|
|
44
|
+
|
|
45
|
+
- Updated dependencies [b15f15d]
|
|
46
|
+
- @opetope/runtime@0.3.0
|
|
47
|
+
- @opetope/core@0.3.0
|
|
48
|
+
|
|
3
49
|
## 0.2.0
|
|
4
50
|
|
|
5
51
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -186,6 +186,25 @@ the layout effects (D209).
|
|
|
186
186
|
Demand retry is scoped to the source identity. Replacing a boundary's demand source allows its new retry to run
|
|
187
187
|
even if the previous source's retry is still pending.
|
|
188
188
|
|
|
189
|
+
Every mount is an error boundary of its own contribution (D256). A render or commit that throws stops there, is
|
|
190
|
+
reported to the feature that published the contribution, and leaves the other mounts of the target untouched.
|
|
191
|
+
`ContributionBoundary` states once, above every slot, what a failed mount shows:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { ContributionBoundary } from '@opetope/react/integration';
|
|
195
|
+
|
|
196
|
+
<ContributionBoundary error={({ error, retry }) => <FailedContribution error={error} onRetry={retry} />}>
|
|
197
|
+
<Slot target={applicationSurface} />
|
|
198
|
+
</ContributionBoundary>;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`error` takes a node or a callback of `{ contribution, error, feature, retry, target }`, the failure shape of
|
|
202
|
+
`FeatureBoundary` plus the identity of what failed: `contribution` is the published entry `<feature>.<provides key>`
|
|
203
|
+
and `target` is the slot, so one branch can answer by surface. `retry` remounts the contribution, so its models are
|
|
204
|
+
created again. The reporter of the publishing feature receives the same identity on a `ContributionError` whose
|
|
205
|
+
`cause` is the original error, with code `render-failed` or `error-content-failed`. Without the provider a failed mount renders nothing — containment
|
|
206
|
+
never depends on it. Error content that throws is contained the same way and reported, never escalated.
|
|
207
|
+
|
|
189
208
|
## Scenario tests and physical activity
|
|
190
209
|
|
|
191
210
|
`createScenario(application, options)` from `@opetope/react/testing` opens the real application and its existing
|
|
@@ -237,11 +256,11 @@ application as contributions (D85).
|
|
|
237
256
|
|
|
238
257
|
## Word map and entries
|
|
239
258
|
|
|
240
|
-
| Entry | What it holds
|
|
241
|
-
| ---------------------------- |
|
|
242
|
-
| `@opetope/react` | `useModel`, `useCommand`, `useCommands`, `useReadable`, `useSelector`, `useResource`, `requiresModels`, `defineSlot`, `defineSwitchSlot`, `Slot`, `ContributionError`; types `SlotTarget`, `SwitchSlotTarget`, `SlotContribution`, `CommandHook`, `CommandOutcome`
|
|
243
|
-
| `@opetope/react/integration` | `FeatureBoundary`, `useFeature`, `useFeatureRetry`, `FeatureBoundaryError` for the integration layer; types `FeatureBoundaryProps`, `FeatureDemandSource`, `FeatureDemandState`, `FeatureDemandResult`, `FeatureDemandLease`
|
|
244
|
-
| `@opetope/react/testing` | `renderSlot`, `command`, `createScenario`, `ScenarioTimeoutError` and fixture/scenario types; tests only
|
|
259
|
+
| Entry | What it holds |
|
|
260
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
261
|
+
| `@opetope/react` | `useModel`, `useCommand`, `useCommands`, `useReadable`, `useSelector`, `useResource`, `requiresModels`, `defineSlot`, `defineSwitchSlot`, `Slot`, `ContributionError`; types `SlotTarget`, `SwitchSlotTarget`, `SlotContribution`, `CommandHook`, `CommandOutcome` |
|
|
262
|
+
| `@opetope/react/integration` | `FeatureBoundary`, `ContributionBoundary`, `useFeature`, `useFeatureRetry`, `FeatureBoundaryError` for the integration layer; types `FeatureBoundaryProps`, `ContributionBoundaryProps`, `ContributionErrorContent`, `ContributionFailure`, `FeatureDemandSource`, `FeatureDemandState`, `FeatureDemandResult`, `FeatureDemandLease` |
|
|
263
|
+
| `@opetope/react/testing` | `renderSlot`, `command`, `createScenario`, `ScenarioTimeoutError` and fixture/scenario types; tests only |
|
|
245
264
|
|
|
246
265
|
`Model` is a typed key for a record of `Readable`, `Call` and readable factories that UI components consume.
|
|
247
266
|
The constructors of the integration entry are not re-exported from the safe entry and cannot come back into
|
|
@@ -256,6 +275,7 @@ The constructors of the integration entry are not re-exported from the safe entr
|
|
|
256
275
|
- the `cancelled` outcome moves neither `result` nor `lastError`, and it is only the `CallError` codes `cancelled`
|
|
257
276
|
and `closed`; a call to a weak port with no provider (`unavailable`) and a rejected publication
|
|
258
277
|
(`publication-rejected`) arrive as the `failed` outcome and settle in `lastError` (D138);
|
|
278
|
+
- a contribution that fails to render or to commit is contained by its own mount and reported to its feature;
|
|
259
279
|
- `useSelector` keeps the selected reference when something else changed;
|
|
260
280
|
- closing and unmounting synchronously fence new calls and release the references of the frame.
|
|
261
281
|
|
package/README.ru.md
CHANGED
|
@@ -185,6 +185,25 @@ React не вводит второй компаратор и второй рее
|
|
|
185
185
|
Retry спроса привязан к источнику. После замены источника boundary новый retry может начаться, даже если retry
|
|
186
186
|
старого ещё не завершён.
|
|
187
187
|
|
|
188
|
+
Каждое монтирование — граница ошибок своего вклада (D256). Сбой рендера или коммита останавливается на нём, уходит в
|
|
189
|
+
reporter опубликовавшей вклад фичи и не трогает остальные монтирования цели. `ContributionBoundary` один раз, над
|
|
190
|
+
всеми слотами, говорит, что показывает упавшее монтирование:
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import { ContributionBoundary } from '@opetope/react/integration';
|
|
194
|
+
|
|
195
|
+
<ContributionBoundary error={({ error, retry }) => <FailedContribution error={error} onRetry={retry} />}>
|
|
196
|
+
<Slot target={applicationSurface} />
|
|
197
|
+
</ContributionBoundary>;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`error` принимает узел или колбэк `{ contribution, error, feature, retry, target }` — форму отказа `FeatureBoundary`
|
|
201
|
+
плюс идентичность упавшего: `contribution` — опубликованная запись `<фича>.<ключ provides>`, `target` — слот, поэтому
|
|
202
|
+
одна ветвь может отвечать по поверхности. `retry` перемонтирует вклад, поэтому его модели создаются заново. Reporter
|
|
203
|
+
опубликовавшей фичи получает ту же идентичность на `ContributionError`, у которого в `cause` исходная ошибка, с кодом
|
|
204
|
+
`render-failed` или `error-content-failed`. Без провайдера упавшее монтирование рендерит пустоту:
|
|
205
|
+
изоляция от него не зависит. Упавшее содержимое ошибки изолируется так же и сообщается, а не эскалируется.
|
|
206
|
+
|
|
188
207
|
## Сценарные тесты и физическая активность
|
|
189
208
|
|
|
190
209
|
`createScenario(application, options)` из `@opetope/react/testing` открывает настоящее приложение и его существующую
|
|
@@ -237,11 +256,11 @@ idle executors могут требовать обхода, чтобы подтв
|
|
|
237
256
|
|
|
238
257
|
## Карта слов и входы
|
|
239
258
|
|
|
240
|
-
| Вход | Что содержит
|
|
241
|
-
| ---------------------------- |
|
|
242
|
-
| `@opetope/react` | `useModel`, `useCommand`, `useCommands`, `useReadable`, `useSelector`, `useResource`, `requiresModels`, `defineSlot`, `defineSwitchSlot`, `Slot`, `ContributionError`; типы `SlotTarget`, `SwitchSlotTarget`, `SlotContribution`, `CommandHook`, `CommandOutcome`
|
|
243
|
-
| `@opetope/react/integration` | `FeatureBoundary`, `useFeature`, `useFeatureRetry`, `FeatureBoundaryError` для integration-слоя; типы `FeatureBoundaryProps`, `FeatureDemandSource`, `FeatureDemandState`, `FeatureDemandResult`, `FeatureDemandLease`
|
|
244
|
-
| `@opetope/react/testing` | `renderSlot`, `command`, `createScenario`, `ScenarioTimeoutError` и типы fixtures/scenarios; только для тестов
|
|
259
|
+
| Вход | Что содержит |
|
|
260
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
261
|
+
| `@opetope/react` | `useModel`, `useCommand`, `useCommands`, `useReadable`, `useSelector`, `useResource`, `requiresModels`, `defineSlot`, `defineSwitchSlot`, `Slot`, `ContributionError`; типы `SlotTarget`, `SwitchSlotTarget`, `SlotContribution`, `CommandHook`, `CommandOutcome` |
|
|
262
|
+
| `@opetope/react/integration` | `FeatureBoundary`, `ContributionBoundary`, `useFeature`, `useFeatureRetry`, `FeatureBoundaryError` для integration-слоя; типы `FeatureBoundaryProps`, `ContributionBoundaryProps`, `ContributionErrorContent`, `ContributionFailure`, `FeatureDemandSource`, `FeatureDemandState`, `FeatureDemandResult`, `FeatureDemandLease` |
|
|
263
|
+
| `@opetope/react/testing` | `renderSlot`, `command`, `createScenario`, `ScenarioTimeoutError` и типы fixtures/scenarios; только для тестов |
|
|
245
264
|
|
|
246
265
|
`Model` — типизированный ключ записи из `Readable`, `Call` и фабрик readable, которую используют UI-компоненты.
|
|
247
266
|
Конструкторы integration-входа не переэкспортируются из безопасного входа и не могут вернуться в
|
|
@@ -256,6 +275,7 @@ idle executors могут требовать обхода, чтобы подтв
|
|
|
256
275
|
- исход `cancelled` не двигает ни `result`, ни `lastError`, и это только коды `CallError` `cancelled`
|
|
257
276
|
и `closed`; вызов слабого порта без провайдера (`unavailable`) и отклонённая публикация
|
|
258
277
|
(`publication-rejected`) приходят исходом `failed` и оседают в `lastError` (D138);
|
|
278
|
+
- вклад, упавший в рендере или коммите, изолируется своим монтированием и сообщается своей фиче;
|
|
259
279
|
- `useSelector` сохраняет выбранную ссылку, когда изменилось что-то другое;
|
|
260
280
|
- закрытие и размонтирование синхронно фенсят новые вызовы и освобождают ссылки кадра.
|
|
261
281
|
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{jsx as m}from"react/jsx-runtime";import{useContext as B,createContext as E,useCallback as v,useRef as M,useMemo as b,useSyncExternalStore as y,useEffect as L,useInsertionEffect as R,useLayoutEffect as p,memo as T,useState as q}from"react";import{isCallTarget as D,isModel as V,isReadable as S,createLifetimeController as P,createCallController as W,reportDetachedError as g,getContributionOwner as K,createState as G,createAggregateError as H}from"@opetope/core/internal";import{reportRuntimeFailure as J,requireContributionModelPlans as N,openContributionModels as Q}from"@opetope/runtime/internal";import{C as u,u as U,b as X}from"./contribution-isolation-Bzfbt4iM.js";const O=E(void 0);function Y(){const t=B(O);if(t===void 0)throw new u("missing","Opetope hook was called outside a contribution mount.");return t}const Z=t=>t;function A(t,e,n=Object.is){const r=v(a=>t.subscribe(a),[t]),i=M(void 0);let o=i.current;o===void 0&&(o={hasValue:!1,value:void 0},i.current=o);const d=b(()=>{let a=!1,l,c;return()=>{const f=t.getSnapshot();if(a&&Object.is(l,f))return c;const h=e(f);if(!a){let w=h;return o.hasValue&&n(o.value,h)&&(w=o.value),a=!0,l=f,c=w,c}return n(c,h)?(l=f,c):(l=f,c=h,c)}},[o,n,t,e]),s=y(r,d,d);return L(()=>{o.hasValue=!0,o.value=s},[o,s]),s}function j(t){return A(t,Z)}const C=new WeakMap;function F(t){if(arguments.length>1)throw new u("binding-invalid","Command follows the target it binds; binding options are forbidden.");if(!D(t))throw new u("binding-invalid","Command target is not authentic.");return C.set(t,{target:t}),t}function _(t){const e=C.get(t);if(e===void 0)throw new u("binding-invalid","Command binding is not authentic.");return e}function k(t){return typeof t=="object"&&t!==null&&C.has(t)}const ee=Symbol("opetope.model-binding"),te=Symbol("opetope.model-binding-identity"),ne=Object.freeze({}),x=new WeakMap;function ie(t,e,n){if(typeof n!="string")throw new u("binding-invalid",`Model ${t.id} accepts only string fields.`);const r=Object.getOwnPropertyDescriptor(e,n);if((r==null?void 0:r.enumerable)!==!0||!("value"in r))throw new u("binding-invalid",`Model ${t.id}.${n} requires an enumerable data binding.`);return r.value}function re(t,e){const n=[],r=Reflect.ownKeys(e);if(r.length===0)throw new u("binding-invalid",`Model ${t.id} requires at least one field.`);for(const i of r){const o=ie(t,e,i);if(!(S(o)||typeof o=="function")){if(!k(o))throw new u("binding-invalid",`Model ${t.id}.${String(i)} requires a Readable or a bound command.`);n.push(o)}}return Object.freeze(n)}function z(t,e){if(!V(t))throw new u("binding-invalid","Model declaration is not authentic.");if(typeof e!="object"||e===null)throw new u("binding-invalid",`Model ${t.id} requires a record binding.`);const n=re(t,e),r=Object.freeze({[ee]:ne,[te]:!0});return x.set(r,{commands:n,declaration:t,model:e}),r}function oe(t){const e=x.get(t);if(e===void 0)throw new u("binding-invalid","Model binding is not authentic.");return e}let se=0;function ae(t){const e=new Map,n=[],r=new Set;for(const i of t){const o=oe(i);if(e.has(o.declaration))throw new u("duplicate",`Model ${o.declaration.id} is bound twice in one mount.`);e.set(o.declaration,o.model);for(const d of o.commands)r.has(d)||(r.add(d),n.push(d))}return{commands:Object.freeze(n),frame:{commandRecords:new Map,models:e}}}class ce{frame;authority;boundCommands;closeTicket=0;isActive;lifetime;onClose;released=!1;rendererAttached=!1;renderListeners=new Set;state="detached";constructor(e,n){const r=++se;this.lifetime=P({id:`contribution.${String(r)}`}),this.isActive=n==null?void 0:n.isActive,this.onClose=n==null?void 0:n.onClose;const{commands:i,frame:o}=ae(e);this.frame=o,this.boundCommands=i,this.buildCommandRecords(r)}attach(){if(this.state==="closing")return this.closeTicket+=1,this.state="active",this.rendererAttached=!0,!0;if(this.state==="closed")return!1;if(this.state==="active")throw new u("duplicate","One contribution mount cannot mount twice.");const e=this.createAuthority(this.lifetime.open());try{for(const n of this.frame.commandRecords.values())n.controller.activate()}catch(n){throw this.authority=e,this.state="active",this.close(),n}return this.authority=e,this.rendererAttached=!0,this.state="active",!0}close(){if(this.state==="closed")return;this.state="closed",this.closeTicket+=1,this.lifetime.close(),this.authority=void 0;for(const n of this.frame.commandRecords.values())n.controller.close();const e=this.onClose;this.onClose=void 0,e==null||e(),this.publishRenderable(),this.rendererAttached||this.releaseFrame()}detach(){if(!this.rendererAttached)return;if(this.rendererAttached=!1,this.state==="closed"){this.releaseFrame();return}if(this.state!=="active")return;this.state="closing";const e=++this.closeTicket;queueMicrotask(()=>{this.state==="closing"&&e===this.closeTicket&&this.close()})}diagnostics(){const e=[...this.frame.commandRecords.values()].map(n=>n.controller.diagnostics());return Object.freeze({active:this.state==="active",calls:Object.freeze(e),commandRecords:this.frame.commandRecords.size,models:this.frame.models.size})}getRenderableSnapshot=()=>this.state!=="closed"&&(this.isActive===void 0||this.isActive());releaseClosedFrame(){this.state==="closed"&&this.releaseFrame()}subscribeRenderable=e=>{if(this.state==="closed")return()=>{};const n={listener:e};this.renderListeners.add(n);let r=!0;return()=>{r&&(r=!1,this.renderListeners.delete(n))}};buildCommandRecords(e){let n=0;for(const r of this.boundCommands){_(r);const i=W(r,{captureAuthority:this.captureAuthority,id:`contributionCommand.${String(e)}.${String(++n)}`,scheduling:{policy:"parallel"}});this.frame.commandRecords.set(r,{controller:i,invoker:Object.freeze({run:i.run})})}}captureAuthority=()=>{const e=this.authority;if(this.state!=="active"||e===void 0)throw new u("inactive","Contribution mount is not active.");return e.assertCurrent(),e};createAuthority(e){return Object.freeze({assertCurrent:()=>{if(this.state!=="active")throw new u("inactive","Contribution mount is not active.");e.assertCurrent()},isCurrent:()=>this.state==="active"&&e.isCurrent()})}publishRenderable(){for(const{listener:e}of[...this.renderListeners])try{e()}catch(n){g(n)}}releaseFrame(){this.released||(this.released=!0,this.frame.commandRecords.clear(),this.frame.models.clear(),this.boundCommands=[],this.renderListeners.clear())}}function de({children:t,mount:e}){const n=y(e.subscribeRenderable,e.getRenderableSnapshot,e.getRenderableSnapshot);return R(()=>{if(e.attach())return()=>e.detach()},[e]),p(()=>{n||e.releaseClosedFrame()},[e,n]),n?m(O,{value:e.frame,children:t}):null}function $(t){const e=t.bundle;t.bundle=void 0,e==null||e.frame.close()}function ue(t,e){if(typeof e!="object"||e===null)return z(t,e);const n=Object.entries(e).map(([r,i])=>[r,S(i)||typeof i=="function"||k(i)?i:F(i)]);return z(t,Object.fromEntries(n))}function le(t){return K(t)}const fe={models:[]};function I(t,e){try{t()}catch(n){throw H([e,n],"Contribution mount setup and cleanup failed.",{cause:e})}throw e}function he(t,e,n,r){const i=G(e),{models:o,reporter:d}=r??fe;try{const s=N(t.models),a=s.length===0?void 0:Q(s,i,d);return{bundle:a,models:[...o,...(a==null?void 0:a.models)??[],...n],props:i}}catch(s){return I(i.close,s)}}function me(t,e,n,r){const{bundle:i,models:o,props:d}=he(t,e,n,r);let s=!1;const a=()=>{if(!s){s=!0;try{d.close()}finally{i==null||i.close().catch(g)}}};try{return{frame:new ce(o.map(([c,f])=>ue(c,f)),{isActive:()=>!s,onClose:a}),isOpen:()=>!s,props:d}}catch(l){return I(a,l)}}function be(t,e){return m(t,{...e})}function pe({bundle:t,contribution:e}){const n=j(t.props),r=b(()=>e.props===void 0?n:e.props(n),[n,e]);return m(de,{mount:t.frame,children:be(e.Component,r)})}function ge({authority:t,contribution:e,fixtures:n,slotProps:r}){const i=b(()=>({authority:t,bundle:void 0,contribution:e,fixtures:n,retired:!1}),[t,e,n]),[o,d]=q(void 0),s=(o==null?void 0:o.owner)===i?o.bundle:void 0,a=M(r);return p(()=>{a.current=r}),R(()=>()=>{i.retired=!0,queueMicrotask(()=>{try{$(i)}catch(l){g(l)}})},[i]),p(()=>{const l=i.bundle??(i.bundle=me(i.contribution,a.current,i.fixtures??[],i.authority));return d(c=>(c==null?void 0:c.owner)===i?c:{bundle:l,owner:i}),()=>{i.retired&&$(i)}},[i]),p(()=>{(s==null?void 0:s.isOpen())===!0&&s.props.set(r)},[s,r]),s===void 0?null:m(pe,{bundle:s,contribution:e})}function Ce(t){const{authority:e,contribution:n,contributionId:r,fixtures:i,targetId:o}=t,d=U(),s=b(()=>({authority:e,contribution:n,fixtures:i}),[e,n,i]),a=e==null?void 0:e.feature,l=b(()=>({contribution:r,feature:a,target:o}),[r,a,o]),c=e==null?void 0:e.reporter,f=v(h=>J(h,c),[c]);return m(X,{content:d,identity:s,report:f,source:l,children:m(ge,{...t})})}const we=T(Ce);export{we as M,j as a,A as b,le as c,F as d,Y as u};
|
|
2
|
+
//# sourceMappingURL=contribution-frame-B1PnpOaP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contribution-frame-B1PnpOaP.js","sources":["../src/mount-context.tsx","../src/readable-hooks.ts","../src/command.ts","../src/model-binding.ts","../src/mount-frame.ts","../src/mount-provider.tsx","../src/contribution-frame.tsx"],"sourcesContent":["import { createContext, useContext } from 'react';\n\nimport { ContributionError } from './errors';\nimport type { Frame } from './mount-frame';\n\nconst FrameContext = createContext<Frame | undefined>(undefined);\n\nfunction useFrame(): Frame {\n const frame = useContext(FrameContext);\n\n if (frame === undefined) {\n throw new ContributionError('missing', 'Opetope hook was called outside a contribution mount.');\n }\n\n return frame;\n}\n\nexport { FrameContext, useFrame };\n","import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';\n\nimport type { Readable } from '@opetope/core';\n\nconst identity = <Value>(value: Value): Value => value;\n\ninterface SelectionInstance<Selected> {\n hasValue: boolean;\n value: Selected | undefined;\n}\n\nexport function useSelector<Value, Selected>(\n readable: Readable<Value>,\n selector: (value: Value) => Selected,\n isEqual: (left: Selected, right: Selected) => boolean = Object.is,\n): Selected {\n const subscribe = useCallback((listener: () => void) => readable.subscribe(listener), [readable]);\n const instanceRef = useRef<SelectionInstance<Selected> | undefined>(undefined);\n let instance = instanceRef.current;\n\n if (instance === undefined) {\n instance = { hasValue: false, value: undefined };\n instanceRef.current = instance;\n }\n\n const getSelectedSnapshot = useMemo(() => {\n let initialized = false;\n let sourceSnapshot: Value;\n let selectedSnapshot: Selected;\n\n return (): Selected => {\n const nextSource = readable.getSnapshot();\n\n if (initialized && Object.is(sourceSnapshot, nextSource)) return selectedSnapshot;\n\n const nextSelected = selector(nextSource);\n\n if (!initialized) {\n let nextSelectedSnapshot = nextSelected;\n\n if (instance.hasValue && isEqual(instance.value as Selected, nextSelected)) {\n nextSelectedSnapshot = instance.value as Selected;\n }\n\n initialized = true;\n sourceSnapshot = nextSource;\n selectedSnapshot = nextSelectedSnapshot;\n\n return selectedSnapshot;\n }\n\n if (isEqual(selectedSnapshot, nextSelected)) {\n sourceSnapshot = nextSource;\n\n return selectedSnapshot;\n }\n\n sourceSnapshot = nextSource;\n selectedSnapshot = nextSelected;\n\n return selectedSnapshot;\n };\n }, [instance, isEqual, readable, selector]);\n\n const selected = useSyncExternalStore(subscribe, getSelectedSnapshot, getSelectedSnapshot);\n\n useEffect(() => {\n instance.hasValue = true;\n instance.value = selected;\n }, [instance, selected]);\n\n return selected;\n}\n\nexport function useReadable<Value>(readable: Readable<Value>): Value {\n return useSelector(readable, identity);\n}\n","import type { Call } from '@opetope/core';\nimport { isCallTarget } from '@opetope/core/internal';\n\nimport { ContributionError } from './errors';\n\n/**\n * A bound command is the authentic `Call` the model field declares, so the common path adds no wrapper. Observable\n * call state belongs to the model as a `Readable` field (D71), so a binding carries no mode.\n */\ntype Command<Input, Output> = Call<Input, Output>;\n\ntype CommandBindingIdentity = Call<never, unknown>;\ntype CommandBinding<Input, Output> = Command<Input, Output>;\n\ntype CommandOutcome<Output> =\n | { readonly error: unknown; readonly status: 'failed' }\n | { readonly reason: unknown; readonly status: 'cancelled' }\n | { readonly status: 'ok'; readonly value: Output };\n\ntype AnyCommandBinding = CommandBinding<never, unknown>;\n\ntype CommandBindingDefinition<Input, Output> = {\n readonly target: Call<Input, Output>;\n};\n\nconst bindingDefinitions = new WeakMap<CommandBindingIdentity, CommandBindingDefinition<never, unknown>>();\n\nfunction bindCommand<Input, Output>(target: Call<Input, Output>): CommandBinding<Input, Output> {\n if (arguments.length > 1) {\n throw new ContributionError(\n 'binding-invalid',\n 'Command follows the target it binds; binding options are forbidden.',\n );\n }\n\n if (!isCallTarget(target)) throw new ContributionError('binding-invalid', 'Command target is not authentic.');\n\n bindingDefinitions.set(target, { target });\n\n return target;\n}\n\nfunction getCommandBindingDefinition<Input, Output>(\n binding: Call<Input, Output>,\n): CommandBindingDefinition<Input, Output> {\n const definition = bindingDefinitions.get(binding);\n\n if (definition === undefined) throw new ContributionError('binding-invalid', 'Command binding is not authentic.');\n\n return definition as CommandBindingDefinition<Input, Output>;\n}\n\nfunction isCommandBinding(value: unknown): value is AnyCommandBinding {\n return typeof value === 'object' && value !== null && bindingDefinitions.has(value as CommandBindingIdentity);\n}\n\nexport { bindCommand, getCommandBindingDefinition, isCommandBinding };\nexport type { Command, CommandOutcome };\n","import type { Call, ModelOf } from '@opetope/core';\nimport { isModel, isReadable } from '@opetope/core/internal';\nimport type { ModelIdentity, NoExtraKeys } from '@opetope/core/internal';\n\nimport { isCommandBinding } from './command';\nimport { ContributionError } from './errors';\n\nconst modelBindingBrand = Symbol('opetope.model-binding');\nconst modelBindingIdentityBrand = Symbol('opetope.model-binding-identity');\nconst modelBindingMarker = Object.freeze({});\n\ntype AnyModel = ModelIdentity;\ntype AnyCommand = Call<never, unknown>;\n\ninterface ModelBindingIdentity {\n readonly [modelBindingIdentityBrand]: true;\n}\n\ninterface ModelBinding<Declaration extends AnyModel> extends ModelBindingIdentity {\n readonly [modelBindingBrand]: (declaration: Declaration) => Declaration;\n}\n\ntype ModelBindingMetadata = {\n readonly commands: readonly AnyCommand[];\n readonly declaration: AnyModel;\n readonly model: object;\n};\n\nconst registry = new WeakMap<ModelBindingIdentity, ModelBindingMetadata>();\n\n/**\n * A model field is either a stable Readable or a bound command. The declaration carries the shape in its type, so\n * binding authority is the runtime check that every published field is one of those two authentic kinds.\n */\nfunction modelFieldValue(declaration: AnyModel, model: object, field: PropertyKey): unknown {\n if (typeof field !== 'string') {\n throw new ContributionError('binding-invalid', `Model ${declaration.id} accepts only string fields.`);\n }\n\n const descriptor = Object.getOwnPropertyDescriptor(model, field);\n\n if (descriptor?.enumerable !== true || !('value' in descriptor)) {\n throw new ContributionError(\n 'binding-invalid',\n `Model ${declaration.id}.${field} requires an enumerable data binding.`,\n );\n }\n\n return descriptor.value;\n}\n\nfunction snapshotModelFields(declaration: AnyModel, model: object): readonly AnyCommand[] {\n const commands: AnyCommand[] = [];\n const fields = Reflect.ownKeys(model);\n\n if (fields.length === 0)\n throw new ContributionError('binding-invalid', `Model ${declaration.id} requires at least one field.`);\n\n for (const field of fields) {\n const value = modelFieldValue(declaration, model, field);\n\n if (isReadable(value) || typeof value === 'function') continue;\n\n if (!isCommandBinding(value)) {\n throw new ContributionError(\n 'binding-invalid',\n `Model ${declaration.id}.${String(field)} requires a Readable or a bound command.`,\n );\n }\n\n commands.push(value);\n }\n\n return Object.freeze(commands);\n}\n\nfunction bindModel<Declaration extends AnyModel, const Actual extends ModelOf<Declaration>>(\n declaration: Declaration,\n model: Actual & NoExtraKeys<Actual, ModelOf<Declaration>>,\n): ModelBinding<Declaration> {\n if (!isModel(declaration)) throw new ContributionError('binding-invalid', 'Model declaration is not authentic.');\n\n if (typeof model !== 'object' || model === null) {\n throw new ContributionError('binding-invalid', `Model ${declaration.id} requires a record binding.`);\n }\n\n const commands = snapshotModelFields(declaration, model);\n const binding = Object.freeze({\n [modelBindingBrand]: modelBindingMarker as unknown as (value: Declaration) => Declaration,\n [modelBindingIdentityBrand]: true as const,\n });\n registry.set(binding, { commands, declaration, model });\n\n return binding;\n}\n\nfunction getModelBindingMetadata(binding: ModelBindingIdentity): ModelBindingMetadata {\n const metadata = registry.get(binding);\n\n if (metadata === undefined) throw new ContributionError('binding-invalid', 'Model binding is not authentic.');\n\n return metadata;\n}\n\nexport { bindModel, getModelBindingMetadata };\nexport type { AnyCommand, AnyModel, ModelBindingIdentity };\n","import type {\n CallAuthority,\n CallController,\n CallDiagnostics,\n CallRunOptions,\n LifetimeLease,\n} from '@opetope/core/internal';\nimport { createCallController, createLifetimeController, reportDetachedError } from '@opetope/core/internal';\n\nimport { getCommandBindingDefinition } from './command';\nimport { ContributionError } from './errors';\nimport { getModelBindingMetadata } from './model-binding';\nimport type { AnyCommand, AnyModel, ModelBindingIdentity } from './model-binding';\n\ntype RuntimeCommandInvoker = {\n readonly run: (input: never, options?: CallRunOptions<unknown>) => Promise<unknown>;\n};\n\ntype CommandRecord = {\n readonly controller: CallController<never, unknown>;\n readonly invoker: RuntimeCommandInvoker;\n};\n\n/** What a mounted contribution grants its components: the models it may read and the commands it may invoke. */\ntype Frame = {\n readonly commandRecords: Map<AnyCommand, CommandRecord>;\n readonly models: Map<AnyModel, unknown>;\n};\n\ntype MountFrameLifecycle = {\n readonly isActive: () => boolean;\n readonly onClose: () => void;\n};\n\ntype MountFrameDiagnostics = {\n readonly active: boolean;\n readonly calls: readonly CallDiagnostics[];\n readonly commandRecords: number;\n readonly models: number;\n};\n\nlet nextMountId = 0;\n\nfunction buildFrame(supplied: readonly ModelBindingIdentity[]): {\n readonly commands: readonly AnyCommand[];\n readonly frame: Frame;\n} {\n const models = new Map<AnyModel, unknown>();\n const commands: AnyCommand[] = [];\n const bound = new Set<AnyCommand>();\n\n for (const candidate of supplied) {\n const binding = getModelBindingMetadata(candidate);\n\n if (models.has(binding.declaration)) {\n throw new ContributionError('duplicate', `Model ${binding.declaration.id} is bound twice in one mount.`);\n }\n\n models.set(binding.declaration, binding.model);\n\n for (const command of binding.commands) {\n // D197: one Call is one command of the mount, however many fields of the models name it. A second name is an\n // alias, not a second command, so it shares the record, the activation and the fence.\n if (bound.has(command)) continue;\n\n bound.add(command);\n commands.push(command);\n }\n }\n\n return { commands: Object.freeze(commands), frame: { commandRecords: new Map(), models } };\n}\n\n/**\n * The lifetime of one mounted contribution. `attach` claims it for the renderer, `detach` schedules the close on the\n * next microtask so a StrictMode remount reuses the same mount, and `close` fences every command it granted (D85).\n */\nclass MountFrameImpl {\n readonly frame: Frame;\n private authority: CallAuthority | undefined;\n private boundCommands: readonly AnyCommand[];\n private closeTicket = 0;\n private readonly isActive: (() => boolean) | undefined;\n private readonly lifetime: ReturnType<typeof createLifetimeController>;\n private onClose: (() => void) | undefined;\n private released = false;\n private rendererAttached = false;\n private readonly renderListeners = new Set<{ readonly listener: () => void }>();\n private state: 'active' | 'closed' | 'closing' | 'detached' = 'detached';\n\n constructor(models: readonly ModelBindingIdentity[], lifecycle?: MountFrameLifecycle) {\n const mountId = ++nextMountId;\n this.lifetime = createLifetimeController({ id: `contribution.${String(mountId)}` });\n this.isActive = lifecycle?.isActive;\n this.onClose = lifecycle?.onClose;\n const { commands, frame } = buildFrame(models);\n this.frame = frame;\n this.boundCommands = commands;\n this.buildCommandRecords(mountId);\n }\n\n attach(): boolean {\n if (this.state === 'closing') {\n this.closeTicket += 1;\n this.state = 'active';\n this.rendererAttached = true;\n\n return true;\n }\n\n if (this.state === 'closed') return false;\n\n if (this.state === 'active') throw new ContributionError('duplicate', 'One contribution mount cannot mount twice.');\n\n const authority = this.createAuthority(this.lifetime.open());\n\n try {\n for (const record of this.frame.commandRecords.values()) record.controller.activate();\n } catch (error) {\n this.authority = authority;\n this.state = 'active';\n this.close();\n\n throw error;\n }\n\n this.authority = authority;\n this.rendererAttached = true;\n this.state = 'active';\n\n return true;\n }\n\n close(): void {\n if (this.state === 'closed') return;\n\n this.state = 'closed';\n this.closeTicket += 1;\n this.lifetime.close();\n this.authority = undefined;\n\n for (const record of this.frame.commandRecords.values()) record.controller.close();\n\n const onClose = this.onClose;\n this.onClose = undefined;\n onClose?.();\n this.publishRenderable();\n\n if (!this.rendererAttached) this.releaseFrame();\n }\n\n detach(): void {\n if (!this.rendererAttached) return;\n\n this.rendererAttached = false;\n\n if (this.state === 'closed') {\n this.releaseFrame();\n\n return;\n }\n\n if (this.state !== 'active') return;\n\n this.state = 'closing';\n const ticket = ++this.closeTicket;\n\n queueMicrotask(() => {\n if (this.state === 'closing' && ticket === this.closeTicket) this.close();\n });\n }\n\n diagnostics(): MountFrameDiagnostics {\n const calls = [...this.frame.commandRecords.values()].map(record => record.controller.diagnostics());\n\n return Object.freeze({\n active: this.state === 'active',\n calls: Object.freeze(calls),\n commandRecords: this.frame.commandRecords.size,\n models: this.frame.models.size,\n });\n }\n\n getRenderableSnapshot = (): boolean => this.state !== 'closed' && (this.isActive === undefined || this.isActive());\n\n releaseClosedFrame(): void {\n if (this.state === 'closed') this.releaseFrame();\n }\n\n subscribeRenderable = (listener: () => void): (() => void) => {\n if (this.state === 'closed') return (): void => undefined;\n\n const registration = { listener };\n this.renderListeners.add(registration);\n let subscribed = true;\n\n return (): void => {\n if (!subscribed) return;\n\n subscribed = false;\n this.renderListeners.delete(registration);\n };\n };\n\n private buildCommandRecords(mountId: number): void {\n let commandIndex = 0;\n\n // `buildFrame` already reduced the models to their distinct Calls, so every command here gets its own record.\n for (const command of this.boundCommands) {\n // Authenticity check only: the binding carries no mode since observe was removed (D71).\n getCommandBindingDefinition(command);\n const controller = createCallController(command, {\n captureAuthority: this.captureAuthority,\n id: `contributionCommand.${String(mountId)}.${String(++commandIndex)}`,\n scheduling: { policy: 'parallel' },\n });\n this.frame.commandRecords.set(command, { controller, invoker: Object.freeze({ run: controller.run }) });\n }\n }\n\n private captureAuthority = (): CallAuthority => {\n const authority = this.authority;\n\n if (this.state !== 'active' || authority === undefined) {\n throw new ContributionError('inactive', 'Contribution mount is not active.');\n }\n\n authority.assertCurrent();\n\n return authority;\n };\n\n private createAuthority(lease: LifetimeLease): CallAuthority {\n return Object.freeze({\n assertCurrent: (): void => {\n if (this.state !== 'active') throw new ContributionError('inactive', 'Contribution mount is not active.');\n\n lease.assertCurrent();\n },\n isCurrent: (): boolean => this.state === 'active' && lease.isCurrent(),\n });\n }\n\n private publishRenderable(): void {\n for (const { listener } of [...this.renderListeners]) {\n try {\n listener();\n } catch (error) {\n reportDetachedError(error);\n }\n }\n }\n\n private releaseFrame(): void {\n if (this.released) return;\n\n this.released = true;\n this.frame.commandRecords.clear();\n this.frame.models.clear();\n this.boundCommands = [];\n this.renderListeners.clear();\n }\n}\n\nexport { MountFrameImpl };\nexport type { Frame };\n","import { useInsertionEffect, useLayoutEffect, useSyncExternalStore } from 'react';\nimport type { PropsWithChildren, ReactElement } from 'react';\n\nimport { FrameContext } from './mount-context';\nimport type { MountFrameImpl } from './mount-frame';\n\nfunction MountProvider({\n children,\n mount,\n}: PropsWithChildren<{ readonly mount: MountFrameImpl }>): ReactElement | null {\n const renderable = useSyncExternalStore(\n mount.subscribeRenderable,\n mount.getRenderableSnapshot,\n mount.getRenderableSnapshot,\n );\n\n useInsertionEffect(() => {\n const attached = mount.attach();\n\n if (!attached) return undefined;\n\n return () => mount.detach();\n }, [mount]);\n\n useLayoutEffect(() => {\n if (!renderable) mount.releaseClosedFrame();\n }, [mount, renderable]);\n\n return renderable ? <FrameContext value={mount.frame}>{children}</FrameContext> : null;\n}\n\nexport { MountProvider };\n","import { memo, useCallback, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { ComponentType, ReactElement } from 'react';\n\nimport type { DeclarationId, State } from '@opetope/core';\nimport {\n createAggregateError,\n createState,\n getContributionOwner,\n isReadable,\n reportDetachedError,\n} from '@opetope/core/internal';\nimport type { ModelIdentity } from '@opetope/core/internal';\nimport { openContributionModels, reportRuntimeFailure, requireContributionModelPlans } from '@opetope/runtime/internal';\nimport type { ContributionModelBundle, RuntimeErrorReporter } from '@opetope/runtime/internal';\n\nimport { bindCommand, isCommandBinding } from './command';\nimport { ContributionIsolation, useContributionErrorContent } from './contribution-isolation';\nimport { bindModel } from './model-binding';\nimport type { ModelBindingIdentity } from './model-binding';\nimport { MountFrameImpl } from './mount-frame';\nimport { MountProvider } from './mount-provider';\nimport { useReadable } from './readable-hooks';\n\ntype AnyModel = ModelIdentity;\ntype ContributionAuthority = {\n /** The feature whose generation published this contribution (D257). */\n readonly feature?: DeclarationId;\n readonly models: readonly (readonly [AnyModel, unknown])[];\n /** The reporter of the feature that published this contribution: a contained failure is its diagnostic, not ours. */\n readonly reporter?: RuntimeErrorReporter;\n};\ntype ModelFixture = readonly [declaration: AnyModel, model: unknown];\n\ntype ContributionValue = {\n readonly Component: ComponentType<never>;\n readonly models?: readonly object[] | undefined;\n readonly props?: ((slotProps: never) => object) | undefined;\n};\n\ntype ContributionMountProps = {\n readonly authority?: ContributionAuthority | undefined;\n readonly contribution: ContributionValue;\n /** The published entry this mount renders, and the slot it renders in: the identity of a contained failure (D257). */\n readonly contributionId: DeclarationId;\n readonly fixtures?: readonly ModelFixture[];\n readonly slotProps: object;\n readonly targetId: DeclarationId;\n};\n\ntype MountBundle = {\n readonly frame: MountFrameImpl;\n readonly isOpen: () => boolean;\n readonly props: State<object>;\n};\n\ntype MountOwner = {\n readonly authority: ContributionAuthority | undefined;\n bundle: MountBundle | undefined;\n readonly contribution: ContributionValue;\n readonly fixtures: readonly ModelFixture[] | undefined;\n retired: boolean;\n};\n\ntype CommittedMount = {\n readonly bundle: MountBundle;\n readonly owner: MountOwner;\n};\n\nfunction closeOwnedMount(owner: MountOwner): void {\n const bundle = owner.bundle;\n owner.bundle = undefined;\n bundle?.frame.close();\n}\n\n/** Model fields arrive as authentic kernel calls; binding them registers the command records the frame needs. */\nfunction bindFrameModel(declaration: AnyModel, model: unknown): ModelBindingIdentity {\n if (typeof model !== 'object' || model === null) return bindModel(declaration as never, model as never);\n\n const fields = Object.entries(model).map(([key, value]): readonly [string, unknown] => [\n key,\n isReadable(value) || typeof value === 'function' || isCommandBinding(value) ? value : bindCommand(value as never),\n ]);\n\n return bindModel(declaration as never, Object.fromEntries(fields) as never);\n}\n\n/**\n * D70: the authority of a contribution belongs to the entry a feature published, not to the value it wrote. One\n * static contribution object may be published by two features, and each mount must read the models of its own.\n */\nfunction contributionAuthority(entry: unknown): ContributionAuthority | undefined {\n return getContributionOwner(entry) as ContributionAuthority | undefined;\n}\n\n/** A mount with no publishing generation behind it: a fixture of a component test grants nothing and reports nowhere. */\nconst unownedContribution: ContributionAuthority = { models: [] };\n\nfunction cleanupFailedMount(close: () => void, cause: unknown): never {\n try {\n close();\n } catch (error) {\n throw createAggregateError([cause, error], 'Contribution mount setup and cleanup failed.', { cause });\n }\n\n throw cause;\n}\n\nfunction contributionModels(\n contribution: ContributionValue,\n slotProps: object,\n fixtures: readonly ModelFixture[],\n owner: ContributionAuthority | undefined,\n): {\n readonly bundle: ContributionModelBundle | undefined;\n readonly models: readonly ModelFixture[];\n readonly props: State<object>;\n} {\n const props = createState(slotProps);\n const { models: granted, reporter } = owner ?? unownedContribution;\n\n try {\n const plans = requireContributionModelPlans(contribution.models);\n const bundle = plans.length === 0 ? undefined : openContributionModels(plans, props, reporter);\n\n return {\n bundle,\n models: [...granted, ...(bundle?.models ?? []), ...fixtures],\n props,\n };\n } catch (error) {\n return cleanupFailedMount(props.close, error);\n }\n}\n\nfunction createMountBundle(\n contribution: ContributionValue,\n slotProps: object,\n fixtures: readonly ModelFixture[],\n authority: ContributionAuthority | undefined,\n): MountBundle {\n const { bundle, models, props } = contributionModels(contribution, slotProps, fixtures, authority);\n let closed = false;\n const close = (): void => {\n if (closed) return;\n\n closed = true;\n\n try {\n props.close();\n } finally {\n void bundle?.close().catch(reportDetachedError);\n }\n };\n try {\n const frame = new MountFrameImpl(\n models.map(([declaration, model]) => bindFrameModel(declaration, model)),\n { isActive: () => !closed, onClose: close },\n );\n\n return { frame, isOpen: () => !closed, props };\n } catch (error) {\n return cleanupFailedMount(close, error);\n }\n}\n\nfunction createComponentElement(Component: ComponentType<never>, props: object): ReactElement {\n const Rendered = Component as ComponentType<object>;\n\n return <Rendered {...props} />;\n}\n\ntype MountedContributionProps = {\n readonly bundle: MountBundle;\n readonly contribution: ContributionValue;\n};\n\n/** Reads the committed props of one live mount: every hook here sees a bundle that already exists. */\nfunction MountedContribution({ bundle, contribution }: MountedContributionProps): ReactElement {\n const committedProps = useReadable(bundle.props);\n const componentProps = useMemo(\n () => (contribution.props === undefined ? committedProps : contribution.props(committedProps as never)),\n [committedProps, contribution],\n );\n\n return (\n <MountProvider mount={bundle.frame}>{createComponentElement(contribution.Component, componentProps)}</MountProvider>\n );\n}\n\n/**\n * D188: a mount is created in commit, never in render. The first pass renders nothing, the layout effect builds the\n * bundle from the props that committed and publishes it, and React flushes that update before paint. A render that\n * is thrown away — StrictMode's double invoke, an abandoned concurrent pass, a suspended sibling — therefore\n * creates no models at all, so there is nothing to sweep, adopt or replace.\n */\nfunction ContributionMount({\n authority,\n contribution,\n fixtures,\n slotProps,\n}: ContributionMountProps): ReactElement | null {\n const owner = useMemo<MountOwner>(\n () => ({ authority, bundle: undefined, contribution, fixtures, retired: false }),\n [authority, contribution, fixtures],\n );\n const [committed, setCommitted] = useState<CommittedMount | undefined>(undefined);\n const bundle = committed?.owner === owner ? committed.bundle : undefined;\n const committedSlotProps = useRef(slotProps);\n\n useLayoutEffect(() => {\n committedSlotProps.current = slotProps;\n });\n\n // Suspense disconnects layout effects while hiding, but insertion cleanup only retires an actual identity.\n // Do not close Readables in insertion: their listeners may update React. A hidden tree has no layout cleanup\n // left to run on unmount, so the microtask also releases a mount that never becomes visible again.\n useInsertionEffect(\n () => () => {\n owner.retired = true;\n queueMicrotask(() => {\n try {\n closeOwnedMount(owner);\n } catch (error) {\n reportDetachedError(error);\n }\n });\n },\n [owner],\n );\n\n // The identity of the contribution owns the mount; a reveal and StrictMode reuse what its first commit created.\n useLayoutEffect(() => {\n const created = (owner.bundle ??= createMountBundle(\n owner.contribution,\n committedSlotProps.current,\n owner.fixtures ?? [],\n owner.authority,\n ));\n setCommitted(current => (current?.owner === owner ? current : { bundle: created, owner }));\n\n return () => {\n if (owner.retired) closeOwnedMount(owner);\n };\n }, [owner]);\n\n // Both the component and its models read the same committed snapshot, published before paint.\n useLayoutEffect(() => {\n if (bundle?.isOpen() === true) bundle.props.set(slotProps);\n }, [bundle, slotProps]);\n\n if (bundle === undefined) return null;\n\n return <MountedContribution bundle={bundle} contribution={contribution} />;\n}\n\n/**\n * D256: every mount is a boundary. The isolation sits above `ContributionMount`, so it contains the render of the\n * component and the commit that builds the mount alike, and a retry rebuilds the mount instead of reusing the bundle\n * that failed. Containment is unconditional; the content a failure shows is the host policy of `ContributionBoundary`.\n */\nfunction IsolatedContributionMount(props: ContributionMountProps): ReactElement {\n const { authority, contribution, contributionId, fixtures, targetId } = props;\n const content = useContributionErrorContent();\n const identity = useMemo(() => ({ authority, contribution, fixtures }), [authority, contribution, fixtures]);\n const feature = authority?.feature;\n const source = useMemo(\n () => ({ contribution: contributionId, feature, target: targetId }),\n [contributionId, feature, targetId],\n );\n const reporter = authority?.reporter;\n const report = useCallback((error: unknown) => reportRuntimeFailure(error, reporter), [reporter]);\n\n return (\n <ContributionIsolation content={content} identity={identity} report={report} source={source}>\n <ContributionMount {...props} />\n </ContributionIsolation>\n );\n}\n\n/**\n * One mount, one memo: `Slot` recreates its children whenever `entries` changes, so publishing or withholding one\n * contribution used to re-render every other mount of that target. The props are the contribution, the slot props\n * and the fixtures, all of which are stable while nothing about that mount changed.\n */\nconst MemoizedContributionMount = memo(IsolatedContributionMount);\n\nexport { contributionAuthority, MemoizedContributionMount as ContributionMount };\nexport type { ContributionValue, ModelFixture };\n"],"names":["FrameContext","createContext","useFrame","frame","useContext","ContributionError","identity","value","useSelector","readable","selector","isEqual","subscribe","useCallback","listener","instanceRef","useRef","instance","getSelectedSnapshot","useMemo","initialized","sourceSnapshot","selectedSnapshot","nextSource","nextSelected","nextSelectedSnapshot","selected","useSyncExternalStore","useEffect","useReadable","bindingDefinitions","bindCommand","target","isCallTarget","getCommandBindingDefinition","binding","definition","isCommandBinding","modelBindingBrand","modelBindingIdentityBrand","modelBindingMarker","registry","modelFieldValue","declaration","model","field","descriptor","snapshotModelFields","commands","fields","isReadable","bindModel","isModel","getModelBindingMetadata","metadata","nextMountId","buildFrame","supplied","models","bound","candidate","command","MountFrameImpl","lifecycle","mountId","createLifetimeController","authority","record","error","onClose","ticket","calls","registration","subscribed","commandIndex","controller","createCallController","lease","reportDetachedError","MountProvider","children","mount","renderable","useInsertionEffect","useLayoutEffect","_jsx","closeOwnedMount","owner","bundle","bindFrameModel","key","contributionAuthority","entry","getContributionOwner","unownedContribution","cleanupFailedMount","close","cause","createAggregateError","contributionModels","contribution","slotProps","fixtures","props","createState","granted","reporter","plans","requireContributionModelPlans","openContributionModels","createMountBundle","closed","createComponentElement","Component","MountedContribution","committedProps","componentProps","ContributionMount","committed","setCommitted","useState","committedSlotProps","created","current","IsolatedContributionMount","contributionId","targetId","content","useContributionErrorContent","feature","source","report","reportRuntimeFailure","ContributionIsolation","MemoizedContributionMount","memo"],"mappings":"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"}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { ComponentType, ReactElement } from 'react';
|
|
2
|
+
import type { DeclarationId } from '@opetope/core';
|
|
2
3
|
import type { ModelIdentity } from '@opetope/core/internal';
|
|
4
|
+
import type { RuntimeErrorReporter } from '@opetope/runtime/internal';
|
|
3
5
|
type AnyModel = ModelIdentity;
|
|
4
6
|
type ContributionAuthority = {
|
|
7
|
+
/** The feature whose generation published this contribution (D257). */
|
|
8
|
+
readonly feature?: DeclarationId;
|
|
5
9
|
readonly models: readonly (readonly [AnyModel, unknown])[];
|
|
10
|
+
/** The reporter of the feature that published this contribution: a contained failure is its diagnostic, not ours. */
|
|
11
|
+
readonly reporter?: RuntimeErrorReporter;
|
|
6
12
|
};
|
|
7
13
|
type ModelFixture = readonly [declaration: AnyModel, model: unknown];
|
|
8
14
|
type ContributionValue = {
|
|
@@ -13,8 +19,11 @@ type ContributionValue = {
|
|
|
13
19
|
type ContributionMountProps = {
|
|
14
20
|
readonly authority?: ContributionAuthority | undefined;
|
|
15
21
|
readonly contribution: ContributionValue;
|
|
22
|
+
/** The published entry this mount renders, and the slot it renders in: the identity of a contained failure (D257). */
|
|
23
|
+
readonly contributionId: DeclarationId;
|
|
16
24
|
readonly fixtures?: readonly ModelFixture[];
|
|
17
25
|
readonly slotProps: object;
|
|
26
|
+
readonly targetId: DeclarationId;
|
|
18
27
|
};
|
|
19
28
|
/**
|
|
20
29
|
* D70: the authority of a contribution belongs to the entry a feature published, not to the value it wrote. One
|
|
@@ -22,17 +31,16 @@ type ContributionMountProps = {
|
|
|
22
31
|
*/
|
|
23
32
|
declare function contributionAuthority(entry: unknown): ContributionAuthority | undefined;
|
|
24
33
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* creates no models at all, so there is nothing to sweep, adopt or replace.
|
|
34
|
+
* D256: every mount is a boundary. The isolation sits above `ContributionMount`, so it contains the render of the
|
|
35
|
+
* component and the commit that builds the mount alike, and a retry rebuilds the mount instead of reusing the bundle
|
|
36
|
+
* that failed. Containment is unconditional; the content a failure shows is the host policy of `ContributionBoundary`.
|
|
29
37
|
*/
|
|
30
|
-
declare function
|
|
38
|
+
declare function IsolatedContributionMount(props: ContributionMountProps): ReactElement;
|
|
31
39
|
/**
|
|
32
40
|
* One mount, one memo: `Slot` recreates its children whenever `entries` changes, so publishing or withholding one
|
|
33
41
|
* contribution used to re-render every other mount of that target. The props are the contribution, the slot props
|
|
34
42
|
* and the fixtures, all of which are stable while nothing about that mount changed.
|
|
35
43
|
*/
|
|
36
|
-
declare const MemoizedContributionMount: import("react").MemoExoticComponent<typeof
|
|
44
|
+
declare const MemoizedContributionMount: import("react").MemoExoticComponent<typeof IsolatedContributionMount>;
|
|
37
45
|
export { contributionAuthority, MemoizedContributionMount as ContributionMount };
|
|
38
46
|
export type { ContributionValue, ModelFixture };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{jsx as s}from"react/jsx-runtime";import{useRef as l,createContext as p,useContext as h,Component as u}from"react";import{markCancellation as a,isCancellation as C}from"@opetope/core/internal";class c extends Error{code;cause;contribution;feature;name="ContributionError";target;constructor(t,r,e={}){super(r),this.code=t,this.cause=e.cause,this.contribution=e.contribution,this.feature=e.feature,this.target=e.target,t==="inactive"&&a(this)}}const b={current:null},d=p(b);function m({children:n,error:t}){const r=l(null);return r.current=t,s(d,{value:r,children:n})}function y(){return h(d)}function f(n,t,r){const e=n==="render-failed"?"Contribution":"Error content of contribution",i=t.feature===void 0?"":` of feature ${String(t.feature)}`,o=new c(n,`${e} ${String(t.contribution)}${i} failed in slot ${String(t.target)}.`,{cause:r,...t});return C(r)?a(o):o}function g({content:n,error:t,retry:r,source:e}){return n({...e,error:t,retry:r})}class E extends u{state={failed:!1};static getDerivedStateFromError(){return{failed:!0}}componentDidCatch(t){this.props.report(f("error-content-failed",this.props.source,t))}render(){if(this.state.failed)return null;const{content:t,error:r,retry:e,source:i}=this.props,o=t.current;return typeof o=="function"?s(g,{content:o,error:r,retry:e,source:i}):o}}class x extends u{state={error:void 0,failed:!1,identity:void 0};static getDerivedStateFromError(t){return{error:t,failed:!0}}static getDerivedStateFromProps(t,r){return r.identity===t.identity?null:{error:void 0,failed:!1,identity:t.identity}}componentDidCatch(t){this.props.report(f("render-failed",this.props.source,t))}retry=()=>{this.setState(t=>t.failed?{error:void 0,failed:!1,identity:t.identity}:null)};render(){const{error:t,failed:r}=this.state;return r?s(E,{content:this.props.content,error:t,report:this.props.report,retry:this.retry,source:this.props.source}):this.props.children}}export{c as C,m as a,x as b,y as u};
|
|
2
|
+
//# sourceMappingURL=contribution-isolation-Bzfbt4iM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contribution-isolation-Bzfbt4iM.js","sources":["../src/errors.ts","../src/contribution-isolation.tsx"],"sourcesContent":["import type { DeclarationId } from '@opetope/core';\nimport { markCancellation } from '@opetope/core/internal';\n\ntype ContributionErrorCode =\n | 'binding-invalid'\n | 'duplicate'\n | 'error-content-failed'\n | 'inactive'\n | 'missing'\n | 'render-failed';\n\n/**\n * D257: what a contained failure says about itself. The mount is the only place that knows all three, and one\n * application reporter serves every feature, so a bare `TypeError` in it would name neither the surface nor the owner.\n */\ntype ContributionErrorDetails = {\n readonly cause?: unknown;\n readonly contribution?: DeclarationId | undefined;\n readonly feature?: DeclarationId | undefined;\n readonly target?: DeclarationId | undefined;\n};\n\n/**\n * One class per subject (D69): after D85 the subject is the contribution mount, which is what grants models and\n * fences commands, so model binding failures are its codes rather than a second class.\n */\nexport class ContributionError extends Error {\n readonly cause: unknown;\n\n /** The published entry, `<feature>.<provides key>`; absent where a failure has no published mount behind it. */\n readonly contribution: DeclarationId | undefined;\n\n readonly feature: DeclarationId | undefined;\n\n override readonly name = 'ContributionError';\n\n readonly target: DeclarationId | undefined;\n\n constructor(\n readonly code: ContributionErrorCode,\n message: string,\n details: ContributionErrorDetails = {},\n ) {\n super(message);\n this.cause = details.cause;\n this.contribution = details.contribution;\n this.feature = details.feature;\n this.target = details.target;\n\n if (code === 'inactive') markCancellation(this);\n }\n}\n\nexport type { ContributionErrorCode };\n","import { Component, createContext, useContext, useRef } from 'react';\nimport type { ReactNode, RefObject } from 'react';\n\nimport type { DeclarationId } from '@opetope/core';\nimport { isCancellation, markCancellation } from '@opetope/core/internal';\n\nimport { ContributionError } from './errors';\n\n/**\n * D257: which contribution failed. The entry id is `<feature>.<provides key>` and the target is the slot it renders\n * in, so one application-wide branch can answer by surface without naming every contribution.\n */\ntype ContributionSource = {\n readonly contribution: DeclarationId;\n readonly feature: DeclarationId | undefined;\n readonly target: DeclarationId;\n};\n\n/**\n * D256: what a failed contribution shows. The shape is the one `FeatureBoundary` already uses for its failure branch,\n * so a host writes one kind of error content for both: the failure itself plus the retry that belongs to it.\n */\ntype ContributionFailure = ContributionSource & {\n readonly error: unknown;\n readonly retry: () => void;\n};\n\ntype ContributionErrorContent = ((failure: ContributionFailure) => ReactNode) | ReactNode;\n\ntype ContributionBoundaryProps = {\n readonly children: ReactNode;\n readonly error: ContributionErrorContent;\n};\n\ntype ContributionErrorContentRef = RefObject<ContributionErrorContent>;\n\nconst noContributionErrorContent: ContributionErrorContentRef = { current: null };\n\n/**\n * One host policy for every mount below it: a contribution whose render or commit throws is replaced by this content\n * instead of reaching the host's own boundary. Containment does not depend on the provider — without it a failed\n * contribution renders nothing — so an application declares the boundary once, at the root, and never per slot.\n *\n * The context carries a stable ref, never the content itself: a host writes the error branch inline, and a context\n * value that changed with it would re-render every mount of the application and undo the per-mount memo (D130).\n * Writing the latest callback into a ref that only readers read is the shape `useModel` already uses for its selector\n * (D214); a mount reads it once, when it is already failing, so content replaced mid-failure lands on the next one.\n */\nconst ErrorContentContext = createContext<ContributionErrorContentRef>(noContributionErrorContent);\n\nfunction ContributionBoundary({ children, error }: ContributionBoundaryProps): ReactNode {\n const content = useRef<ContributionErrorContent>(null);\n content.current = error;\n\n return <ErrorContentContext value={content}>{children}</ErrorContentContext>;\n}\n\nfunction useContributionErrorContent(): ContributionErrorContentRef {\n return useContext(ErrorContentContext);\n}\n\n/**\n * A `RuntimeErrorReporter` takes one argument, so the identity of a contained failure travels inside the error.\n * Cancellation is a brand of the failure, not of a class (D69): a wrapper that dropped it would turn every teardown\n * race a host filters out into a product failure it has to read.\n */\nfunction contributionFailure(\n code: 'error-content-failed' | 'render-failed',\n source: ContributionSource,\n cause: unknown,\n): ContributionError {\n const subject = code === 'render-failed' ? 'Contribution' : 'Error content of contribution';\n const publisher = source.feature === undefined ? '' : ` of feature ${String(source.feature)}`;\n const failure = new ContributionError(\n code,\n `${subject} ${String(source.contribution)}${publisher} failed in slot ${String(source.target)}.`,\n { cause, ...source },\n );\n\n return isCancellation(cause) ? markCancellation(failure) : failure;\n}\n\ntype ContributionErrorProps = {\n readonly content: (failure: ContributionFailure) => ReactNode;\n readonly error: unknown;\n readonly retry: () => void;\n readonly source: ContributionSource;\n};\n\n/** The host callback runs in a child, never in the render of a boundary: a boundary cannot catch its own render. */\nfunction ContributionErrorContent({ content, error, retry, source }: ContributionErrorProps): ReactNode {\n return content({ ...source, error, retry });\n}\n\ntype ContributionFailureViewProps = {\n readonly content: ContributionErrorContentRef;\n readonly error: unknown;\n readonly report: (error: unknown) => void;\n readonly retry: () => void;\n readonly source: ContributionSource;\n};\n\n/**\n * The second stage of containment: error content written by the host is foreign code like any other, and a mount that\n * is already failing must not escalate because its replacement failed too. It exists only after a failure, so the\n * successful path pays for one boundary, not two.\n */\nclass ContributionFailureView extends Component<ContributionFailureViewProps, { readonly failed: boolean }> {\n override state: { readonly failed: boolean } = { failed: false };\n\n static getDerivedStateFromError(): { readonly failed: boolean } {\n return { failed: true };\n }\n\n override componentDidCatch(error: unknown): void {\n this.props.report(contributionFailure('error-content-failed', this.props.source, error));\n }\n\n override render(): ReactNode {\n if (this.state.failed) return null;\n\n const { content, error, retry, source } = this.props;\n const declared = content.current;\n\n return typeof declared === 'function' ? (\n <ContributionErrorContent content={declared} error={error} retry={retry} source={source} />\n ) : (\n declared\n );\n }\n}\n\ntype ContributionIsolationProps = {\n readonly children: ReactNode;\n readonly content: ContributionErrorContentRef;\n /** The mount this boundary belongs to: a replaced contribution starts clean instead of inheriting a dead failure. */\n readonly identity: object;\n readonly report: (error: unknown) => void;\n readonly source: ContributionSource;\n};\n\n/**\n * `failed` is a flag, not the presence of `error`: foreign code may throw `undefined`, and a sentinel would read that\n * as a healthy mount, render the children again and fail again until React gives up.\n */\ntype ContributionIsolationState = {\n readonly error: unknown;\n readonly failed: boolean;\n readonly identity: object | undefined;\n};\n\nclass ContributionIsolation extends Component<ContributionIsolationProps, ContributionIsolationState> {\n override state: ContributionIsolationState = { error: undefined, failed: false, identity: undefined };\n\n static getDerivedStateFromError(error: unknown): Pick<ContributionIsolationState, 'error' | 'failed'> {\n return { error, failed: true };\n }\n\n static getDerivedStateFromProps(\n props: ContributionIsolationProps,\n state: ContributionIsolationState,\n ): ContributionIsolationState | null {\n return state.identity === props.identity ? null : { error: undefined, failed: false, identity: props.identity };\n }\n\n override componentDidCatch(error: unknown): void {\n this.props.report(contributionFailure('render-failed', this.props.source, error));\n }\n\n /** Retry rebuilds the mount: the children remount, so their models are created again rather than reused. */\n private readonly retry = (): void => {\n this.setState(current => (current.failed ? { error: undefined, failed: false, identity: current.identity } : null));\n };\n\n override render(): ReactNode {\n const { error, failed } = this.state;\n\n if (!failed) return this.props.children;\n\n return (\n <ContributionFailureView\n content={this.props.content}\n error={error}\n report={this.props.report}\n retry={this.retry}\n source={this.props.source}\n />\n );\n }\n}\n\nexport { ContributionBoundary, ContributionIsolation, useContributionErrorContent };\nexport type { ContributionBoundaryProps, ContributionErrorContent, ContributionFailure };\n"],"names":["ContributionError","code","message","details","markCancellation","noContributionErrorContent","ErrorContentContext","createContext","ContributionBoundary","children","error","content","useRef","_jsx","useContributionErrorContent","useContext","contributionFailure","source","cause","subject","publisher","failure","isCancellation","ContributionErrorContent","retry","ContributionFailureView","Component","declared","ContributionIsolation","props","state","current","failed"],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
import type { ReactNode, RefObject } from 'react';
|
|
3
|
+
import type { DeclarationId } from '@opetope/core';
|
|
4
|
+
/**
|
|
5
|
+
* D257: which contribution failed. The entry id is `<feature>.<provides key>` and the target is the slot it renders
|
|
6
|
+
* in, so one application-wide branch can answer by surface without naming every contribution.
|
|
7
|
+
*/
|
|
8
|
+
type ContributionSource = {
|
|
9
|
+
readonly contribution: DeclarationId;
|
|
10
|
+
readonly feature: DeclarationId | undefined;
|
|
11
|
+
readonly target: DeclarationId;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* D256: what a failed contribution shows. The shape is the one `FeatureBoundary` already uses for its failure branch,
|
|
15
|
+
* so a host writes one kind of error content for both: the failure itself plus the retry that belongs to it.
|
|
16
|
+
*/
|
|
17
|
+
type ContributionFailure = ContributionSource & {
|
|
18
|
+
readonly error: unknown;
|
|
19
|
+
readonly retry: () => void;
|
|
20
|
+
};
|
|
21
|
+
type ContributionErrorContent = ((failure: ContributionFailure) => ReactNode) | ReactNode;
|
|
22
|
+
type ContributionBoundaryProps = {
|
|
23
|
+
readonly children: ReactNode;
|
|
24
|
+
readonly error: ContributionErrorContent;
|
|
25
|
+
};
|
|
26
|
+
type ContributionErrorContentRef = RefObject<ContributionErrorContent>;
|
|
27
|
+
declare function ContributionBoundary({ children, error }: ContributionBoundaryProps): ReactNode;
|
|
28
|
+
declare function useContributionErrorContent(): ContributionErrorContentRef;
|
|
29
|
+
type ContributionErrorProps = {
|
|
30
|
+
readonly content: (failure: ContributionFailure) => ReactNode;
|
|
31
|
+
readonly error: unknown;
|
|
32
|
+
readonly retry: () => void;
|
|
33
|
+
readonly source: ContributionSource;
|
|
34
|
+
};
|
|
35
|
+
/** The host callback runs in a child, never in the render of a boundary: a boundary cannot catch its own render. */
|
|
36
|
+
declare function ContributionErrorContent({ content, error, retry, source }: ContributionErrorProps): ReactNode;
|
|
37
|
+
type ContributionIsolationProps = {
|
|
38
|
+
readonly children: ReactNode;
|
|
39
|
+
readonly content: ContributionErrorContentRef;
|
|
40
|
+
/** The mount this boundary belongs to: a replaced contribution starts clean instead of inheriting a dead failure. */
|
|
41
|
+
readonly identity: object;
|
|
42
|
+
readonly report: (error: unknown) => void;
|
|
43
|
+
readonly source: ContributionSource;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* `failed` is a flag, not the presence of `error`: foreign code may throw `undefined`, and a sentinel would read that
|
|
47
|
+
* as a healthy mount, render the children again and fail again until React gives up.
|
|
48
|
+
*/
|
|
49
|
+
type ContributionIsolationState = {
|
|
50
|
+
readonly error: unknown;
|
|
51
|
+
readonly failed: boolean;
|
|
52
|
+
readonly identity: object | undefined;
|
|
53
|
+
};
|
|
54
|
+
declare class ContributionIsolation extends Component<ContributionIsolationProps, ContributionIsolationState> {
|
|
55
|
+
state: ContributionIsolationState;
|
|
56
|
+
static getDerivedStateFromError(error: unknown): Pick<ContributionIsolationState, 'error' | 'failed'>;
|
|
57
|
+
static getDerivedStateFromProps(props: ContributionIsolationProps, state: ContributionIsolationState): ContributionIsolationState | null;
|
|
58
|
+
componentDidCatch(error: unknown): void;
|
|
59
|
+
/** Retry rebuilds the mount: the children remount, so their models are created again rather than reused. */
|
|
60
|
+
private readonly retry;
|
|
61
|
+
render(): ReactNode;
|
|
62
|
+
}
|
|
63
|
+
export { ContributionBoundary, ContributionIsolation, useContributionErrorContent };
|
|
64
|
+
export type { ContributionBoundaryProps, ContributionErrorContent, ContributionFailure };
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
|
+
import type { DeclarationId } from '@opetope/core';
|
|
2
|
+
type ContributionErrorCode = 'binding-invalid' | 'duplicate' | 'error-content-failed' | 'inactive' | 'missing' | 'render-failed';
|
|
3
|
+
/**
|
|
4
|
+
* D257: what a contained failure says about itself. The mount is the only place that knows all three, and one
|
|
5
|
+
* application reporter serves every feature, so a bare `TypeError` in it would name neither the surface nor the owner.
|
|
6
|
+
*/
|
|
7
|
+
type ContributionErrorDetails = {
|
|
8
|
+
readonly cause?: unknown;
|
|
9
|
+
readonly contribution?: DeclarationId | undefined;
|
|
10
|
+
readonly feature?: DeclarationId | undefined;
|
|
11
|
+
readonly target?: DeclarationId | undefined;
|
|
12
|
+
};
|
|
1
13
|
/**
|
|
2
14
|
* One class per subject (D69): after D85 the subject is the contribution mount, which is what grants models and
|
|
3
15
|
* fences commands, so model binding failures are its codes rather than a second class.
|
|
4
16
|
*/
|
|
5
17
|
export declare class ContributionError extends Error {
|
|
6
|
-
readonly code:
|
|
18
|
+
readonly code: ContributionErrorCode;
|
|
19
|
+
readonly cause: unknown;
|
|
20
|
+
/** The published entry, `<feature>.<provides key>`; absent where a failure has no published mount behind it. */
|
|
21
|
+
readonly contribution: DeclarationId | undefined;
|
|
22
|
+
readonly feature: DeclarationId | undefined;
|
|
7
23
|
readonly name = "ContributionError";
|
|
8
|
-
|
|
24
|
+
readonly target: DeclarationId | undefined;
|
|
25
|
+
constructor(code: ContributionErrorCode, message: string, details?: ContributionErrorDetails);
|
|
9
26
|
}
|
|
27
|
+
export type { ContributionErrorCode };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{useState as S,useInsertionEffect as h,useLayoutEffect as f,useMemo as g,useSyncExternalStore as b,useRef as m,useCallback as w,useEffect as
|
|
1
|
+
import{useState as S,useInsertionEffect as h,useLayoutEffect as f,useMemo as g,useSyncExternalStore as b,useRef as m,useCallback as w,useEffect as P,createElement as E,Fragment as K}from"react";import{CallError as x,declarationId as $}from"@opetope/core";import{compatibleAbortReason as H,isCancellation as T,reportDetachedError as v,isCallTarget as G,isReadable as U,createAggregateError as D,createContributionTarget as L}from"@opetope/core/internal";import{C as d}from"./contribution-isolation-Bzfbt4iM.js";import{u as p,a as O,M as W,c as B}from"./contribution-frame-B1PnpOaP.js";import{b as ze}from"./contribution-frame-B1PnpOaP.js";function C(n,e){var t;if(T(e))return{reason:e,status:"cancelled"};try{const s=(t=n==null?void 0:n.onFailure)==null?void 0:t.call(n,e);s!==void 0&&Promise.resolve(s).catch(v)}catch(s){v(s)}return{error:e,status:"failed"}}function J(n){const e=n==null?void 0:n.onSuccess,t=n==null?void 0:n.signal;return e===void 0?t===void 0?void 0:{signal:t}:t===void 0?{onSuccess:e}:{onSuccess:e,signal:t}}function N(n,e){let t;const s=new Promise(r=>{t=r});return{input:n,options:e===void 0?void 0:{...e},promise:s,resolve:t}}function Q(n,e){const t=n.resolve;n.input=void 0,n.options=void 0,n.resolve=void 0,t==null||t(e)}class j{invoker;run=((e,t)=>{if(this.notify===void 0)return Promise.resolve({reason:new d("inactive","Command consumer is not mounted."),status:"cancelled"});const s=this.cancelledInput(t);if(s!==void 0)return Promise.resolve(s);const r=N(e,t);return this.start(r),r.promise});inFlight=0;notify;constructor(e){this.invoker=e}attach(e){return this.notify=e,()=>{this.notify===e&&(this.notify=void 0)}}synchronize(){var e;(e=this.notify)==null||e.call(this,this.inFlight>0)}aborted(e){return{reason:new x("cancelled","Command input was cancelled.",H(e)),status:"cancelled"}}cancelledInput(e){var t;return((t=e==null?void 0:e.signal)==null?void 0:t.aborted)===!0?this.aborted(e.signal):void 0}finish(e,t){var s;e.resolve!==void 0&&(this.inFlight-=1,Q(e,t),(s=this.notify)==null||s.call(this,this.inFlight>0,t))}invoke(e){let t;try{t=this.invoker.run(e.input,J(e.options)),e.input=void 0}catch(s){this.finish(e,C(e.options,s));return}t.then(s=>this.finish(e,{status:"ok",value:s}),s=>this.finish(e,C(e.options,s)))}start(e){var t;this.inFlight+=1,(t=this.notify)==null||t.call(this,!0),this.invoke(e)}}const V={inFlight:!1,lastError:null,result:void 0};function X(n,e,t,s){return s===void 0&&n.slot===e&&n.inFlight===t}function Y(n,e,t,s){if(X(n,e,t,s))return n;const{lastError:r,result:i}=n.slot===e?n:{lastError:null,result:void 0};return(s==null?void 0:s.status)==="ok"?{inFlight:t,lastError:null,result:s.value,slot:e}:(s==null?void 0:s.status)==="failed"?{inFlight:t,lastError:s.error,result:i,slot:e}:{inFlight:t,lastError:r,result:i,slot:e}}function Z(n){const[e]=S(()=>new WeakMap);let t=e.get(n);return t===void 0&&(t=new j(n),e.set(n,t)),t}function _(n){const t=p().commandRecords.get(n);if(t===void 0)throw new d("missing",`Command ${n.id} is not bound in this contribution.`);const s=Z(t.invoker),[r,i]=S(()=>({inFlight:!1,lastError:null,result:void 0,slot:s}));h(()=>s.attach((u,A)=>i(I=>Y(I,s,u,A))),[s]),f(()=>s.synchronize(),[s]);const{inFlight:o,lastError:l,result:c}=r.slot===s?r:V,a=s.run;return g(()=>({inFlight:o,lastError:l,result:c,run:a}),[o,l,c,a])}const q=()=>{},M=()=>q,ee=()=>0,te=Object.freeze({});function ne(n){const e=new j(n);return{detach:void 0,hook:{inFlight:!1,lastError:null,result:void 0,run:e.run},invoker:n,slot:e}}function se(n,e){return n.inFlight===e.inFlight&&Object.is(n.result,e.result)&&Object.is(n.lastError,e.lastError)}function re(n,e){return(e==null?void 0:e.status)==="ok"?{lastError:null,result:e.value}:(e==null?void 0:e.status)==="failed"?{lastError:e.error,result:n.result}:n}function ie(n,e,t){const s=n.hook;if(t===void 0&&s.inFlight===e)return!1;const{lastError:r,result:i}=re(s,t),o={inFlight:e,lastError:r,result:i,run:s.run};return se(s,o)?!1:(n.hook=o,!0)}function oe(n,e){return(n==null?void 0:n.invoker)===e?n:ne(e)}class ce{entries=new Map;listeners=new Set;revision=0;close(){var e;for(const t of this.entries.values())(e=t.detach)==null||e.call(t),t.detach=void 0;this.entries.clear()}commit(e){var t;for(const[s,r]of this.entries)e.get(s)!==r&&((t=r.detach)==null||t.call(r),r.detach=void 0);this.entries=e;for(const s of e.values())s.detach??(s.detach=s.slot.attach((r,i)=>{if(ie(s,r,i)){this.revision+=1;for(const o of this.listeners)o()}}))}getSnapshot=()=>this.revision;prepare(e,t){const s=new Map;for(const r of Reflect.ownKeys(e)){if(!Object.prototype.propertyIsEnumerable.call(e,r))continue;const i=e[r],o=i===void 0?void 0:t.commandRecords.get(i);if(o===void 0)throw new d("missing",`Command at ${String(r)} is not bound in this contribution.`);s.set(r,oe(this.entries.get(r),o.invoker))}return s}subscribe=e=>(this.listeners.add(e),()=>{this.listeners.delete(e)})}function le(n){const e=m(void 0);return Reflect.ownKeys(n).some(s=>Object.prototype.propertyIsEnumerable.call(n,s))?e.current??(e.current=new ce):void 0}function k(n){const e=p(),t=le(n),s=(t==null?void 0:t.getSnapshot)??ee;b((t==null?void 0:t.subscribe)??M,s,s);const r=t==null?void 0:t.prepare(n,e);return h(()=>()=>t==null?void 0:t.close(),[t]),h(()=>{r!==void 0&&(t==null||t.commit(r))}),f(()=>{if(r!==void 0)for(const i of r.values())i.slot.synchronize()}),r===void 0?te:Object.fromEntries([...r].map(([i,o])=>[i,o.hook]))}function ae(n,e){if(!U(n))throw new d("binding-invalid","Model selection read requires a Readable.");if(e.has(n))return e.get(n);const t=n.getSnapshot();return e.set(n,t),t}function ue(n,e){const s=n({read:(r,i)=>{const o=ae(r,e);return i===void 0?o:i(o)}});if(typeof s!="object"||s===null||Array.isArray(s))throw new d("binding-invalid","Model selection requires a record.");return Object.fromEntries(Reflect.ownKeys(s).filter(r=>Object.prototype.propertyIsEnumerable.call(s,r)).map(r=>[r,s[r]]))}function de(n,e,t){const s=Reflect.ownKeys(e),r=Reflect.ownKeys(n.values);if(s.length!==r.length||t.size!==n.sources.size)return!1;for(const i of t)if(!n.sources.has(i))return!1;return s.every((i,o)=>r[o]===i&&Object.is(n.values[i],e[i]))}const y={commands:{},sources:new Set,values:{}};function fe(n){return Object.fromEntries(Reflect.ownKeys(n).filter(e=>G(n[e])).map(e=>[e,n[e]]))}function he(n,e){let t,s,r=y;return()=>{const i=e.current;if(i===void 0)return y;if(t!==void 0&&s===i&&[...t].every(([a,u])=>Object.is(a.getSnapshot(),u)))return r;const o=new Map,l=ue(a=>i(n,a),o),c=new Set(o.keys());return(t===void 0||!de(r,l,c))&&(r={commands:fe(l),sources:c,values:l}),t=o,s=i,r}}function R(n,e=[]){for(const t of n){const s=t.dispose;t.dispose=void 0;try{s==null||s()}catch(r){e.push(r)}}if(e.length===1)throw e[0];if(e.length>1)throw D(e,"Model selection sources failed to release.")}class be{enabled=!1;listeners=new Set;sources=new Set;subscriptions=new Map;close(){this.enabled=!1,this.sources=new Set,this.releaseAll()}commit(e){this.sources=e,this.enabled=!0;try{this.synchronize()}catch(t){this.releaseAll([t])}}subscribe=e=>{const t={notify:e};this.listeners.add(t);try{this.synchronize()}catch(s){this.listeners.delete(t),this.releaseAll([s])}return()=>{this.listeners.delete(t),this.listeners.size===0&&this.releaseAll()}};add(e){const t={dispose:void 0};this.subscriptions.set(e,t);const s=e.subscribe(this.notify);if(typeof s!="function")throw new TypeError("Model selection source must return a disposer.");this.subscriptions.get(e)===t?t.dispose=s:s()}notify=()=>{for(const e of[...this.listeners])this.listeners.has(e)&&e.notify()};releaseAll(e){const t=[...this.subscriptions.values()];this.subscriptions.clear(),R(t,e)}removeUnselected(){const e=[];for(const[t,s]of this.subscriptions)this.sources.has(t)||(this.subscriptions.delete(t),e.push(s));R(e)}synchronize(){this.removeUnselected();for(const e of this.sources){if(!this.enabled||this.listeners.size===0)return;this.sources.has(e)&&!this.subscriptions.has(e)&&this.add(e)}}}const me=()=>y;function pe(n){const e=m(void 0);return n?e.current??(e.current=new be):void 0}function ye(n,e){const t=p();if(!t.models.has(n))throw new d("missing",`Model ${n.id} is not granted to this contribution.`);const s=t.models.get(n),r=pe(e!==void 0),i=m(void 0);i.current=e;const o=e!==void 0,l=g(()=>o?he(s,i):me,[s,o]),c=b((r==null?void 0:r.subscribe)??M,l,l),a=k(c.commands);return f(()=>()=>r==null?void 0:r.close(),[r]),f(()=>r==null?void 0:r.commit(c.sources),[c,r]),e===void 0?s:Object.fromEntries(Reflect.ownKeys(c.values).map(u=>[u,Object.prototype.hasOwnProperty.call(a,u)?a[u]:c.values[u]]))}function Se(n){const e=Object.freeze([...n]);return t=>{if(t.requires!==void 0)throw new d("duplicate","Component already declares the models it requires.");return Object.defineProperty(t,"requires",{enumerable:!1,value:e}),t}}function ge(n){const e=w(r=>n.subscribe(r),[n]),t=w(()=>n.getSnapshot(),[n]),s=b(e,t,t);return P(()=>n.retain(),[n]),s}const z=Object.freeze({});function F(n){return L(n)}function we(n){const e=$(n.id),t=new Map,s=r=>{const i=t.get(r);if(i!==void 0)return i;const o=F({id:`${e}/${r}`});return t.set(r,o),o};return Object.defineProperty(s,"id",{enumerable:!0,value:e}),Object.freeze(s)}function Ee(n){const e=O(n.target.entries),t="props"in n?n.props??z:z;return e.length===0?null:e.map(s=>E(K,{key:s.id},E(W,{authority:B(s),contribution:s.value,contributionId:s.id,slotProps:t,targetId:n.target.id})))}export{d as ContributionError,Ee as Slot,F as defineSlot,we as defineSwitchSlot,Se as requiresModels,_ as useCommand,k as useCommands,ye as useModel,O as useReadable,ge as useResource,ze as useSelector};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|