@opetope/react 0.1.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +266 -0
  4. package/README.ru.md +266 -0
  5. package/dist/command-hook-controller.d.ts +33 -0
  6. package/dist/command-hook.d.ts +13 -0
  7. package/dist/command.d.ts +26 -0
  8. package/dist/commands-hook.d.ts +10 -0
  9. package/dist/contribution-frame-1td5XTES.js +2 -0
  10. package/dist/contribution-frame-1td5XTES.js.map +1 -0
  11. package/dist/contribution-frame.d.ts +38 -0
  12. package/dist/errors.d.ts +9 -0
  13. package/dist/feature-boundary.d.ts +29 -0
  14. package/dist/feature-demand.d.ts +21 -0
  15. package/dist/idle-subscription.d.ts +3 -0
  16. package/dist/index.d.ts +11 -0
  17. package/dist/index.js +2 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/integration.d.ts +5 -0
  20. package/dist/integration.js +2 -0
  21. package/dist/integration.js.map +1 -0
  22. package/dist/model-binding.d.ts +21 -0
  23. package/dist/model-hook.d.ts +12 -0
  24. package/dist/model-selection-snapshot.d.ts +32 -0
  25. package/dist/model-selection-store.d.ts +17 -0
  26. package/dist/mount-context.d.ts +4 -0
  27. package/dist/mount-frame.d.ts +56 -0
  28. package/dist/mount-provider.d.ts +6 -0
  29. package/dist/readable-hooks.d.ts +3 -0
  30. package/dist/requires-models.d.ts +17 -0
  31. package/dist/resource-hook.d.ts +4 -0
  32. package/dist/scenario-diagnostics.d.ts +13 -0
  33. package/dist/scenario-mount.d.ts +17 -0
  34. package/dist/scenario-slot.d.ts +29 -0
  35. package/dist/scenario-types.d.ts +57 -0
  36. package/dist/scenario-wait.d.ts +20 -0
  37. package/dist/scenario.d.ts +5 -0
  38. package/dist/slot.d.ts +40 -0
  39. package/dist/testing.d.ts +5 -0
  40. package/dist/testing.js +4 -0
  41. package/dist/testing.js.map +1 -0
  42. package/package.json +77 -0
@@ -0,0 +1,10 @@
1
+ import type { Call } from '@opetope/core';
2
+ import type { CommandHook } from './command-hook.js';
3
+ type HookOf<Command> = Command extends Call<infer Input, infer Output> ? CommandHook<Input, Output> : never;
4
+ type CommandsHook<Commands> = {
5
+ readonly [Key in keyof Commands]: HookOf<Commands[Key]>;
6
+ };
7
+ declare function useCommands<Commands extends {
8
+ readonly [Key in keyof Commands]: Call<never, unknown>;
9
+ }>(commands: Commands): CommandsHook<Commands>;
10
+ export { useCommands };
@@ -0,0 +1,2 @@
1
+ import{jsx as m}from"react/jsx-runtime";import{useContext as B,createContext as E,useCallback as L,useRef as v,useMemo as p,useSyncExternalStore as M,useEffect as I,useInsertionEffect as y,useLayoutEffect as b,memo as T,useState as q}from"react";import{markCancellation as D,isCallTarget as V,isModel as P,isReadable as S,createLifetimeController as W,createCallController as K,reportDetachedError as g,getContributionOwner as G,createState as H,createAggregateError as J}from"@opetope/core/internal";import{requireContributionModelPlans as N,openContributionModels as Q}from"@opetope/runtime/internal";class d extends Error{code;name="ContributionError";constructor(e,n){super(n),this.code=e,e==="inactive"&&D(this)}}const R=E(void 0);function U(){const t=B(R);if(t===void 0)throw new d("missing","Opetope hook was called outside a contribution mount.");return t}const X=t=>t;function O(t,e,n=Object.is){const i=L(u=>t.subscribe(u),[t]),r=v(void 0);let o=r.current;o===void 0&&(o={hasValue:!1,value:void 0},r.current=o);const s=p(()=>{let u=!1,l,c;return()=>{const f=t.getSnapshot();if(u&&Object.is(l,f))return c;const h=e(f);if(!u){let w=h;return o.hasValue&&n(o.value,h)&&(w=o.value),u=!0,l=f,c=w,c}return n(c,h)?(l=f,c):(l=f,c=h,c)}},[o,n,t,e]),a=M(i,s,s);return I(()=>{o.hasValue=!0,o.value=a},[o,a]),a}function A(t){return O(t,X)}const C=new WeakMap;function j(t){if(arguments.length>1)throw new d("binding-invalid","Command follows the target it binds; binding options are forbidden.");if(!V(t))throw new d("binding-invalid","Command target is not authentic.");return C.set(t,{target:t}),t}function Y(t){const e=C.get(t);if(e===void 0)throw new d("binding-invalid","Command binding is not authentic.");return e}function k(t){return typeof t=="object"&&t!==null&&C.has(t)}const Z=Symbol("opetope.model-binding"),_=Symbol("opetope.model-binding-identity"),ee=Object.freeze({}),F=new WeakMap;function te(t,e,n){if(typeof n!="string")throw new d("binding-invalid",`Model ${t.id} accepts only string fields.`);const i=Object.getOwnPropertyDescriptor(e,n);if((i==null?void 0:i.enumerable)!==!0||!("value"in i))throw new d("binding-invalid",`Model ${t.id}.${n} requires an enumerable data binding.`);return i.value}function ne(t,e){const n=[],i=Reflect.ownKeys(e);if(i.length===0)throw new d("binding-invalid",`Model ${t.id} requires at least one field.`);for(const r of i){const o=te(t,e,r);if(!(S(o)||typeof o=="function")){if(!k(o))throw new d("binding-invalid",`Model ${t.id}.${String(r)} requires a Readable or a bound command.`);n.push(o)}}return Object.freeze(n)}function x(t,e){if(!P(t))throw new d("binding-invalid","Model declaration is not authentic.");if(typeof e!="object"||e===null)throw new d("binding-invalid",`Model ${t.id} requires a record binding.`);const n=ne(t,e),i=Object.freeze({[Z]:ee,[_]:!0});return F.set(i,{commands:n,declaration:t,model:e}),i}function ie(t){const e=F.get(t);if(e===void 0)throw new d("binding-invalid","Model binding is not authentic.");return e}let re=0;function oe(t){const e=new Map,n=[],i=new Set;for(const r of t){const o=ie(r);if(e.has(o.declaration))throw new d("duplicate",`Model ${o.declaration.id} is bound twice in one mount.`);e.set(o.declaration,o.model);for(const s of o.commands)i.has(s)||(i.add(s),n.push(s))}return{commands:Object.freeze(n),frame:{commandRecords:new Map,models:e}}}class se{frame;authority;boundCommands;closeTicket=0;isActive;lifetime;onClose;released=!1;rendererAttached=!1;renderListeners=new Set;state="detached";constructor(e,n){const i=++re;this.lifetime=W({id:`contribution.${String(i)}`}),this.isActive=n==null?void 0:n.isActive,this.onClose=n==null?void 0:n.onClose;const{commands:r,frame:o}=oe(e);this.frame=o,this.boundCommands=r,this.buildCommandRecords(i)}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 d("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 i=!0;return()=>{i&&(i=!1,this.renderListeners.delete(n))}};buildCommandRecords(e){let n=0;for(const i of this.boundCommands){Y(i);const r=K(i,{captureAuthority:this.captureAuthority,id:`contributionCommand.${String(e)}.${String(++n)}`,scheduling:{policy:"parallel"}});this.frame.commandRecords.set(i,{controller:r,invoker:Object.freeze({run:r.run})})}}captureAuthority=()=>{const e=this.authority;if(this.state!=="active"||e===void 0)throw new d("inactive","Contribution mount is not active.");return e.assertCurrent(),e};createAuthority(e){return Object.freeze({assertCurrent:()=>{if(this.state!=="active")throw new d("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 ae({children:t,mount:e}){const n=M(e.subscribeRenderable,e.getRenderableSnapshot,e.getRenderableSnapshot);return y(()=>{if(e.attach())return()=>e.detach()},[e]),b(()=>{n||e.releaseClosedFrame()},[e,n]),n?m(R,{value:e.frame,children:t}):null}function z(t){const e=t.bundle;t.bundle=void 0,e==null||e.frame.close()}function de(t,e){if(typeof e!="object"||e===null)return x(t,e);const n=Object.entries(e).map(([i,r])=>[i,S(r)||typeof r=="function"||k(r)?r:j(r)]);return x(t,Object.fromEntries(n))}function ce(t){return G(t)}function $(t,e){try{t()}catch(n){throw J([e,n],"Contribution mount setup and cleanup failed.",{cause:e})}throw e}function ue(t,e,n,i){const r=H(e);try{const o=N(t.models),s=o.length===0?void 0:Q(o,r,void 0);return{bundle:s,models:[...(i==null?void 0:i.models)??[],...(s==null?void 0:s.models)??[],...n],props:r}}catch(o){return $(r.close,o)}}function le(t,e,n,i){const{bundle:r,models:o,props:s}=ue(t,e,n,i);let a=!1;const u=()=>{if(!a){a=!0;try{s.close()}finally{r==null||r.close().catch(g)}}};try{return{frame:new se(o.map(([c,f])=>de(c,f)),{isActive:()=>!a,onClose:u}),isOpen:()=>!a,props:s}}catch(l){return $(u,l)}}function fe(t,e){return m(t,{...e})}function he({bundle:t,contribution:e}){const n=A(t.props),i=p(()=>e.props===void 0?n:e.props(n),[n,e]);return m(ae,{mount:t.frame,children:fe(e.Component,i)})}function me({authority:t,contribution:e,fixtures:n,slotProps:i}){const r=p(()=>({authority:t,bundle:void 0,contribution:e,fixtures:n,retired:!1}),[t,e,n]),[o,s]=q(void 0),a=(o==null?void 0:o.owner)===r?o.bundle:void 0,u=v(i);return b(()=>{u.current=i}),y(()=>()=>{r.retired=!0,queueMicrotask(()=>{try{z(r)}catch(l){g(l)}})},[r]),b(()=>{const l=r.bundle??(r.bundle=le(r.contribution,u.current,r.fixtures??[],r.authority));return s(c=>(c==null?void 0:c.owner)===r?c:{bundle:l,owner:r}),()=>{r.retired&&z(r)}},[r]),b(()=>{(a==null?void 0:a.isOpen())===!0&&a.props.set(i)},[a,i]),a===void 0?null:m(he,{bundle:a,contribution:e})}const be=T(me);export{d as C,be as M,A as a,O as b,ce as c,j as d,U as u};
2
+ //# sourceMappingURL=contribution-frame-1td5XTES.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contribution-frame-1td5XTES.js","sources":["../src/errors.ts","../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 { markCancellation } from '@opetope/core/internal';\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 override readonly name = 'ContributionError';\n\n constructor(\n readonly code: 'binding-invalid' | 'duplicate' | 'inactive' | 'missing',\n message: string,\n ) {\n super(message);\n\n if (code === 'inactive') markCancellation(this);\n }\n}\n","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, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { ComponentType, ReactElement } from 'react';\n\nimport type { 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, requireContributionModelPlans } from '@opetope/runtime/internal';\nimport type { ContributionModelBundle } from '@opetope/runtime/internal';\n\nimport { bindCommand, isCommandBinding } from './command';\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 = { readonly models: readonly (readonly [AnyModel, unknown])[] };\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 readonly fixtures?: readonly ModelFixture[];\n readonly slotProps: object;\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\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\n try {\n const plans = requireContributionModelPlans(contribution.models);\n const bundle = plans.length === 0 ? undefined : openContributionModels(plans, props, undefined);\n\n return {\n bundle,\n models: [...(owner?.models ?? []), ...(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 * 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(ContributionMount);\n\nexport { contributionAuthority, MemoizedContributionMount as ContributionMount };\nexport type { ContributionValue, ModelFixture };\n"],"names":["ContributionError","code","message","markCancellation","FrameContext","createContext","useFrame","frame","useContext","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","cleanupFailedMount","close","cause","createAggregateError","contributionModels","contribution","slotProps","fixtures","props","createState","plans","requireContributionModelPlans","openContributionModels","createMountBundle","closed","createComponentElement","Component","MountedContribution","committedProps","componentProps","ContributionMount","committed","setCommitted","useState","committedSlotProps","created","current","MemoizedContributionMount","memo"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { ComponentType, ReactElement } from 'react';
2
+ import type { ModelIdentity } from '@opetope/core/internal';
3
+ type AnyModel = ModelIdentity;
4
+ type ContributionAuthority = {
5
+ readonly models: readonly (readonly [AnyModel, unknown])[];
6
+ };
7
+ type ModelFixture = readonly [declaration: AnyModel, model: unknown];
8
+ type ContributionValue = {
9
+ readonly Component: ComponentType<never>;
10
+ readonly models?: readonly object[] | undefined;
11
+ readonly props?: ((slotProps: never) => object) | undefined;
12
+ };
13
+ type ContributionMountProps = {
14
+ readonly authority?: ContributionAuthority | undefined;
15
+ readonly contribution: ContributionValue;
16
+ readonly fixtures?: readonly ModelFixture[];
17
+ readonly slotProps: object;
18
+ };
19
+ /**
20
+ * D70: the authority of a contribution belongs to the entry a feature published, not to the value it wrote. One
21
+ * static contribution object may be published by two features, and each mount must read the models of its own.
22
+ */
23
+ declare function contributionAuthority(entry: unknown): ContributionAuthority | undefined;
24
+ /**
25
+ * D188: a mount is created in commit, never in render. The first pass renders nothing, the layout effect builds the
26
+ * bundle from the props that committed and publishes it, and React flushes that update before paint. A render that
27
+ * is thrown away — StrictMode's double invoke, an abandoned concurrent pass, a suspended sibling — therefore
28
+ * creates no models at all, so there is nothing to sweep, adopt or replace.
29
+ */
30
+ declare function ContributionMount({ authority, contribution, fixtures, slotProps, }: ContributionMountProps): ReactElement | null;
31
+ /**
32
+ * One mount, one memo: `Slot` recreates its children whenever `entries` changes, so publishing or withholding one
33
+ * contribution used to re-render every other mount of that target. The props are the contribution, the slot props
34
+ * and the fixtures, all of which are stable while nothing about that mount changed.
35
+ */
36
+ declare const MemoizedContributionMount: import("react").MemoExoticComponent<typeof ContributionMount>;
37
+ export { contributionAuthority, MemoizedContributionMount as ContributionMount };
38
+ export type { ContributionValue, ModelFixture };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * One class per subject (D69): after D85 the subject is the contribution mount, which is what grants models and
3
+ * fences commands, so model binding failures are its codes rather than a second class.
4
+ */
5
+ export declare class ContributionError extends Error {
6
+ readonly code: 'binding-invalid' | 'duplicate' | 'inactive' | 'missing';
7
+ readonly name = "ContributionError";
8
+ constructor(code: 'binding-invalid' | 'duplicate' | 'inactive' | 'missing', message: string);
9
+ }
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { FeatureDemandSource } from './feature-demand.js';
3
+ /**
4
+ * D177: a branch may be a node or a render callback. The callback receives what only the boundary knows — the ready
5
+ * instance, or the failure with the retry that belongs to this demand — so a ready consumer needs no second
6
+ * `useFeature` and no second lease. A function is never a `ReactNode`, so the two forms cannot be confused.
7
+ */
8
+ type FeatureBoundaryChildren<Ready> = ((instance: Ready) => ReactNode) | ReactNode;
9
+ type FeatureBoundaryErrorContent = ((context: {
10
+ readonly error: unknown;
11
+ readonly retry: () => void;
12
+ }) => ReactNode) | ReactNode;
13
+ /** The boundary keeps the feature open while it is mounted; its UI enters the host through slots (D85). */
14
+ type FeatureBoundaryProps<Ready = unknown> = {
15
+ readonly children: FeatureBoundaryChildren<Ready>;
16
+ readonly demand: FeatureDemandSource<Ready>;
17
+ readonly error: FeatureBoundaryErrorContent;
18
+ readonly fallback: ReactNode;
19
+ };
20
+ /** One class per subject with the state in `code` (D69): the boundary is its own subject, not a feature failure. */
21
+ declare class FeatureBoundaryError extends Error {
22
+ readonly code: 'missing';
23
+ readonly name = "FeatureBoundaryError";
24
+ constructor(code: 'missing', message: string);
25
+ }
26
+ declare function useFeatureRetry(): () => void;
27
+ declare function FeatureBoundary<Ready>({ children, demand, error, fallback }: FeatureBoundaryProps<Ready>): ReactNode;
28
+ export { FeatureBoundary, FeatureBoundaryError, useFeatureRetry };
29
+ export type { FeatureBoundaryProps };
@@ -0,0 +1,21 @@
1
+ interface FeatureDemandLease {
2
+ readonly release: () => Promise<void>;
3
+ readonly settled: Promise<void>;
4
+ }
5
+ interface FeatureDemandState<Ready> {
6
+ readonly error: unknown | null;
7
+ readonly instance: Ready | null;
8
+ }
9
+ interface FeatureDemandSource<Ready> {
10
+ acquire(): FeatureDemandLease;
11
+ getState(): FeatureDemandState<Ready>;
12
+ subscribe(listener: () => void): () => void;
13
+ }
14
+ interface FeatureDemandResult<Ready> {
15
+ /** Resolves when the attempt it started has settled, in success and in failure alike (D198). */
16
+ readonly retry: () => Promise<void>;
17
+ readonly state: FeatureDemandState<Ready>;
18
+ }
19
+ declare function useFeature<Ready>(source: FeatureDemandSource<Ready>): FeatureDemandResult<Ready>;
20
+ export { useFeature };
21
+ export type { FeatureDemandLease, FeatureDemandResult, FeatureDemandSource, FeatureDemandState };
@@ -0,0 +1,3 @@
1
+ declare const subscribeIdle: () => (() => void);
2
+ declare const idleRevision: () => number;
3
+ export { idleRevision, subscribeIdle };
@@ -0,0 +1,11 @@
1
+ export type { CommandOutcome } from './command.js';
2
+ export { useCommand } from './command-hook.js';
3
+ export type { CommandHook } from './command-hook.js';
4
+ export { useCommands } from './commands-hook.js';
5
+ export { ContributionError } from './errors.js';
6
+ export { useModel } from './model-hook.js';
7
+ export { useReadable, useSelector } from './readable-hooks.js';
8
+ export { requiresModels } from './requires-models.js';
9
+ export { useResource } from './resource-hook.js';
10
+ export { defineSlot, defineSwitchSlot, Slot } from './slot.js';
11
+ export type { SlotContribution, SlotTarget, SwitchSlotTarget } from './slot.js';
package/dist/index.js ADDED
@@ -0,0 +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 I,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,u as p,a as O,M as W,c as B}from"./contribution-frame-1td5XTES.js";import{b as Re}from"./contribution-frame-1td5XTES.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(P=>Y(P,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 I(()=>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,slotProps:t})))}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,Re as useSelector};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/command-hook-controller.ts","../src/command-hook.ts","../src/idle-subscription.ts","../src/commands-hook.ts","../src/model-selection-snapshot.ts","../src/model-selection-store.ts","../src/model-hook.ts","../src/requires-models.ts","../src/resource-hook.ts","../src/slot.tsx"],"sourcesContent":["import { CallError } from '@opetope/core';\nimport { compatibleAbortReason, isCancellation, reportDetachedError } from '@opetope/core/internal';\nimport type { CallRunOptions } from '@opetope/core/internal';\n\nimport type { CommandOutcome } from './command';\nimport { ContributionError } from './errors';\n\ntype CommandRunOptions<Output> = Readonly<{\n onFailure?: (error: unknown) => PromiseLike<void> | void;\n onSuccess?: (value: Output) => PromiseLike<void> | void;\n signal?: AbortSignal;\n}>;\n\ntype CommandRun<Input, Output> = [Input] extends [void]\n ? (input?: Input, options?: CommandRunOptions<Output>) => Promise<CommandOutcome<Output>>\n : (input: Input, options?: CommandRunOptions<Output>) => Promise<CommandOutcome<Output>>;\n\ntype CommandInvoker = Readonly<{\n run: (input: never, options?: CallRunOptions<unknown>) => Promise<unknown>;\n}>;\n\ntype CommandNotification<Output> = (inFlight: boolean, outcome?: CommandOutcome<Output>) => void;\n\ntype CommandRequest<Input, Output> = {\n input: Input | undefined;\n options: CommandRunOptions<Output> | undefined;\n readonly promise: Promise<CommandOutcome<Output>>;\n resolve: ((outcome: CommandOutcome<Output>) => void) | undefined;\n};\n\nfunction failureOutcome<Output>(options: CommandRunOptions<Output> | undefined, error: unknown): CommandOutcome<never> {\n if (isCancellation(error)) return { reason: error, status: 'cancelled' };\n\n try {\n const notification = options?.onFailure?.(error);\n\n if (notification !== undefined) void Promise.resolve(notification).catch(reportDetachedError);\n } catch (callbackError) {\n reportDetachedError(callbackError);\n }\n\n return { error, status: 'failed' };\n}\n\nfunction runOptions<Output>(options: CommandRunOptions<Output> | undefined): CallRunOptions<Output> | undefined {\n const onSuccess = options?.onSuccess;\n const signal = options?.signal;\n\n if (onSuccess === undefined) return signal === undefined ? undefined : { signal };\n\n return signal === undefined ? { onSuccess } : { onSuccess, signal };\n}\n\nfunction requestOf<Input, Output>(\n input: Input,\n options: CommandRunOptions<Output> | undefined,\n): CommandRequest<Input, Output> {\n let resolve!: (outcome: CommandOutcome<Output>) => void;\n const promise = new Promise<CommandOutcome<Output>>(settle => {\n resolve = settle;\n });\n\n return { input, options: options === undefined ? undefined : { ...options }, promise, resolve };\n}\n\nfunction settle<Input, Output>(request: CommandRequest<Input, Output>, outcome: CommandOutcome<Output>): void {\n const resolve = request.resolve;\n request.input = undefined;\n request.options = undefined;\n request.resolve = undefined;\n resolve?.(outcome);\n}\n\n/**\n * D203: the consumer keeps a status, not a schedule. Every `run` reaches the call, and the policy the call was\n * created with — `queue`, `latest`, `parallel`, `once`, `singleFlight` — decides what happens to it. What stays\n * local is what only this consumer knows: whether it is mounted, its own input signal, and the outcome it shows.\n */\nclass CommandHookController<Input, Output> {\n readonly run: CommandRun<Input, Output> = ((input: Input, options?: CommandRunOptions<Output>) => {\n if (this.notify === undefined) {\n return Promise.resolve({\n reason: new ContributionError('inactive', 'Command consumer is not mounted.'),\n status: 'cancelled' as const,\n });\n }\n\n const aborted = this.cancelledInput(options);\n\n if (aborted !== undefined) return Promise.resolve(aborted);\n\n const request = requestOf(input, options);\n this.start(request);\n\n return request.promise;\n }) as CommandRun<Input, Output>;\n private inFlight = 0;\n private notify: CommandNotification<Output> | undefined;\n\n constructor(private readonly invoker: CommandInvoker) {}\n\n attach(notify: CommandNotification<Output>): () => void {\n this.notify = notify;\n\n return () => {\n if (this.notify !== notify) return;\n\n this.notify = undefined;\n };\n }\n\n synchronize(): void {\n this.notify?.(this.inFlight > 0);\n }\n\n private aborted(signal: AbortSignal): CommandOutcome<never> {\n return {\n reason: new CallError('cancelled', 'Command input was cancelled.', compatibleAbortReason(signal)),\n status: 'cancelled',\n };\n }\n\n private cancelledInput(options: CommandRunOptions<Output> | undefined): CommandOutcome<never> | undefined {\n return options?.signal?.aborted === true ? this.aborted(options.signal) : undefined;\n }\n\n private finish(request: CommandRequest<Input, Output>, outcome: CommandOutcome<Output>): void {\n if (request.resolve === undefined) return;\n\n this.inFlight -= 1;\n settle(request, outcome);\n this.notify?.(this.inFlight > 0, outcome);\n }\n\n private invoke(request: CommandRequest<Input, Output>): void {\n let invocation: Promise<Output>;\n\n try {\n invocation = this.invoker.run(request.input as never, runOptions(request.options) as never) as Promise<Output>;\n request.input = undefined;\n } catch (error) {\n this.finish(request, failureOutcome(request.options, error));\n\n return;\n }\n\n void invocation.then(\n value => this.finish(request, { status: 'ok', value }),\n (error: unknown) => this.finish(request, failureOutcome(request.options, error)),\n );\n }\n\n private start(request: CommandRequest<Input, Output>): void {\n this.inFlight += 1;\n // Reserve the flight before publishing: a reentrant run must already see this consumer as busy.\n this.notify?.(true);\n this.invoke(request);\n }\n}\n\nexport { CommandHookController };\nexport type { CommandInvoker, CommandRun };\n","import { useInsertionEffect, useLayoutEffect, useMemo, useState } from 'react';\n\nimport type { Call } from '@opetope/core';\n\nimport type { CommandOutcome } from './command';\nimport { CommandHookController } from './command-hook-controller';\nimport type { CommandInvoker, CommandRun } from './command-hook-controller';\nimport { ContributionError } from './errors';\nimport { useFrame } from './mount-context';\n\ninterface CommandHookStatus<Output> {\n readonly inFlight: boolean;\n readonly lastError: unknown | null;\n readonly result: Output | undefined;\n}\n\nconst emptyStatus: CommandHookStatus<never> = { inFlight: false, lastError: null, result: undefined };\n\ntype CommandHook<Input, Output> = CommandHookStatus<Output> & { readonly run: CommandRun<Input, Output> };\n\ntype CommandHookState<Input, Output> = CommandHookStatus<Output> & {\n readonly slot: CommandHookController<Input, Output>;\n};\n\nfunction unchangedState<Input, Output>(\n current: CommandHookState<Input, Output>,\n slot: CommandHookController<Input, Output>,\n inFlight: boolean,\n outcome: CommandOutcome<Output> | undefined,\n): boolean {\n return outcome === undefined && current.slot === slot && current.inFlight === inFlight;\n}\n\nfunction nextState<Input, Output>(\n current: CommandHookState<Input, Output>,\n slot: CommandHookController<Input, Output>,\n inFlight: boolean,\n outcome?: CommandOutcome<Output>,\n): CommandHookState<Input, Output> {\n if (unchangedState(current, slot, inFlight, outcome)) return current;\n\n const { lastError, result } = current.slot === slot ? current : { lastError: null, result: undefined };\n\n if (outcome?.status === 'ok') return { inFlight, lastError: null, result: outcome.value, slot };\n\n if (outcome?.status === 'failed') return { inFlight, lastError: outcome.error, result, slot };\n\n return { inFlight, lastError, result, slot };\n}\n\nfunction useCommandSlot<Input, Output>(invoker: CommandInvoker): CommandHookController<Input, Output> {\n // Slots are inert until commit. An interrupted render cannot fence the committed source or admit its own work.\n const [slots] = useState(() => new WeakMap<object, CommandHookController<Input, Output>>());\n let slot = slots.get(invoker);\n\n if (slot === undefined) {\n slot = new CommandHookController(invoker);\n slots.set(invoker, slot);\n }\n\n return slot;\n}\n\nfunction useCommand<Input, Output>(command: Call<Input, Output>): CommandHook<Input, Output> {\n const frame = useFrame();\n const record = frame.commandRecords.get(command);\n\n if (record === undefined) {\n throw new ContributionError('missing', `Command ${command.id} is not bound in this contribution.`);\n }\n\n const currentSlot = useCommandSlot<Input, Output>(record.invoker);\n const [hookState, setHookState] = useState<CommandHookState<Input, Output>>(() => ({\n inFlight: false,\n lastError: null,\n result: undefined,\n slot: currentSlot,\n }));\n useInsertionEffect(\n () =>\n currentSlot.attach((inFlight, outcome) =>\n setHookState(current => nextState(current, currentSlot, inFlight, outcome)),\n ),\n [currentSlot],\n );\n useLayoutEffect(() => currentSlot.synchronize(), [currentSlot]);\n const { inFlight, lastError, result } = hookState.slot === currentSlot ? hookState : emptyStatus;\n const run = currentSlot.run;\n\n return useMemo(() => ({ inFlight, lastError, result, run }), [inFlight, lastError, result, run]);\n}\n\nexport { useCommand };\nexport type { CommandHook };\n","const idleRelease = (): void => undefined;\nconst subscribeIdle = (): (() => void) => idleRelease;\nconst idleRevision = (): number => 0;\n\nexport { idleRevision, subscribeIdle };\n","import { useInsertionEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react';\n\nimport type { Call } from '@opetope/core';\n\nimport type { CommandOutcome } from './command';\nimport type { CommandHook } from './command-hook';\nimport { CommandHookController } from './command-hook-controller';\nimport type { CommandInvoker } from './command-hook-controller';\nimport { ContributionError } from './errors';\nimport { idleRevision, subscribeIdle } from './idle-subscription';\nimport { useFrame } from './mount-context';\nimport type { Frame } from './mount-frame';\n\ntype CommandSelection = Readonly<Record<string, Call<never, unknown>>>;\ntype HookOf<Command> = Command extends Call<infer Input, infer Output> ? CommandHook<Input, Output> : never;\ntype CommandsHook<Commands> = {\n readonly [Key in keyof Commands]: HookOf<Commands[Key]>;\n};\nconst emptyHooks = Object.freeze({});\n\ntype CommandEntry = {\n detach: (() => void) | undefined;\n hook: CommandHook<never, unknown>;\n readonly invoker: CommandInvoker;\n readonly slot: CommandHookController<never, unknown>;\n};\n\nfunction createEntry(invoker: CommandInvoker): CommandEntry {\n const slot = new CommandHookController<never, unknown>(invoker);\n\n return {\n detach: undefined,\n hook: { inFlight: false, lastError: null, result: undefined, run: slot.run },\n invoker,\n slot,\n };\n}\n\nfunction unchangedHook(previous: CommandHook<never, unknown>, next: CommandHook<never, unknown>): boolean {\n return (\n previous.inFlight === next.inFlight &&\n Object.is(previous.result, next.result) &&\n Object.is(previous.lastError, next.lastError)\n );\n}\n\nfunction outcomeStatus(previous: CommandHook<never, unknown>, outcome: CommandOutcome<unknown> | undefined) {\n if (outcome?.status === 'ok') return { lastError: null, result: outcome.value };\n\n if (outcome?.status === 'failed') return { lastError: outcome.error, result: previous.result };\n\n return previous;\n}\n\nfunction updateEntry(entry: CommandEntry, inFlight: boolean, outcome?: CommandOutcome<unknown>): boolean {\n const previous = entry.hook;\n\n if (outcome === undefined && previous.inFlight === inFlight) return false;\n\n const { lastError, result } = outcomeStatus(previous, outcome);\n const next = { inFlight, lastError, result, run: previous.run };\n\n if (unchangedHook(previous, next)) return false;\n\n entry.hook = next;\n\n return true;\n}\n\nfunction selectEntry(previous: CommandEntry | undefined, invoker: CommandInvoker): CommandEntry {\n return previous?.invoker === invoker ? previous : createEntry(invoker);\n}\n\n/** Selection is inert during render; only a committed selection can admit work or discard an old queue. */\nclass CommandGroup {\n private entries = new Map<PropertyKey, CommandEntry>();\n private readonly listeners = new Set<() => void>();\n private revision = 0;\n\n close(): void {\n for (const entry of this.entries.values()) {\n entry.detach?.();\n entry.detach = undefined;\n }\n\n this.entries.clear();\n }\n\n commit(selected: Map<PropertyKey, CommandEntry>): void {\n for (const [key, entry] of this.entries) {\n if (selected.get(key) === entry) continue;\n\n entry.detach?.();\n entry.detach = undefined;\n }\n\n this.entries = selected;\n\n for (const entry of selected.values()) {\n entry.detach ??= entry.slot.attach((inFlight, outcome) => {\n if (!updateEntry(entry, inFlight, outcome)) return;\n\n this.revision += 1;\n\n for (const listener of this.listeners) listener();\n });\n }\n }\n\n getSnapshot = (): number => this.revision;\n\n prepare(commands: CommandSelection, frame: Frame): Map<PropertyKey, CommandEntry> {\n const selected = new Map<PropertyKey, CommandEntry>();\n\n for (const key of Reflect.ownKeys(commands)) {\n if (!Object.prototype.propertyIsEnumerable.call(commands, key)) continue;\n\n const command = (commands as Readonly<Record<PropertyKey, Call<never, unknown>>>)[key];\n const record = command === undefined ? undefined : frame.commandRecords.get(command);\n\n if (record === undefined) {\n throw new ContributionError('missing', `Command at ${String(key)} is not bound in this contribution.`);\n }\n\n selected.set(key, selectEntry(this.entries.get(key), record.invoker));\n }\n\n return selected;\n }\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener);\n\n return () => {\n this.listeners.delete(listener);\n };\n };\n}\n\nfunction useCommandGroup(commands: CommandSelection): CommandGroup | undefined {\n const groupRef = useRef<CommandGroup | undefined>(undefined);\n const populated = Reflect.ownKeys(commands).some(key => Object.prototype.propertyIsEnumerable.call(commands, key));\n\n return populated ? (groupRef.current ??= new CommandGroup()) : undefined;\n}\n\nfunction useCommands<Commands extends { readonly [Key in keyof Commands]: Call<never, unknown> }>(\n commands: Commands,\n): CommandsHook<Commands> {\n const frame = useFrame();\n const group = useCommandGroup(commands);\n const getSnapshot = group?.getSnapshot ?? idleRevision;\n useSyncExternalStore(group?.subscribe ?? subscribeIdle, getSnapshot, getSnapshot);\n const selected = group?.prepare(commands, frame);\n useInsertionEffect(() => () => group?.close(), [group]);\n useInsertionEffect(() => {\n if (selected !== undefined) group?.commit(selected);\n });\n useLayoutEffect(() => {\n if (selected === undefined) return;\n\n for (const entry of selected.values()) entry.slot.synchronize();\n });\n\n return (\n selected === undefined ? emptyHooks : Object.fromEntries([...selected].map(([key, entry]) => [key, entry.hook]))\n ) as CommandsHook<Commands>;\n}\n\nexport { useCommands };\n","import type { Readable } from '@opetope/core';\nimport { isCallTarget, isReadable } from '@opetope/core/internal';\n\nimport { ContributionError } from './errors';\nimport type { AnyCommand } from './model-binding';\n\ninterface SelectionRead {\n <Value>(source: Readable<Value>): Value;\n <Value, Selected>(source: Readable<Value>, select: (value: Value) => Selected): Selected;\n}\n\ntype SelectionContext = Readonly<{ read: SelectionRead }>;\ntype NonSelectionRecord =\n | readonly unknown[]\n | ((...arguments_: never[]) => unknown)\n | (abstract new (...arguments_: never[]) => unknown)\n | Date\n | PromiseLike<unknown>\n | ReadonlyMap<unknown, unknown>\n | ReadonlySet<unknown>\n | RegExp\n | WeakMap<object, unknown>\n | WeakSet<object>;\n/** A named interface needs no open index signature; container and callable results are not field selections. */\ntype SelectionShape<Selected extends object> = Selected extends NonSelectionRecord ? never : Selected;\ntype SelectionRecord = Readonly<Record<PropertyKey, unknown>>;\ntype SelectionSnapshot = Readonly<{\n commands: Readonly<Record<PropertyKey, AnyCommand>>;\n sources: ReadonlySet<Readable<unknown>>;\n values: SelectionRecord;\n}>;\n\nfunction readSource<Value>(source: Readable<Value>, sources: Map<Readable<unknown>, unknown>): Value {\n if (!isReadable(source)) {\n throw new ContributionError('binding-invalid', 'Model selection read requires a Readable.');\n }\n\n if (sources.has(source)) return sources.get(source) as Value;\n\n const value = source.getSnapshot();\n sources.set(source, value);\n\n return value;\n}\n\nfunction collectSelection(\n select: (context: SelectionContext) => object,\n sources: Map<Readable<unknown>, unknown>,\n): SelectionRecord {\n const read = <Value, Selected>(source: Readable<Value>, project?: (value: Value) => Selected) => {\n const value = readSource(source, sources);\n\n return project === undefined ? value : project(value);\n };\n const selected = select({ read });\n\n if (typeof selected !== 'object' || selected === null || Array.isArray(selected)) {\n throw new ContributionError('binding-invalid', 'Model selection requires a record.');\n }\n\n return Object.fromEntries(\n Reflect.ownKeys(selected)\n .filter(key => Object.prototype.propertyIsEnumerable.call(selected, key))\n .map(key => [key, (selected as SelectionRecord)[key]]),\n );\n}\n\nfunction equalSelection(previous: SelectionSnapshot, values: SelectionRecord, sources: ReadonlySet<Readable<unknown>>) {\n const keys = Reflect.ownKeys(values);\n const previousKeys = Reflect.ownKeys(previous.values);\n\n if (keys.length !== previousKeys.length || sources.size !== previous.sources.size) return false;\n\n for (const source of sources) if (!previous.sources.has(source)) return false;\n\n return keys.every((key, index) => previousKeys[index] === key && Object.is(previous.values[key], values[key]));\n}\n\ntype ModelSelector<Model> = (model: Model, context: SelectionContext) => object;\ntype SelectionReaderRef<Model> = { readonly current: ModelSelector<Model> | undefined };\n\nconst emptySelectionSnapshot: SelectionSnapshot = { commands: {}, sources: new Set(), values: {} };\n\nfunction selectionCommands(values: SelectionRecord): Readonly<Record<PropertyKey, AnyCommand>> {\n return Object.fromEntries(\n Reflect.ownKeys(values)\n .filter(key => isCallTarget(values[key]))\n .map(key => [key, values[key]]),\n ) as Readonly<Record<PropertyKey, AnyCommand>>;\n}\n\n/**\n * D214: one reader per model, not per render. The author's selector arrives through a ref, so an inline arrow does not\n * rebuild the reader; the fast path — nothing observed has moved — belongs to the selector that computed the snapshot,\n * so a new selector is always called again, its result compared field by field, and an equal selection keeps the\n * previous snapshot. A rejected or abandoned render still changes nothing: only a committed selection subscribes.\n */\nfunction createSelectionSnapshot<Model>(model: Model, selectRef: SelectionReaderRef<Model>): () => SelectionSnapshot {\n let observed: Map<Readable<unknown>, unknown> | undefined;\n let observedSelect: ModelSelector<Model> | undefined;\n let snapshot: SelectionSnapshot = emptySelectionSnapshot;\n\n return () => {\n const select = selectRef.current;\n\n if (select === undefined) return emptySelectionSnapshot;\n\n if (\n observed !== undefined &&\n observedSelect === select &&\n [...observed].every(([source, value]) => Object.is(source.getSnapshot(), value))\n ) {\n return snapshot;\n }\n\n const nextObserved = new Map<Readable<unknown>, unknown>();\n const values = collectSelection(context => select(model, context), nextObserved);\n const sources = new Set(nextObserved.keys());\n\n if (observed === undefined || !equalSelection(snapshot, values, sources)) {\n snapshot = { commands: selectionCommands(values), sources, values };\n }\n\n observed = nextObserved;\n observedSelect = select;\n\n return snapshot;\n };\n}\n\nexport { createSelectionSnapshot, emptySelectionSnapshot };\nexport type { ModelSelector, SelectionContext, SelectionShape };\n","import type { Readable } from '@opetope/core';\nimport { createAggregateError } from '@opetope/core/internal';\n\ntype Subscription = { dispose: (() => void) | undefined };\ntype Listener = Readonly<{ notify: () => void }>;\n\nfunction releaseSubscriptions(subscriptions: Iterable<Subscription>, failures: unknown[] = []): void {\n for (const subscription of subscriptions) {\n const dispose = subscription.dispose;\n subscription.dispose = undefined;\n\n try {\n dispose?.();\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length === 1) throw failures[0];\n\n if (failures.length > 1) throw createAggregateError(failures, 'Model selection sources failed to release.');\n}\n\n/** A subscription set, not a scheduler: source notifications use React's snapshot consistency check. */\nclass ModelSelectionStore {\n private enabled = false;\n private readonly listeners = new Set<Listener>();\n private sources: ReadonlySet<Readable<unknown>> = new Set();\n private readonly subscriptions = new Map<Readable<unknown>, Subscription>();\n\n close(): void {\n this.enabled = false;\n this.sources = new Set();\n this.releaseAll();\n }\n\n commit(sources: ReadonlySet<Readable<unknown>>): void {\n this.sources = sources;\n this.enabled = true;\n\n try {\n this.synchronize();\n } catch (error) {\n this.releaseAll([error]);\n }\n }\n\n subscribe = (notify: () => void): (() => void) => {\n const listener = { notify };\n this.listeners.add(listener);\n\n try {\n this.synchronize();\n } catch (error) {\n this.listeners.delete(listener);\n this.releaseAll([error]);\n }\n\n return () => {\n this.listeners.delete(listener);\n\n if (this.listeners.size === 0) this.releaseAll();\n };\n };\n\n private add(source: Readable<unknown>): void {\n const subscription: Subscription = { dispose: undefined };\n // Reserve before foreign code: a synchronous notification may replace or close this selection.\n this.subscriptions.set(source, subscription);\n const dispose = source.subscribe(this.notify);\n\n if (typeof dispose !== 'function') throw new TypeError('Model selection source must return a disposer.');\n\n if (this.subscriptions.get(source) === subscription) subscription.dispose = dispose;\n else dispose();\n }\n\n private notify = (): void => {\n for (const listener of [...this.listeners]) {\n if (this.listeners.has(listener)) listener.notify();\n }\n };\n\n private releaseAll(failures?: unknown[]): void {\n const subscriptions = [...this.subscriptions.values()];\n this.subscriptions.clear();\n releaseSubscriptions(subscriptions, failures);\n }\n\n private removeUnselected(): void {\n const removed: Subscription[] = [];\n\n for (const [source, subscription] of this.subscriptions) {\n if (this.sources.has(source)) continue;\n\n this.subscriptions.delete(source);\n removed.push(subscription);\n }\n\n releaseSubscriptions(removed);\n }\n\n private synchronize(): void {\n this.removeUnselected();\n\n for (const source of this.sources) {\n if (!this.enabled || this.listeners.size === 0) return;\n\n if (this.sources.has(source) && !this.subscriptions.has(source)) this.add(source);\n }\n }\n}\n\nexport { ModelSelectionStore };\n","import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';\n\nimport type { Call, ModelOf } from '@opetope/core';\n\nimport type { CommandHook } from './command-hook';\nimport { useCommands } from './commands-hook';\nimport { ContributionError } from './errors';\nimport { subscribeIdle } from './idle-subscription';\nimport type { AnyModel } from './model-binding';\nimport { createSelectionSnapshot, emptySelectionSnapshot } from './model-selection-snapshot';\nimport type { ModelSelector, SelectionContext, SelectionShape } from './model-selection-snapshot';\nimport { ModelSelectionStore } from './model-selection-store';\nimport { useFrame } from './mount-context';\n\ntype SelectedValue<Value> = Value extends Call<infer Input, infer Output> ? CommandHook<Input, Output> : Value;\ntype ModelSelection<Selected> = { readonly [Key in keyof Selected]: SelectedValue<Selected[Key]> };\n\nconst readEmptySnapshot = () => emptySelectionSnapshot;\n\nfunction useSelectionStore(selected: boolean): ModelSelectionStore | undefined {\n const storeRef = useRef<ModelSelectionStore | undefined>(undefined);\n\n return selected ? (storeRef.current ??= new ModelSelectionStore()) : undefined;\n}\n\n/** D205: resolve only a granted model, explicitly read data, and reuse the ordinary per-key command consumers. */\nfunction useModel<Declaration extends AnyModel>(declaration: Declaration): ModelOf<Declaration>;\nfunction useModel<Declaration extends AnyModel, const Selected extends object>(\n declaration: Declaration,\n select: (model: ModelOf<Declaration>, context: SelectionContext) => Selected & SelectionShape<Selected>,\n): ModelSelection<Selected>;\nfunction useModel<Declaration extends AnyModel>(\n declaration: Declaration,\n select?: (model: ModelOf<Declaration>, context: SelectionContext) => object,\n): unknown {\n const frame = useFrame();\n\n if (!frame.models.has(declaration)) {\n throw new ContributionError('missing', `Model ${declaration.id} is not granted to this contribution.`);\n }\n\n const model = frame.models.get(declaration) as ModelOf<Declaration>;\n const store = useSelectionStore(select !== undefined);\n // D214: the selector of this render, read by a reader that belongs to the model. Writing the latest callback into\n // a ref is what `useSyncExternalStoreWithSelector` does: the reader only reads it, and never writes to it.\n const selectRef = useRef<ModelSelector<ModelOf<Declaration>> | undefined>(undefined);\n selectRef.current = select;\n const selected = select !== undefined;\n const getSnapshot = useMemo(\n () => (selected ? createSelectionSnapshot(model, selectRef) : readEmptySnapshot),\n [model, selected],\n );\n const snapshot = useSyncExternalStore(store?.subscribe ?? subscribeIdle, getSnapshot, getSnapshot);\n const commands = useCommands(snapshot.commands);\n useLayoutEffect(() => () => store?.close(), [store]);\n useLayoutEffect(() => store?.commit(snapshot.sources), [snapshot, store]);\n\n if (select === undefined) return model;\n\n return Object.fromEntries(\n Reflect.ownKeys(snapshot.values).map(key => [\n key,\n Object.prototype.hasOwnProperty.call(commands, key) ? commands[key] : snapshot.values[key],\n ]),\n );\n}\n\nexport { useModel };\n","import type { FunctionComponent } from 'react';\n\nimport type { ModelIdentity } from '@opetope/core/internal';\n\nimport { ContributionError } from './errors';\n\ntype AnyModel = ModelIdentity;\n\n/**\n * The marker a component uses to state which UI models of its contribution it reads. `slot` checks by type that the\n * contribution grants every model of this list — it may grant more, because the mount serves its whole instance.\n *\n * The runtime authority is the mount frame, not this marker: `useModel` resolves against what the mount was given\n * and refuses anything else. The lint rule `opetope/require-declared-models` checks visible contribution sites and\n * each reader of a per-mount model within the same module. Imported implementations and dynamic declaration lists\n * remain unknown to this syntactic check (D85, D158, D250).\n */\ntype ComponentRequiringModels<Props, Models extends readonly AnyModel[]> = FunctionComponent<Props> & {\n readonly requires: Models;\n};\n\nfunction requiresModels<const Models extends readonly AnyModel[]>(\n models: Models,\n): <Props>(component: FunctionComponent<Props>) => ComponentRequiringModels<Props, Models> {\n const declared = Object.freeze([...models]) as unknown as Models;\n\n return <Props>(component: FunctionComponent<Props>): ComponentRequiringModels<Props, Models> => {\n const existing = (component as { readonly requires?: readonly AnyModel[] }).requires;\n\n if (existing !== undefined) {\n throw new ContributionError('duplicate', 'Component already declares the models it requires.');\n }\n\n Object.defineProperty(component, 'requires', { enumerable: false, value: declared });\n\n return component as ComponentRequiringModels<Props, Models>;\n };\n}\n\nexport { requiresModels };\n","import { useCallback, useEffect, useSyncExternalStore } from 'react';\n\nimport type { Resource, ResourceSnapshot } from '@opetope/runtime';\nimport type { ResourceRequestKey } from '@opetope/runtime/internal';\n\nfunction useResource<Data, Key extends ResourceRequestKey>(resource: Resource<Data, Key>): ResourceSnapshot<Data, Key> {\n const subscribe = useCallback((listener: () => void) => resource.subscribe(listener), [resource]);\n // D199: a resource is a structural contract, so it may be an object with methods. Reading through it keeps the\n // receiver the adapter expects; handing `resource.getSnapshot` to React would call it with none.\n const getSnapshot = useCallback(() => resource.getSnapshot(), [resource]);\n const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n useEffect(() => resource.retain(), [resource]);\n\n return snapshot;\n}\n\nexport { useResource };\n","import type { ComponentType, ReactNode } from 'react';\nimport { createElement, Fragment } from 'react';\n\nimport type { DeclarationId } from '@opetope/core';\nimport { declarationId } from '@opetope/core';\nimport type { ContributionTarget } from '@opetope/core/internal';\nimport { createContributionTarget } from '@opetope/core/internal';\nimport type { ContributionModel } from '@opetope/runtime/internal';\n\nimport { contributionAuthority, ContributionMount } from './contribution-frame';\nimport { useReadable } from './readable-hooks';\n\ntype SlotProperties = object | undefined;\ntype EmptySlotProperties = Readonly<Record<string, never>>;\ntype SlotComponentProperties<Props extends SlotProperties> = [Props] extends [undefined]\n ? EmptySlotProperties\n : Extract<Props, object>;\n\n/**\n * What a contribution publishes: the component, the optional props adapter and the optional UI models the mount\n * creates. Without an adapter the component takes the slot props; with one it takes exactly the adapter result (D85).\n */\ntype SlotContribution<Props extends SlotProperties = undefined> = {\n readonly Component: ComponentType<never>;\n readonly models?: readonly ContributionModel[];\n readonly props?: (slotProps: SlotComponentProperties<Props>) => object;\n};\n\ntype SlotTarget<Props extends SlotProperties = undefined> = ContributionTarget<SlotContribution<Props>>;\n\ntype SwitchSlotTarget<Route extends string, Props extends SlotProperties = undefined> = ((\n route: Route,\n) => SlotTarget<Props>) & {\n readonly id: DeclarationId;\n};\n\ntype SlotRenderProps<Props extends SlotProperties> = [Props] extends [undefined]\n ? {\n readonly props?: never;\n readonly target: SlotTarget<Props>;\n }\n : {\n readonly props: SlotComponentProperties<Props>;\n readonly target: SlotTarget<Props>;\n };\n\nconst emptySlotProperties: EmptySlotProperties = Object.freeze({});\n\nfunction defineSlot<Props extends SlotProperties = undefined>(options: { readonly id: string }): SlotTarget<Props> {\n return createContributionTarget<SlotContribution<Props>>(options);\n}\n\nfunction defineSwitchSlot<Route extends string, Props extends SlotProperties = undefined>(options: {\n readonly id: string;\n}): SwitchSlotTarget<Route, Props> {\n const id = declarationId(options.id);\n const targets = new Map<Route, SlotTarget<Props>>();\n\n const switchSlot = (route: Route): SlotTarget<Props> => {\n const existing = targets.get(route);\n\n if (existing !== undefined) return existing;\n\n const target = defineSlot<Props>({ id: `${id}/${route}` });\n targets.set(route, target);\n\n return target;\n };\n\n Object.defineProperty(switchSlot, 'id', { enumerable: true, value: id });\n\n return Object.freeze(switchSlot) as SwitchSlotTarget<Route, Props>;\n}\n\n/**\n * Every contribution renders inside its own mount: the models its generation owns plus the UI models it declared,\n * created on mount and closed on unmount (D70, D85).\n */\nfunction Slot<Props extends SlotProperties>(options: SlotRenderProps<Props>): ReactNode {\n const entries = useReadable(options.target.entries);\n const slotProps: object = 'props' in options ? (options.props ?? emptySlotProperties) : emptySlotProperties;\n\n if (entries.length === 0) return null;\n\n return entries.map(entry =>\n createElement(\n Fragment,\n { key: entry.id },\n createElement(ContributionMount, {\n authority: contributionAuthority(entry),\n contribution: entry.value,\n slotProps,\n }),\n ),\n );\n}\n\nexport { defineSlot, defineSwitchSlot, Slot };\nexport type { SlotComponentProperties, SlotContribution, SlotProperties, SlotTarget, SwitchSlotTarget };\n"],"names":["failureOutcome","options","error","isCancellation","notification","_a","reportDetachedError","callbackError","runOptions","onSuccess","signal","requestOf","input","resolve","promise","settle","request","outcome","CommandHookController","ContributionError","aborted","invoker","notify","CallError","compatibleAbortReason","invocation","value","emptyStatus","unchangedState","current","slot","inFlight","nextState","lastError","result","useCommandSlot","slots","useState","useCommand","command","record","useFrame","currentSlot","hookState","setHookState","useInsertionEffect","useLayoutEffect","run","useMemo","idleRelease","subscribeIdle","idleRevision","emptyHooks","createEntry","unchangedHook","previous","next","outcomeStatus","updateEntry","entry","selectEntry","CommandGroup","selected","key","listener","commands","frame","useCommandGroup","groupRef","useRef","useCommands","group","getSnapshot","useSyncExternalStore","readSource","source","sources","isReadable","collectSelection","select","project","equalSelection","values","keys","previousKeys","index","emptySelectionSnapshot","selectionCommands","isCallTarget","createSelectionSnapshot","model","selectRef","observed","observedSelect","snapshot","nextObserved","context","releaseSubscriptions","subscriptions","failures","subscription","dispose","createAggregateError","ModelSelectionStore","removed","readEmptySnapshot","useSelectionStore","storeRef","useModel","declaration","store","requiresModels","models","declared","component","useResource","resource","subscribe","useCallback","useEffect","emptySlotProperties","defineSlot","createContributionTarget","defineSwitchSlot","id","declarationId","targets","switchSlot","route","existing","target","Slot","entries","useReadable","slotProps","createElement","Fragment","ContributionMount","contributionAuthority"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export type { Command, CommandOutcome } from './command.js';
2
+ export { FeatureBoundary, FeatureBoundaryError, useFeatureRetry } from './feature-boundary.js';
3
+ export type { FeatureBoundaryProps } from './feature-boundary.js';
4
+ export { useFeature } from './feature-demand.js';
5
+ export type { FeatureDemandLease, FeatureDemandResult, FeatureDemandSource, FeatureDemandState, } from './feature-demand.js';
@@ -0,0 +1,2 @@
1
+ import{jsx as E}from"react/jsx-runtime";import{useCallback as f,useSyncExternalStore as S,useMemo as p,useEffect as x,useState as C,createContext as g,useContext as B}from"react";const y=()=>{};function b(e){const r=f(t=>e.subscribe(t),[e]),u=f(()=>e.getState(),[e]),d=S(r,u,u),n=p(()=>({current:null,source:e}),[e]);x(()=>{const t=e.acquire();return t.settled.catch(y),()=>{t.release().catch(y)}},[e]);const o=f(()=>{if(n.current!==null)return n.current.settled;let t=()=>{};const s={lease:null,settled:new Promise(h=>{t=h})},a=()=>{n.current===s&&(n.current=null),t()};n.current=s;let i;try{i=n.source.acquire()}catch{return a(),s.settled}return s.lease=i,i.settled.catch(y).finally(a),i.release().catch(y),s.settled},[n]);return p(()=>Object.freeze({retry:o,state:d}),[o,d])}const F=g(void 0);class v extends Error{code;name="FeatureBoundaryError";constructor(r,u){super(u),this.code=r}}function w(){const e=B(F);if(e===void 0)throw new v("missing","Feature error hooks may only be used by the error subtree of a FeatureBoundary.");return e}function q(){return w().retry}function j({children:e,demand:r,error:u,fallback:d}){const{retry:n,state:o}=b(r),[t,s]=C(),a=f(()=>{const c={},m=()=>s(l=>(l==null?void 0:l.attempt)===c?void 0:l);s({attempt:c,demand:r});try{n().finally(m)}catch(l){throw m(),l}},[r,n]),i=p(()=>({retry:a}),[a]),h=(t==null?void 0:t.demand)===r;if(x(()=>{s(c=>c===void 0||c.demand===r&&o.instance===null?c:void 0)},[r,o.instance]),o.instance!==null)return typeof e=="function"?e(o.instance):e;if(o.error!==null&&!h){const c=typeof u=="function"?u({error:o.error,retry:a}):u;return E(F,{value:i,children:c})}return d}export{j as FeatureBoundary,v as FeatureBoundaryError,b as useFeature,q as useFeatureRetry};
2
+ //# sourceMappingURL=integration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.js","sources":["../src/feature-demand.ts","../src/feature-boundary.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react';\n\ninterface FeatureDemandLease {\n readonly release: () => Promise<void>;\n readonly settled: Promise<void>;\n}\n\ninterface FeatureDemandState<Ready> {\n readonly error: unknown | null;\n readonly instance: Ready | null;\n}\n\ninterface FeatureDemandSource<Ready> {\n acquire(): FeatureDemandLease;\n getState(): FeatureDemandState<Ready>;\n subscribe(listener: () => void): () => void;\n}\n\ninterface FeatureDemandResult<Ready> {\n /** Resolves when the attempt it started has settled, in success and in failure alike (D198). */\n readonly retry: () => Promise<void>;\n readonly state: FeatureDemandState<Ready>;\n}\n\nconst ignoreFailure = (): void => undefined;\n\nfunction useFeature<Ready>(source: FeatureDemandSource<Ready>): FeatureDemandResult<Ready> {\n const subscribe = useCallback((listener: () => void) => source.subscribe(listener), [source]);\n const getSnapshot = useCallback(() => source.getState(), [source]);\n const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n const retrying = useMemo<{\n current: { lease: FeatureDemandLease | null; readonly settled: Promise<void> } | null;\n readonly source: FeatureDemandSource<Ready>;\n }>(() => ({ current: null, source }), [source]);\n\n useEffect(() => {\n const demand = source.acquire();\n void demand.settled.catch(ignoreFailure);\n\n return () => void demand.release().catch(ignoreFailure);\n }, [source]);\n\n const retry = useCallback((): Promise<void> => {\n // Single-flight per source: a second call joins the attempt already running instead of starting another.\n if (retrying.current !== null) return retrying.current.settled;\n\n // D198: `acquire` runs foreign code, and a host that answers it synchronously may call `retry` again. The\n // attempt is reserved with its own promise before that call, so the reentrant caller joins this attempt\n // instead of taking a second lease of its own.\n let settle = (): void => undefined;\n const attempt = {\n lease: null as FeatureDemandLease | null,\n settled: new Promise<void>(resolve => {\n settle = resolve;\n }),\n };\n const finish = (): void => {\n if (retrying.current === attempt) retrying.current = null;\n\n settle();\n };\n retrying.current = attempt;\n\n let lease: FeatureDemandLease;\n\n try {\n lease = retrying.source.acquire();\n } catch {\n // D198: `retry` answers with the promise of the attempt it started, and an acquisition that refused is an\n // attempt that is over. The refusal belongs to the state the source publishes, which is where the caller\n // reads it; throwing it back here would leave every caller of `retry` waiting for an attempt that ended.\n finish();\n\n return attempt.settled;\n }\n\n attempt.lease = lease;\n void lease.settled.catch(ignoreFailure).finally(finish);\n void lease.release().catch(ignoreFailure);\n\n return attempt.settled;\n }, [retrying]);\n\n return useMemo(() => Object.freeze({ retry, state }), [retry, state]);\n}\n\nexport { useFeature };\nexport type { FeatureDemandLease, FeatureDemandResult, FeatureDemandSource, FeatureDemandState };\n","import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport { useFeature } from './feature-demand';\nimport type { FeatureDemandSource } from './feature-demand';\n\n/**\n * D177: a branch may be a node or a render callback. The callback receives what only the boundary knows — the ready\n * instance, or the failure with the retry that belongs to this demand — so a ready consumer needs no second\n * `useFeature` and no second lease. A function is never a `ReactNode`, so the two forms cannot be confused.\n */\ntype FeatureBoundaryChildren<Ready> = ((instance: Ready) => ReactNode) | ReactNode;\ntype FeatureBoundaryErrorContent =\n | ((context: { readonly error: unknown; readonly retry: () => void }) => ReactNode)\n | ReactNode;\n\n/** The boundary keeps the feature open while it is mounted; its UI enters the host through slots (D85). */\ntype FeatureBoundaryProps<Ready = unknown> = {\n readonly children: FeatureBoundaryChildren<Ready>;\n readonly demand: FeatureDemandSource<Ready>;\n readonly error: FeatureBoundaryErrorContent;\n readonly fallback: ReactNode;\n};\n\n/** Only `retry` is read: the error itself belongs to the `error` subtree a host already renders (D142). */\ntype FeatureBoundaryErrorContext = {\n readonly retry: () => void;\n};\n\n/** D198: the error is hidden for the life of the attempt that was started, not until the error object changes. */\ntype SuppressedError = {\n readonly attempt: object;\n readonly demand: object;\n};\n\nconst ErrorContext = createContext<FeatureBoundaryErrorContext | undefined>(undefined);\n\n/** One class per subject with the state in `code` (D69): the boundary is its own subject, not a feature failure. */\nclass FeatureBoundaryError extends Error {\n override readonly name = 'FeatureBoundaryError';\n\n constructor(\n readonly code: 'missing',\n message: string,\n ) {\n super(message);\n }\n}\n\nfunction useBoundaryErrorContext(): FeatureBoundaryErrorContext {\n const context = useContext(ErrorContext);\n\n if (context === undefined) {\n throw new FeatureBoundaryError(\n 'missing',\n 'Feature error hooks may only be used by the error subtree of a FeatureBoundary.',\n );\n }\n\n return context;\n}\n\nfunction useFeatureRetry(): () => void {\n return useBoundaryErrorContext().retry;\n}\n\nfunction FeatureBoundary<Ready>({ children, demand, error, fallback }: FeatureBoundaryProps<Ready>): ReactNode {\n const { retry: requestRetry, state } = useFeature(demand);\n const [suppressed, setSuppressed] = useState<SuppressedError>();\n const retry = useCallback(() => {\n const attempt = {};\n // The attempt is over: whatever it ended with, including the very same error object, is the answer to show.\n const reveal = (): void => setSuppressed(current => (current?.attempt === attempt ? undefined : current));\n setSuppressed({ attempt, demand });\n\n try {\n void requestRetry().finally(reveal);\n } catch (error) {\n // A retry that refuses synchronously has no promise to settle, and an error hidden by it would never be\n // shown again: the suppression is lifted here before the refusal goes back to whoever asked (D198).\n reveal();\n\n throw error;\n }\n }, [demand, requestRetry]);\n const errorContext = useMemo(() => ({ retry }), [retry]);\n const errorIsSuppressed = suppressed?.demand === demand;\n\n useEffect(() => {\n // A suppression outlives neither its source nor a ready feature: only the attempt of this demand may hide it.\n setSuppressed(current =>\n current === undefined || (current.demand === demand && state.instance === null) ? current : undefined,\n );\n }, [demand, state.instance]);\n\n // Only the branch that is shown runs its callback: an abandoned branch renders nothing and computes nothing.\n if (state.instance !== null) return typeof children === 'function' ? children(state.instance) : children;\n\n if (state.error !== null && !errorIsSuppressed) {\n const content = typeof error === 'function' ? error({ error: state.error, retry }) : error;\n\n return <ErrorContext value={errorContext}>{content}</ErrorContext>;\n }\n\n return fallback;\n}\n\nexport { FeatureBoundary, FeatureBoundaryError, useFeatureRetry };\nexport type { FeatureBoundaryProps };\n"],"names":["ignoreFailure","useFeature","source","subscribe","useCallback","listener","getSnapshot","state","useSyncExternalStore","retrying","useMemo","useEffect","demand","retry","settle","attempt","resolve","finish","lease","ErrorContext","createContext","FeatureBoundaryError","code","message","useBoundaryErrorContext","context","useContext","useFeatureRetry","FeatureBoundary","children","error","fallback","requestRetry","suppressed","setSuppressed","useState","reveal","current","errorContext","errorIsSuppressed","content","_jsx"],"mappings":"mLAwBA,MAAMA,EAAgB,IAAA,GAEtB,SAASC,EAAkBC,EAAkC,CAC3D,MAAMC,EAAYC,EAAaC,GAAyBH,EAAO,UAAUG,CAAQ,EAAG,CAACH,CAAM,CAAC,EACtFI,EAAcF,EAAY,IAAMF,EAAO,SAAQ,EAAI,CAACA,CAAM,CAAC,EAC3DK,EAAQC,EAAqBL,EAAWG,EAAaA,CAAW,EAChEG,EAAWC,EAGd,KAAO,CAAE,QAAS,KAAM,OAAAR,CAAM,GAAK,CAACA,CAAM,CAAC,EAE9CS,EAAU,IAAK,CACb,MAAMC,EAASV,EAAO,QAAO,EAC7B,OAAKU,EAAO,QAAQ,MAAMZ,CAAa,EAEhC,IAAA,CAAWY,EAAO,QAAO,EAAG,MAAMZ,CAAa,EACxD,EAAG,CAACE,CAAM,CAAC,EAEX,MAAMW,EAAQT,EAAY,IAAoB,CAE5C,GAAIK,EAAS,UAAY,KAAM,OAAOA,EAAS,QAAQ,QAKvD,IAAIK,EAAS,IAAA,GACb,MAAMC,EAAU,CACd,MAAO,KACP,QAAS,IAAI,QAAcC,GAAU,CACnCF,EAASE,CACX,CAAC,GAEGC,EAAS,IAAW,CACpBR,EAAS,UAAYM,IAASN,EAAS,QAAU,MAErDK,EAAM,CACR,EACAL,EAAS,QAAUM,EAEnB,IAAIG,EAEJ,GAAI,CACFA,EAAQT,EAAS,OAAO,QAAO,CACjC,MAAQ,CAIN,OAAAQ,EAAM,EAECF,EAAQ,OACjB,CAEA,OAAAA,EAAQ,MAAQG,EACXA,EAAM,QAAQ,MAAMlB,CAAa,EAAE,QAAQiB,CAAM,EACjDC,EAAM,UAAU,MAAMlB,CAAa,EAEjCe,EAAQ,OACjB,EAAG,CAACN,CAAQ,CAAC,EAEb,OAAOC,EAAQ,IAAM,OAAO,OAAO,CAAE,MAAAG,EAAO,MAAAN,CAAK,CAAE,EAAG,CAACM,EAAON,CAAK,CAAC,CACtE,CCjDA,MAAMY,EAAeC,EAAuD,MAAS,EAGrF,MAAMC,UAA6B,KAAK,CAI3B,KAHO,KAAO,uBAEzB,YACWC,EACTC,EAAe,CAEf,MAAMA,CAAO,EAHJ,KAAA,KAAAD,CAIX,CACD,CAED,SAASE,GAAuB,CAC9B,MAAMC,EAAUC,EAAWP,CAAY,EAEvC,GAAIM,IAAY,OACd,MAAM,IAAIJ,EACR,UACA,iFAAiF,EAIrF,OAAOI,CACT,CAEA,SAASE,GAAe,CACtB,OAAOH,EAAuB,EAAG,KACnC,CAEA,SAASI,EAAuB,CAAE,SAAAC,EAAU,OAAAjB,EAAQ,MAAAkB,EAAO,SAAAC,CAAQ,EAA+B,CAChG,KAAM,CAAE,MAAOC,EAAc,MAAAzB,CAAK,EAAKN,EAAWW,CAAM,EAClD,CAACqB,EAAYC,CAAa,EAAIC,EAAQ,EACtCtB,EAAQT,EAAY,IAAK,CAC7B,MAAMW,EAAU,CAAA,EAEVqB,EAAS,IAAYF,EAAcG,IAAYA,GAAA,YAAAA,EAAS,WAAYtB,EAAU,OAAYsB,CAAQ,EACxGH,EAAc,CAAE,QAAAnB,EAAS,OAAAH,EAAQ,EAEjC,GAAI,CACGoB,EAAY,EAAG,QAAQI,CAAM,CACpC,OAASN,EAAO,CAGd,MAAAM,EAAM,EAEAN,CACR,CACF,EAAG,CAAClB,EAAQoB,CAAY,CAAC,EACnBM,EAAe5B,EAAQ,KAAO,CAAE,MAAAG,CAAK,GAAK,CAACA,CAAK,CAAC,EACjD0B,GAAoBN,GAAA,YAAAA,EAAY,UAAWrB,EAUjD,GARAD,EAAU,IAAK,CAEbuB,EAAcG,GACZA,IAAY,QAAcA,EAAQ,SAAWzB,GAAUL,EAAM,WAAa,KAAQ8B,EAAU,MAAS,CAEzG,EAAG,CAACzB,EAAQL,EAAM,QAAQ,CAAC,EAGvBA,EAAM,WAAa,KAAM,OAAO,OAAOsB,GAAa,WAAaA,EAAStB,EAAM,QAAQ,EAAIsB,EAEhG,GAAItB,EAAM,QAAU,MAAQ,CAACgC,EAAmB,CAC9C,MAAMC,EAAU,OAAOV,GAAU,WAAaA,EAAM,CAAE,MAAOvB,EAAM,MAAO,MAAAM,CAAK,CAAE,EAAIiB,EAErF,OAAOW,EAACtB,EAAY,CAAC,MAAOmB,EAAY,SAAGE,EAAO,CACpD,CAEA,OAAOT,CACT"}
@@ -0,0 +1,21 @@
1
+ import type { Call, ModelOf } from '@opetope/core';
2
+ import type { ModelIdentity, NoExtraKeys } from '@opetope/core/internal';
3
+ declare const modelBindingBrand: unique symbol;
4
+ declare const modelBindingIdentityBrand: unique symbol;
5
+ type AnyModel = ModelIdentity;
6
+ type AnyCommand = Call<never, unknown>;
7
+ interface ModelBindingIdentity {
8
+ readonly [modelBindingIdentityBrand]: true;
9
+ }
10
+ interface ModelBinding<Declaration extends AnyModel> extends ModelBindingIdentity {
11
+ readonly [modelBindingBrand]: (declaration: Declaration) => Declaration;
12
+ }
13
+ type ModelBindingMetadata = {
14
+ readonly commands: readonly AnyCommand[];
15
+ readonly declaration: AnyModel;
16
+ readonly model: object;
17
+ };
18
+ declare function bindModel<Declaration extends AnyModel, const Actual extends ModelOf<Declaration>>(declaration: Declaration, model: Actual & NoExtraKeys<Actual, ModelOf<Declaration>>): ModelBinding<Declaration>;
19
+ declare function getModelBindingMetadata(binding: ModelBindingIdentity): ModelBindingMetadata;
20
+ export { bindModel, getModelBindingMetadata };
21
+ export type { AnyCommand, AnyModel, ModelBindingIdentity };
@@ -0,0 +1,12 @@
1
+ import type { Call, ModelOf } from '@opetope/core';
2
+ import type { CommandHook } from './command-hook.js';
3
+ import type { AnyModel } from './model-binding.js';
4
+ import type { SelectionContext, SelectionShape } from './model-selection-snapshot.js';
5
+ type SelectedValue<Value> = Value extends Call<infer Input, infer Output> ? CommandHook<Input, Output> : Value;
6
+ type ModelSelection<Selected> = {
7
+ readonly [Key in keyof Selected]: SelectedValue<Selected[Key]>;
8
+ };
9
+ /** D205: resolve only a granted model, explicitly read data, and reuse the ordinary per-key command consumers. */
10
+ declare function useModel<Declaration extends AnyModel>(declaration: Declaration): ModelOf<Declaration>;
11
+ declare function useModel<Declaration extends AnyModel, const Selected extends object>(declaration: Declaration, select: (model: ModelOf<Declaration>, context: SelectionContext) => Selected & SelectionShape<Selected>): ModelSelection<Selected>;
12
+ export { useModel };
@@ -0,0 +1,32 @@
1
+ import type { Readable } from '@opetope/core';
2
+ import type { AnyCommand } from './model-binding.js';
3
+ interface SelectionRead {
4
+ <Value>(source: Readable<Value>): Value;
5
+ <Value, Selected>(source: Readable<Value>, select: (value: Value) => Selected): Selected;
6
+ }
7
+ type SelectionContext = Readonly<{
8
+ read: SelectionRead;
9
+ }>;
10
+ type NonSelectionRecord = readonly unknown[] | ((...arguments_: never[]) => unknown) | (abstract new (...arguments_: never[]) => unknown) | Date | PromiseLike<unknown> | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> | RegExp | WeakMap<object, unknown> | WeakSet<object>;
11
+ /** A named interface needs no open index signature; container and callable results are not field selections. */
12
+ type SelectionShape<Selected extends object> = Selected extends NonSelectionRecord ? never : Selected;
13
+ type SelectionRecord = Readonly<Record<PropertyKey, unknown>>;
14
+ type SelectionSnapshot = Readonly<{
15
+ commands: Readonly<Record<PropertyKey, AnyCommand>>;
16
+ sources: ReadonlySet<Readable<unknown>>;
17
+ values: SelectionRecord;
18
+ }>;
19
+ type ModelSelector<Model> = (model: Model, context: SelectionContext) => object;
20
+ type SelectionReaderRef<Model> = {
21
+ readonly current: ModelSelector<Model> | undefined;
22
+ };
23
+ declare const emptySelectionSnapshot: SelectionSnapshot;
24
+ /**
25
+ * D214: one reader per model, not per render. The author's selector arrives through a ref, so an inline arrow does not
26
+ * rebuild the reader; the fast path — nothing observed has moved — belongs to the selector that computed the snapshot,
27
+ * so a new selector is always called again, its result compared field by field, and an equal selection keeps the
28
+ * previous snapshot. A rejected or abandoned render still changes nothing: only a committed selection subscribes.
29
+ */
30
+ declare function createSelectionSnapshot<Model>(model: Model, selectRef: SelectionReaderRef<Model>): () => SelectionSnapshot;
31
+ export { createSelectionSnapshot, emptySelectionSnapshot };
32
+ export type { ModelSelector, SelectionContext, SelectionShape };
@@ -0,0 +1,17 @@
1
+ import type { Readable } from '@opetope/core';
2
+ /** A subscription set, not a scheduler: source notifications use React's snapshot consistency check. */
3
+ declare class ModelSelectionStore {
4
+ private enabled;
5
+ private readonly listeners;
6
+ private sources;
7
+ private readonly subscriptions;
8
+ close(): void;
9
+ commit(sources: ReadonlySet<Readable<unknown>>): void;
10
+ subscribe: (notify: () => void) => (() => void);
11
+ private add;
12
+ private notify;
13
+ private releaseAll;
14
+ private removeUnselected;
15
+ private synchronize;
16
+ }
17
+ export { ModelSelectionStore };
@@ -0,0 +1,4 @@
1
+ import type { Frame } from './mount-frame.js';
2
+ declare const FrameContext: import("react").Context<Frame | undefined>;
3
+ declare function useFrame(): Frame;
4
+ export { FrameContext, useFrame };
@@ -0,0 +1,56 @@
1
+ import type { CallController, CallDiagnostics, CallRunOptions } from '@opetope/core/internal';
2
+ import type { AnyCommand, AnyModel, ModelBindingIdentity } from './model-binding.js';
3
+ type RuntimeCommandInvoker = {
4
+ readonly run: (input: never, options?: CallRunOptions<unknown>) => Promise<unknown>;
5
+ };
6
+ type CommandRecord = {
7
+ readonly controller: CallController<never, unknown>;
8
+ readonly invoker: RuntimeCommandInvoker;
9
+ };
10
+ /** What a mounted contribution grants its components: the models it may read and the commands it may invoke. */
11
+ type Frame = {
12
+ readonly commandRecords: Map<AnyCommand, CommandRecord>;
13
+ readonly models: Map<AnyModel, unknown>;
14
+ };
15
+ type MountFrameLifecycle = {
16
+ readonly isActive: () => boolean;
17
+ readonly onClose: () => void;
18
+ };
19
+ type MountFrameDiagnostics = {
20
+ readonly active: boolean;
21
+ readonly calls: readonly CallDiagnostics[];
22
+ readonly commandRecords: number;
23
+ readonly models: number;
24
+ };
25
+ /**
26
+ * The lifetime of one mounted contribution. `attach` claims it for the renderer, `detach` schedules the close on the
27
+ * next microtask so a StrictMode remount reuses the same mount, and `close` fences every command it granted (D85).
28
+ */
29
+ declare class MountFrameImpl {
30
+ readonly frame: Frame;
31
+ private authority;
32
+ private boundCommands;
33
+ private closeTicket;
34
+ private readonly isActive;
35
+ private readonly lifetime;
36
+ private onClose;
37
+ private released;
38
+ private rendererAttached;
39
+ private readonly renderListeners;
40
+ private state;
41
+ constructor(models: readonly ModelBindingIdentity[], lifecycle?: MountFrameLifecycle);
42
+ attach(): boolean;
43
+ close(): void;
44
+ detach(): void;
45
+ diagnostics(): MountFrameDiagnostics;
46
+ getRenderableSnapshot: () => boolean;
47
+ releaseClosedFrame(): void;
48
+ subscribeRenderable: (listener: () => void) => (() => void);
49
+ private buildCommandRecords;
50
+ private captureAuthority;
51
+ private createAuthority;
52
+ private publishRenderable;
53
+ private releaseFrame;
54
+ }
55
+ export { MountFrameImpl };
56
+ export type { Frame };
@@ -0,0 +1,6 @@
1
+ import type { PropsWithChildren, ReactElement } from 'react';
2
+ import type { MountFrameImpl } from './mount-frame.js';
3
+ declare function MountProvider({ children, mount, }: PropsWithChildren<{
4
+ readonly mount: MountFrameImpl;
5
+ }>): ReactElement | null;
6
+ export { MountProvider };