DSH / Atlas
2026-09-10implementedarchitecture

Reusable component factories with local slots

带局部 slot 的可复用组件 Factory

The browser Slot system starts with a parent-owned extension position. A parent entry declares a child through `children`, and unrelated plugins may then register implementations into that position. The declaration fixes the child Slot's kind, scope, render authority, and lifetime. A reusable component assembly has the opposite ownership direction. One package defines the assembly, unrelated parents render independen

English

Problem

The browser Slot system starts with a parent-owned extension position. A parent entry declares a child through children, and unrelated plugins may then register implementations into that position. The declaration fixes the child Slot's kind, scope, render authority, and lifetime.

A reusable component assembly has the opposite ownership direction. One package defines the assembly, unrelated parents render independent occurrences, and each parent may choose a different Component for a named internal region. An ordinary Slot cannot represent this relationship because its definition belongs to one parent position in the global Slot tree.

The defining and consuming packages compile independently. TypeScript cannot infer a selected Component's props from a runtime registration in another package, and a separately maintained flattened props type would duplicate the store, injection, locale, child-render, and scope declarations.

Decision

ui-slots and ui-renderer provide named Component Factories beside the ordinary Slot system. registerFactory() installs one reusable definition, renderFactorySlot() renders an occurrence, and the definition reads caller-selected local Components through useFactorySlot().

Opposite registration directions

Ordinary Slots and Component Factories retain distinct ownership models.

PropertyOrdinary SlotComponent Factory
First declarationParent declares a child SlotDefinition owner declares the Factory
Later operationChild registers into the parent positionParent renders an occurrence
Static authoritySlotMap describes the positionSlotFactoryMap describes the complete definition
Live definitionsMultiple entries may occupy cellsOne definition owns one Factory name
Parent inputrenderSlot() owner and keyed propsrenderFactorySlot() occurrence props
Parent-selected ComponentRegistry routing selects entriesCaller selects one Component per local slot
Descendant extension pointsEntry-owned ordinary childrenDefinition-owned ordinary children

SlotFactoryMap declaration-merges the complete static definition:

interface SlotFactoryDef {
  scope: SlotScope
  props?: object
  children?: ChildrenDecl
  store?: StoreDecl
  inject?: object
  locale?: keyof LocaleNamespaceMap & string
  slots?: Record<string, { scope: SlotScope; props?: object }>
}

The map is the only type authority. registerFactory() checks the runtime definition and main Component against its map entry. Store declarations normalize through HandleOf, so registration accepts one shared handle or one factory that produces that handle, never a nested factory. FactoryComponentPropsOf<F> and FactoryLocalComponentPropsOf<F, N> derive the complete Component props from the same entry; definition owners and consumers do not restate a flattened shared type.

Definition and occurrence lifecycle

A Factory name has one live definition in a registry ledger separate from ordinary Slot cells. Registration follows the caller's Cordis effect. Disposal removes the definition, collapses its ordinary child declarations, notifies mounted outlets, and invalidates retained child-render or local-Component authority.

Each renderFactorySlot() call creates an occurrence whose identity follows its React position and key. Definition replacement remounts occurrences. Definition Components and their fallback local Components report failures against the definition, while caller-selected local Components report against the caller registration; nested Factory renders preserve the same owner. All failures remain contained to that occurrence without retiring the shared definition. Assembly errors propagate, while ownership and stale-authorization failures report like ordinary component failures. The outer error boundary resets with the Factory scope incarnation, and each local boundary resets with its local slot's scope incarnation.

Factory scope follows the scope binding at the occurrence's React position. renderFactorySlot() accepts no Session id or scope target. A strict session Factory requires a current binding and remounts when its identity changes; a session-maybe Factory preserves its first empty-to-Session adoption and remounts on later identity changes, matching ordinary Slot behavior.

A shared store handle keeps the ordinary handle-by-scope behavior, including requiring a Session binding for either non-root scope. An exclusive store factory creates one handle when an occurrence first materializes and rejects that handle if it declares spec.persist, because registration must remain lazy and independently live occurrences cannot share one persistence key safely. Render-time records stay weak until an idempotent effect setup retains the committed occurrence; effect cleanup removes the strong mounted reference, while the occurrence-keyed WeakMap preserves identity during React effect replay and permits collection after unmount.

Local slots and ordinary children

The caller may select one Component for each name in the Factory's slots declaration. The Factory calls useFactorySlot(name, fallback) and receives an identity-stable bound Component that accepts only that local slot's occurrence props. The renderer supplies the Factory's store, injection, locale, child renderers, and the local slot's own standard scope props when the bound Component renders.

Local slots have no list, keyed, or chain routing and no independent registration lifetime. Multi-contributor extension points remain ordinary child Slots declared by the Factory. Those child declarations are global to the definition, while every occurrence renders their registered entries under its inherited scope.

Live inspection represents each definition as a type: 'factory' node and nests its ordinary child Slots beneath it. Ordinary nodes retain type: 'slot' and their existing kind; callers select a Factory root with factory:<name>.

Every renderer-created Component receives renderFactorySlot, so a Factory occurrence needs no parent-side use declaration. Local selection remains per occurrence and does not introduce a runtime value import from the defining package.

First shipped use

ui-conversation registers the optional-Session conversation.content Factory around the shared body and Composer. Its strict-Session views local position defaults to an adapter that renders the existing conversation.session Slot; another occurrence can select a different view Component without mounting the main Conversation Header.

The Factory does not own the Conversation store. The ordinary conversation.session body and conversation.session.header retain one shared strict-Session handle, preserving draft and View-selection identity without mounting that handle under both session and session-maybe scopes.

Type and runtime enforcement

The type chain rejects unknown Factory names, missing or extra occurrence props, child specs that disagree with SlotMap, definition fields that disagree with SlotFactoryMap, nested store factories, unknown local names, incompatible selected Components, and overlapping ownership among input, registration, injection, and scope props.

Runtime checks cover dynamically assembled and plain-JavaScript callers: duplicate definitions, child-declaration conflicts, undeclared local names, recursive rendering, prop collisions, stale authority, strict-scope absence, and component failure isolation. Type and runtime tests also pin independent stores per occurrence, shared handles by scope, fallback behavior before registration, definition replacement, local scope projection, and ordinary child rendering.

Alternatives considered

Reuse an ordinary Slot as a portable definition. An ordinary Slot belongs to one parent declaration and one position in the global tree. Reusing it elsewhere would borrow the wrong ownership and lifetime.

Move the main Conversation Header into the Factory. Only the main host renders that Header. Keeping it outside the Factory lets embedded occurrences omit it without another local selection and preserves its existing strict-Session Slot lifecycle.

Move the shared Conversation store onto the optional-Session Factory. The Header and Session body share one strict-Session handle. Mounting that handle on both the session-maybe Factory and the session Header would violate the one-handle-one-scope rule.

Register the same assembly under every parent. Separate registrations would duplicate the definition and its child declarations. Global contributors would need parallel child names or conflicting declarations.

Maintain a flattened shared props type. This repeats facts already present in children, store, inject, locale, and scope fields, allowing the declaration and Component props to drift.

Pass React nodes or render callbacks as business props. Those values bypass renderer-supplied scope props, store and injection assembly, stale-authority checks, and local Component type checking.

Give local slots ordinary Slot routing. Ordinary Slots already own multi-contributor routing. A local slot represents one caller choice for one occurrence.

Pass Session identity to renderFactorySlot(). The occurrence inherits its render-position scope like an ordinary Slot. A second identity argument would create two authorities that can disagree; independently addressed Session providers are a separate capability.

Consequences

Feature packages can publish one reusable UI assembly without runtime-importing its Component from consumers. Each occurrence gets independently selected local Components and exclusive state while preserving global ordinary child contributions and the existing scope, locale, injection, and store rules.

The additional registry ledger and occurrence bookkeeping increase renderer complexity. Factory definitions must be globally unique, local slots intentionally support only one selected Component, and the API does not itself create an independently addressed Session scope.

The Factory type and runtime tests are the executable compatibility record. The Slots subsystem reference and the ui-slots and ui-renderer package references document the consumer API.

中文

问题

浏览器 Slot 系统从 parent-owned 扩展位置开始。parent entry 通过 children 声明 child,之后互不相关的插件可以向该位置注册实现。声明固定 child Slot 的 kind、scope、渲染权限与生命周期。

可复用组件装配采用相反的所有权方向。一个包定义装配,互不相关的 parents 渲染独立 occurrence,并且每个 parent 可以为具名内部区域选择不同 Component。普通 Slot 无法表示这种关系,因为它的 definition 属于全局 Slot 树中的一个 parent 位置。

定义包与消费包独立编译。TypeScript 无法从另一个包中的运行时 registration 推导所选 Component 的 props,另行维护的扁平 props 类型则会重复 store、injection、locale、child-render 与 scope 声明。

决策

ui-slots 与 ui-renderer 在普通 Slot 体系之外提供具名 Component Factory。registerFactory() 安装一个可复用 definition,renderFactorySlot() 渲染一个 occurrence,definition 通过 useFactorySlot() 读取调用方选择的局部 Component。

相反的注册方向

普通 Slot 与 Component Factory 保留不同的所有权模型。

属性普通 SlotComponent Factory
首个声明Parent 声明 child SlotDefinition owner 声明 Factory
后续操作Child 注册进 parent 位置Parent 渲染 occurrence
静态权威SlotMap 描述位置SlotFactoryMap 描述完整 definition
有效 definitions多个 entries 可以占据 cells一个 definition 独占一个 Factory 名
Parent 输入renderSlot() owner 与 keyed propsrenderFactorySlot() occurrence props
Parent 选择的 ComponentRegistry routing 选择 entries调用方为每个局部 slot 选择一个 Component
后代扩展点Entry-owned 普通 childrenDefinition-owned 普通 children

SlotFactoryMap 通过声明合并给出完整静态 definition:

interface SlotFactoryDef {
  scope: SlotScope
  props?: object
  children?: ChildrenDecl
  store?: StoreDecl
  inject?: object
  locale?: keyof LocaleNamespaceMap & string
  slots?: Record<string, { scope: SlotScope; props?: object }>
}

该 map 是唯一类型权威。registerFactory() 根据对应 map 条目检查运行时 definition 与主 Component。Store 声明通过 HandleOf 规范化,因此 registration 只接受一个共享 handle 或一个生成该 handle 的 factory,绝不接受嵌套 factory。FactoryComponentPropsOf<F> 和 FactoryLocalComponentPropsOf<F, N> 从同一条目推导完整 Component props;definition owner 与消费方无需重述扁平共享类型。

Definition 与 occurrence 生命周期

每个 Factory 名在独立于普通 Slot cells 的 registry ledger 中只有一个有效 definition。registration 遵循调用方的 Cordis effect。dispose 会移除 definition、折叠其普通 child 声明、通知已挂载 outlets,并令保留的 child-render 或局部 Component 权限失效。

每次 renderFactorySlot() 调用都会创建 occurrence,其 identity 由 React 位置和 key 决定。替换 definition 会重新挂载 occurrence。Definition Component 及其 fallback 局部 Component 的失败归属 definition,调用方所选局部 Component 的失败归属调用方 registration;嵌套 Factory 渲染保留同一 owner。所有失败都限制在当前 occurrence 内,不会移除共享 definition。装配错误继续向外传播,所有权与陈旧授权错误则像普通组件失败一样上报。外层错误边界随 Factory scope incarnation 重置,每个局部边界随自身局部 slot 的 scope incarnation 重置。

Factory scope 跟随 occurrence 所在 React 位置的 scope binding。renderFactorySlot() 不接受 Session id 或 scope target。严格 session Factory 要求当前 binding 存在,并在 identity 变化时重新挂载;session-maybe Factory 保留首次从空状态采纳 Session 的过程,并在后续 identity 变化时重新挂载,与普通 Slot 行为一致。

共享 store handle 保留普通模式的 handle-by-scope 行为,包括两个非 root scope 均要求 Session binding。独占 store factory 在 occurrence 首次物化时创建一个 handle;若该 handle 声明 spec.persist,renderer 会拒绝它,因为 registration 必须保持 lazy,且多个同时存活的独立 occurrence 无法安全共享一个 persistence key。渲染期记录在幂等 effect setup 保留已 commit occurrence 前只持有弱引用;effect cleanup 会移除 mounted 强引用,而 occurrence-keyed WeakMap 在 React effect replay 期间保留 identity,并允许实例在卸载后被回收。

局部 slots 与普通 children

调用方可以为 Factory slots 声明中的每个名称选择一个 Component。Factory 调用 useFactorySlot(name, fallback),取得 identity 稳定的绑定 Component;该 Component 只接受对应局部 slot 的 occurrence props。绑定 Component 渲染时,renderer 提供 Factory 的 store、injection、locale、child renderers 与该局部 slot 自身的标准 scope props。

局部 slots 没有 list、keyed 或 chain routing,也没有独立 registration 生命周期。多贡献方扩展点仍使用 Factory 声明的普通 child Slots。这些 child 声明在 definition 范围内全局共享,而每个 occurrence 都在继承的 scope 下渲染其 registered entries。

实时检查将每个 definition 表示为 type: 'factory' 节点,并把其普通 child Slots 嵌套在该节点下。普通节点保留 type: 'slot' 与现有 kind;调用方通过 factory:<name> 选择 Factory 根节点。

每个由 renderer 创建的 Component 都会收到 renderFactorySlot,因此 Factory occurrence 不需要 parent-side use declaration。局部选择仍然属于单个 occurrence,并且不会引入对定义包的运行时 value import。

首个交付用途

ui-conversation 在共享正文与 Composer 外注册 optional-Session conversation.content Factory。其 strict-Session views 局部位置默认使用一个渲染现有 conversation.session Slot 的 adapter;其他 occurrence 可以选择不同的 View Component,且不会挂载主 Conversation Header。

Factory 不拥有 Conversation store。普通 conversation.session body 与 conversation.session.header 保留同一个 strict-Session handle,在保持草稿与 View 选择 identity 的同时,避免将该 handle 同时挂到 session 和 session-maybe scope。

类型与运行时强制规则

类型链拒绝未知 Factory 名、缺失或多余的 occurrence props、与 SlotMap 不一致的 child spec、与 SlotFactoryMap 不一致的 definition 字段、嵌套 store factory、未知局部名称、不兼容的选中 Component,以及 input、registration、injection 与 scope props 之间的所有权重叠。

运行时检查覆盖动态装配与纯 JavaScript 调用方:重复 definitions、child 声明冲突、未声明的局部名称、递归渲染、prop 冲突、陈旧权限、严格 scope 缺失与组件失败隔离。类型和运行时测试还固定了各 occurrence 的独占 store、按 scope 共享的 handles、注册前 fallback 行为、definition 替换、局部 scope 投影与普通 child 渲染。

考虑过的替代方案

把普通 Slot 复用为可移植 definition。 普通 Slot 属于一个 parent 声明以及全局树中的一个位置。在其他位置复用它会借用错误的所有权与生命周期。

把主 Conversation Header 移进 Factory。 只有主 host 渲染该 Header。将其留在 Factory 外,使嵌入式 occurrence 无需另一个局部选择即可省略 Header,并保留其现有 strict-Session Slot 生命周期。

把共享 Conversation store 移到 optional-Session Factory。 Header 与 Session body 共享一个 strict-Session handle。将该 handle 同时挂到 session-maybe Factory 和 session Header 会违反 one-handle-one-scope 规则。

在每个 parent 下分别注册同一装配。 独立 registrations 会重复 definition 及其 child 声明。全局贡献方需要使用平行 child 名称,或造成声明冲突。

维护扁平共享 props 类型。 这会重复 children、store、inject、locale 与 scope 字段中已有的事实,使声明与 Component props 可以发生漂移。

把 React node 或 render callback 作为业务 props 传递。 这些值绕过 renderer 提供的 scope props、store 与 injection 装配、陈旧权限检查和局部 Component 类型检查。

让局部 slots 具备普通 Slot routing。 普通 Slots 已经负责多贡献方 routing。局部 slot 表示一次 occurrence 的一个调用方选择。

向 renderFactorySlot() 传递 Session identity。 occurrence 像普通 Slot 一样继承渲染位置的 scope。第二个 identity 参数会产生两个可能不一致的权威;独立定址的 Session provider 属于另一项能力。

影响

功能包可以发布一份可复用 UI 装配,而消费方无需运行时导入其 Component。每个 occurrence 可以获得独立选择的局部 Components 与独占状态,同时保留全局普通 child 贡献以及现有 scope、locale、injection 和 store 规则。

新增的 registry ledger 与 occurrence 记录增加了 renderer 复杂度。Factory definitions 必须全局唯一,局部 slots 有意只支持一个选中 Component,并且该 API 本身不会创建可独立定址的 Session scope。

Factory 类型与运行时测试是可执行的兼容性记录。Slots 子系统参考以及 ui-slots 和 ui-renderer 包参考记录消费方 API。