@x1a0f3n9/dsh-client-ui-renderer 0.1.5-rc.4 → 0.1.7-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -1,6 +1,52 @@
1
- # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
- # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
- # after editing either side, bring the other along and re-record with:
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
3
+ # After editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-renderer/README.md
5
- README.md: 582de984025c54b5ed6cc12dc3c39f50e5c0df1a
6
- README.zh.md: f38c0089fcbb8a3fdf741d97a66f0bc2c08fd24d
5
+ /:
6
+ en: 06bd9d926c743736
7
+ zh: 39acdcb577fd8c6d
8
+ /x1a0f3n9-dsh-client-ui-renderer:
9
+ en: 3569f2670b8a1fce
10
+ zh: 5bbedcc4e539d186
11
+ /x1a0f3n9-dsh-client-ui-renderer/summary:
12
+ en: cae0bce110641caf
13
+ zh: e5bde9dba75389bc
14
+ /x1a0f3n9-dsh-client-ui-renderer/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 09388d293f9be9cb
17
+ /x1a0f3n9-dsh-client-ui-renderer/use-this-package:
18
+ en: 4873b12ddeac04f2
19
+ zh: 027845407b207c4f
20
+ /x1a0f3n9-dsh-client-ui-renderer/use-this-package/what-mounting-does:
21
+ en: 16f8f781e259f0ee
22
+ zh: aa2b4783807fe943
23
+ /x1a0f3n9-dsh-client-ui-renderer/use-this-package/for-business-plugins:
24
+ en: 15a2bfee2c0bd051
25
+ zh: 0c5b336fc5afbbc6
26
+ /x1a0f3n9-dsh-client-ui-renderer/understand-the-implementation:
27
+ en: 0de0a8ad857b93f6
28
+ zh: 98a0fae2d6606a7d
29
+ /x1a0f3n9-dsh-client-ui-renderer/understand-the-implementation/activation-and-mount:
30
+ en: 822adba4ecd1b7e3
31
+ zh: bab3626fbe30ff55
32
+ /x1a0f3n9-dsh-client-ui-renderer/understand-the-implementation/slot-bindings:
33
+ en: 6ab26cf5744a23d4
34
+ zh: 84a531322a724a84
35
+ /x1a0f3n9-dsh-client-ui-renderer/understand-the-implementation/identity:
36
+ en: 2a349d500ead79cb
37
+ zh: ac49c8b1d73dcd6c
38
+ /x1a0f3n9-dsh-client-ui-renderer/further-exploration:
39
+ en: 77b43b2f3d55c933
40
+ zh: 9847b53486372aeb
41
+ /x1a0f3n9-dsh-client-ui-renderer/model-experience:
42
+ en: ca4e29db08d34e6d
43
+ zh: de6ec680414b1d54
44
+ /x1a0f3n9-dsh-client-ui-renderer/model-experience/kv-cache-effect:
45
+ en: ca75c51c89c2c9b0
46
+ zh: de54f3467ef3b9be
47
+ /x1a0f3n9-dsh-client-ui-renderer/known-limitations-and-deferred-work:
48
+ en: 686023788b76788f
49
+ zh: 2bb3ca909746c485
50
+ /x1a0f3n9-dsh-client-ui-renderer/known-limitations-and-deferred-work/dev-note:
51
+ en: e67f633dc6fe7773
52
+ zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Browser UI renderer: React slot bindings, ctx.uiRenderer, and the assembled application root for the dsh web client."
2
+ description: "Browser UI renderer: React bindings for ordinary Slots and reusable Component Factories, ctx.uiRenderer, and the assembled dsh web application root."
3
3
  kind: "package-reference"
4
4
  ---
5
5
 
@@ -33,7 +33,7 @@ This package is infrastructure: the web shell and the boot kernel are its only d
33
33
 
34
34
  ### For business plugins
35
35
 
36
- A business plugin registers a component through the slot system; the renderer binds the runtime's session and workspace observable sources into selector hooks at the outlet. The plugin receives the standard session props (session id, conversation snapshot hooks) through its composed props — it never imports the renderer or touches React internals.
36
+ A business plugin registers an ordinary Slot entry or a reusable Component Factory; the renderer binds the runtime's session and workspace observable sources into selector hooks at the render position. The plugin receives standard scope props through its derived Component props — it never imports the renderer or touches React internals. Every renderer-created Component can render a Factory occurrence, and a Factory can expose caller-selected local Components without sharing implementation values between packages.
37
37
 
38
38
  -----
39
39
 
@@ -51,7 +51,7 @@ The plugin installs `createSlotRenderer()` and reflects the `uiRenderer` service
51
51
 
52
52
  ### Slot bindings
53
53
 
54
- `createSlotRenderer` connects the slot registry to React: entry lists become reactive sources, and each outlet renders through the installed renderer. Business plugins pass bare observable sources through typed slot `hooks`; the renderer binds them at the outlet via the uSES adapter.
54
+ `createSlotRenderer` connects the slot registry to React: ordinary entry lists and Factory definitions become reactive sources, and each outlet or occurrence renders through the installed renderer. Business plugins pass bare observable sources through typed `hooks`; the renderer binds them at the render position via the uSES adapter. A Factory Store factory stays lazy until an occurrence first materializes; its exclusive handle rejects persistence, render-time records stay weak, and an idempotent effect strongly retains only mounted occurrences while preserving identity across effect replay. Factory errors use the ordinary supervision channel without abdicating the shared definition: the definition and its fallback local Components report against the definition, caller-selected local Components report against the caller registration, and each boundary resets with its own scope incarnation.
55
55
 
56
56
  ### Identity
57
57
 
@@ -71,6 +71,7 @@ These pages cover the surrounding machinery and the composition model.
71
71
  - [ui-session](../ui-session/README.md) — the adapter that supplies the standard Session sources and hooks this renderer binds.
72
72
  - [Web client architecture](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) — the loading chain, object layer, and layering red lines.
73
73
  - [Slot system standard](../../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md) — the definitive composition model.
74
+ - [Component Factories](../../../.agents/notes/implemented/architecture/2026-09-10-component-factories-and-local-slots.md) — reusable definitions, local Component selection, and occurrence lifetimes.
74
75
 
75
76
  -----
76
77
 
package/README.zh.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "浏览器 UI 渲染器:React slot 绑定、ctx.uiRenderer 与 dsh Web 客户端组装后的应用根。"
2
+ description: "浏览器 UI 渲染器:普通 Slot 与可复用 Component Factory 的 React 绑定、ctx.uiRenderer 与组装后的 dsh Web 应用根。"
3
3
  kind: "package-reference"
4
4
  ---
5
5
 
@@ -33,7 +33,7 @@ kind: "package-reference"
33
33
 
34
34
  ### 对业务插件
35
35
 
36
- 业务插件通过 slot 系统注册组件;渲染器在 outlet 处把运行时的会话与 Workspace observable source 绑定为 selector 钩子。插件通过其组合 props 收到标准会话 props(session id、对话快照钩子)——它绝不导入渲染器,也不触碰 React 内部机制。
36
+ 业务插件注册普通 Slot entry 或可复用 Component Factory;渲染器在渲染位置把运行时的会话与 Workspace observable source 绑定为 selector 钩子。插件通过推导出的 Component props 收到标准 scope props——它绝不导入渲染器,也不触碰 React 内部机制。每个由 renderer 创建的 Component 都能渲染 Factory occurrence,Factory 也能暴露由调用方选择的局部 Component,而无需在包之间共享实现值。
37
37
 
38
38
  -----
39
39
 
@@ -51,7 +51,7 @@ kind: "package-reference"
51
51
 
52
52
  ### Slot 绑定
53
53
 
54
- `createSlotRenderer` 把 slot 注册表连接到 React:条目列表成为响应式 source,每个 outlet 经已安装的渲染器渲染。业务插件通过带类型的 slot `hooks` 传递裸 observable source;渲染器经 uSES 适配器在 outlet 处完成绑定。
54
+ `createSlotRenderer` 把 slot 注册表连接到 React:普通 entry list 与 Factory definition 成为响应式 source,每个 outlet 或 occurrence 经已安装的渲染器渲染。业务插件通过带类型的 `hooks` 传递裸 observable source;渲染器经 uSES 适配器在渲染位置完成绑定。Factory Store factory 保持 lazy,直到 occurrence 首次物化时才创建 handle;其 exclusive handle 拒绝持久化,渲染期记录保持弱引用,幂等 effect 仅强引用 mounted occurrences,同时在 effect replay 期间保留 identity。Factory 错误使用普通监督通道且不会 abdicate 共享 definition:definition 及其 fallback 局部 Component 的失败归属 definition,调用方所选局部 Component 的失败归属调用方 registration,每个边界随自身 scope incarnation 重置。
55
55
 
56
56
  ### 身份
57
57
 
@@ -71,6 +71,7 @@ React、React DOM、Cordis、ui-slots 与 ui-primitives 通过 Web 外壳的静
71
71
  - [ui-session](../ui-session/README.zh.md)——提供本渲染器所绑定标准会话 source 与钩子的适配器。
72
72
  - [Web 客户端架构](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md)——加载链、对象层与分层红线。
73
73
  - [slot 系统标准](../../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md)——权威组合模型。
74
+ - [Component Factory](../../../.agents/notes/implemented/architecture/2026-09-10-component-factories-and-local-slots.zh.md)——可复用 definitions、局部 Component 选择与 occurrence 生命周期。
74
75
 
75
76
  -----
76
77
 
package/lib/client.js CHANGED
@@ -160,10 +160,13 @@ window.__ModuleLoader__.load({
160
160
  };
161
161
  }
162
162
  //#endregion
163
+ //#region lib/types/client/errors.js
164
+ /** Shared renderer failure categories. @module */
165
+ /** Renderer assembly failures that must escape component error boundaries. */
166
+ var SlotAssemblyError = class extends Error {};
167
+ //#endregion
163
168
  //#region lib/types/client/bindings.js
164
169
  /** Internal React bindings for renderer hosts and standard-source scopes. */
165
- /** Missing renderer assembly dependency. */
166
- var SlotAssemblyError = class extends Error {};
167
170
  /** In-package renderer host context. */
168
171
  const HostContext = (0, react.createContext)(null);
169
172
  /**
@@ -196,6 +199,19 @@ window.__ModuleLoader__.load({
196
199
  return binding;
197
200
  }
198
201
  /**
202
+ * Publish one resolved scope binding to a renderer subtree.
203
+ * @param props - provider inputs.
204
+ * @param props.binding - binding exposed to scoped entries.
205
+ * @param props.children - subtree that inherits the binding.
206
+ * @returns the scoped React provider.
207
+ */
208
+ function ScopeBindingProvider({ binding, children }) {
209
+ return (0, react_jsx_runtime.jsx)(ScopeBindingContext.Provider, {
210
+ value: binding,
211
+ children
212
+ });
213
+ }
214
+ /**
199
215
  * Bind one observable source to an identity-stable selector Hook.
200
216
  * @param source - observable source.
201
217
  * @returns cached selector Hook.
@@ -230,20 +246,36 @@ window.__ModuleLoader__.load({
230
246
  * @param source - keyed resolver, or absence for an optional scope.
231
247
  * @returns cached keyed selector Hook.
232
248
  */
233
- function keyedObservableHook(source) {
249
+ function keyedObservableHook(source, defaultKey) {
234
250
  if (source === void 0) return absentKeyedHook;
235
- let hook = keyedHookCache.get(source);
251
+ let hooks = keyedHookCache.get(source);
252
+ if (hooks === void 0) {
253
+ hooks = /* @__PURE__ */ new Map();
254
+ keyedHookCache.set(source, hooks);
255
+ }
256
+ const cacheKey = defaultKey ?? NO_DEFAULT_KEY;
257
+ let hook = hooks.get(cacheKey);
236
258
  if (hook === void 0) {
237
- hook = (key, selector, equal) => {
238
- return observableHook(source(key) ?? absentSource)(selector ?? identity, equal);
259
+ hook = (keyOrSelector, selectorOrEqual, equal) => {
260
+ const keyed = typeof keyOrSelector === "string";
261
+ const key = keyed ? keyOrSelector : defaultKey;
262
+ const selector = keyed ? selectorOrEqual : keyOrSelector;
263
+ const comparison = keyed ? equal : selectorOrEqual;
264
+ return observableHook(key === void 0 ? absentSource : source(key) ?? absentSource)(selector ?? identity, comparison);
239
265
  };
240
- keyedHookCache.set(source, hook);
266
+ hooks.set(cacheKey, hook);
241
267
  }
242
268
  return hook;
243
269
  }
270
+ const NO_DEFAULT_KEY = Symbol("no default key");
244
271
  const keyedHookCache = /* @__PURE__ */ new WeakMap();
245
272
  const identity = (value) => value;
246
- const absentKeyedHook = (_key, selector, equal) => observableHook(absentSource)(selector ?? identity, equal);
273
+ const absentKeyedHook = (keyOrSelector, selectorOrEqual, equal) => {
274
+ const keyed = typeof keyOrSelector === "string";
275
+ const selector = keyed ? selectorOrEqual : keyOrSelector;
276
+ const comparison = keyed ? equal : selectorOrEqual;
277
+ return observableHook(absentSource)(selector ?? identity, comparison);
278
+ };
247
279
  /** Subscribe the tree to the atomically assembled root standard-source roster. */
248
280
  function RootStandardProvider({ children }) {
249
281
  const binding = observableHook(useHost().root)((value) => value);
@@ -270,6 +302,20 @@ window.__ModuleLoader__.load({
270
302
  * React renderer for declarative slots. Per-entry bindings enforce child
271
303
  * authorization, and entry boundaries contain registrant failures.
272
304
  */
305
+ const factoryRenderCache = /* @__PURE__ */ new WeakMap();
306
+ function boundRenderFactorySlot(caller) {
307
+ let render = factoryRenderCache.get(caller);
308
+ if (render !== void 0) return render;
309
+ render = ((name, props, options) => (0, react_jsx_runtime.jsx)(FactoryOutlet, {
310
+ name,
311
+ inputProps: props,
312
+ slots: options?.slots,
313
+ fallback: options?.fallback,
314
+ caller
315
+ }));
316
+ factoryRenderCache.set(caller, render);
317
+ return render;
318
+ }
273
319
  /**
274
320
  * Per-entry renderSlot bindings. The binding is identity-stable per entry
275
321
  * (memoized components must not resubscribe on unrelated re-renders) and dies
@@ -320,6 +366,42 @@ window.__ModuleLoader__.load({
320
366
  }
321
367
  return binding;
322
368
  }
369
+ const factoryRenderSlotCache = /* @__PURE__ */ new WeakMap();
370
+ const factoryRenderSlotChainCache = /* @__PURE__ */ new WeakMap();
371
+ function boundFactoryRenderSlot(host, definition) {
372
+ let binding = factoryRenderSlotCache.get(definition);
373
+ if (binding !== void 0) return binding;
374
+ binding = (key, owner, opts) => {
375
+ if (!host.isFactoryLive(definition)) throw new _x1a0f3n9_dsh_client_ui_slots.StaleAuthorizationError(`renderSlot('${key}') from a disposed Factory`);
376
+ const declared = definition.children?.[key];
377
+ if (declared === void 0) throw new _x1a0f3n9_dsh_client_ui_slots.SlotOwnershipError(`slot '${key}' is not declared by this Factory`);
378
+ if (declared.kind === "chain") throw new _x1a0f3n9_dsh_client_ui_slots.SlotOwnershipError(`slot '${key}' is declared 'chain' — use renderSlotChain`);
379
+ return (0, react_jsx_runtime.jsx)(SlotOutlet, {
380
+ slotKey: key,
381
+ ownerProps: owner,
382
+ opts
383
+ });
384
+ };
385
+ factoryRenderSlotCache.set(definition, binding);
386
+ return binding;
387
+ }
388
+ function boundFactoryRenderSlotChain(host, definition) {
389
+ let binding = factoryRenderSlotChainCache.get(definition);
390
+ if (binding !== void 0) return binding;
391
+ binding = (key, owner, opts) => {
392
+ if (!host.isFactoryLive(definition)) throw new _x1a0f3n9_dsh_client_ui_slots.StaleAuthorizationError(`renderSlotChain('${key}') from a disposed Factory`);
393
+ const declared = definition.children?.[key];
394
+ if (declared === void 0) throw new _x1a0f3n9_dsh_client_ui_slots.SlotOwnershipError(`slot '${key}' is not declared by this Factory`);
395
+ if (declared.kind !== "chain") throw new _x1a0f3n9_dsh_client_ui_slots.SlotOwnershipError(`slot '${key}' is declared '${declared.kind}', not 'chain' — use renderSlot`);
396
+ return (0, react_jsx_runtime.jsx)(SlotOutlet, {
397
+ slotKey: key,
398
+ ownerProps: owner,
399
+ opts
400
+ });
401
+ };
402
+ factoryRenderSlotChainCache.set(definition, binding);
403
+ return binding;
404
+ }
323
405
  /**
324
406
  * Inject results cache: root entries per entry, session entries per
325
407
  * (entry x scope binding). WeakMap keys are entry/binding objects (both
@@ -506,6 +588,16 @@ window.__ModuleLoader__.load({
506
588
  }
507
589
  return key;
508
590
  }
591
+ let nextSessionGenerationKey = 0;
592
+ const sessionGenerationKeys = /* @__PURE__ */ new WeakMap();
593
+ function sessionGenerationKeyOf(binding) {
594
+ let key = sessionGenerationKeys.get(binding.ctx);
595
+ if (key === void 0) {
596
+ key = nextSessionGenerationKey++;
597
+ sessionGenerationKeys.set(binding.ctx, key);
598
+ }
599
+ return key;
600
+ }
509
601
  /**
510
602
  * Per-entry isolation: one registrant crashing (component render or inject
511
603
  * factory) must not take down siblings. Assembly errors (missing providers)
@@ -531,11 +623,27 @@ window.__ModuleLoader__.load({
531
623
  return this.props.children;
532
624
  }
533
625
  };
626
+ /** Contain one Factory occurrence without retiring the shared definition. */
627
+ var FactoryErrorBoundary = class extends react.Component {
628
+ state = { failed: false };
629
+ static getDerivedStateFromError(error) {
630
+ if (error instanceof SlotAssemblyError) throw error;
631
+ return { failed: true };
632
+ }
633
+ componentDidCatch(error) {
634
+ console.error(`slot factory occurrence crashed in '${this.props.name}':`, error);
635
+ this.props.onEntryError(error);
636
+ }
637
+ render() {
638
+ if (this.state.failed) return (0, react_jsx_runtime.jsx)("div", { "data-factory-error": this.props.name });
639
+ return this.props.children;
640
+ }
641
+ };
534
642
  const rootStandardCache = /* @__PURE__ */ new WeakMap();
535
643
  const sessionStandardCache = /* @__PURE__ */ new WeakMap();
536
644
  const sessionMaybeStandardCache = /* @__PURE__ */ new WeakMap();
537
645
  /** Materialize one binding into stable framework Hook and plain-prop seats. */
538
- function materializeStandardBinding(binding, optional) {
646
+ function materializeStandardBinding(binding, optional, defaultKey) {
539
647
  const standard = { ...binding.props };
540
648
  for (const [name, source] of Object.entries(binding.hooks)) {
541
649
  if (source === void 0 && !optional) throw new SlotAssemblyError(`strict standard hook '${name}' has no source`);
@@ -543,7 +651,7 @@ window.__ModuleLoader__.load({
543
651
  }
544
652
  for (const [name, source] of Object.entries(binding.keyedHooks)) {
545
653
  if (source === void 0 && !optional) throw new SlotAssemblyError(`strict keyed standard hook '${name}' has no source resolver`);
546
- standard[(0, _x1a0f3n9_dsh_client_ui_slots.standardHookPropName)(name)] = keyedObservableHook(source);
654
+ standard[(0, _x1a0f3n9_dsh_client_ui_slots.standardHookPropName)(name)] = keyedObservableHook(source, defaultKey);
547
655
  }
548
656
  return standard;
549
657
  }
@@ -565,7 +673,7 @@ window.__ModuleLoader__.load({
565
673
  let standard = perScope.get(scopeBinding);
566
674
  if (standard !== void 0) return standard;
567
675
  standard = {
568
- ...root,
676
+ ...materializeStandardBinding(rootBinding, false, scopeBinding.key),
569
677
  ...materializeStandardBinding(scopeBinding, scope === "session-maybe")
570
678
  };
571
679
  perScope.set(scopeBinding, standard);
@@ -579,7 +687,15 @@ window.__ModuleLoader__.load({
579
687
  if (adapter.renderArea === void 0) throw new SlotAssemblyError("scope 'session' adapter does not provide its area renderer");
580
688
  const renderArea = adapter.renderArea.bind(adapter);
581
689
  Provider = function ScopeAreaProvider(props) {
582
- return renderArea(useScopeBinding(), props);
690
+ const inherited = useScopeBinding();
691
+ const explicit = Object.hasOwn(props, "session");
692
+ const source = adapter.bindingSource(props.session);
693
+ const resolved = (0, react.useSyncExternalStore)((listener) => source.subscribe(listener), () => source.getSnapshot(), () => source.getSnapshot());
694
+ const binding = explicit ? resolved : inherited;
695
+ return (0, react_jsx_runtime.jsx)(ScopeBindingProvider, {
696
+ binding,
697
+ children: renderArea(binding, props)
698
+ });
583
699
  };
584
700
  scopeAreaCache.set(adapter, Provider);
585
701
  return Provider;
@@ -598,7 +714,10 @@ window.__ModuleLoader__.load({
598
714
  */
599
715
  function standardKit(host, entry, scope, rootBinding, scopeBinding) {
600
716
  const standard = standardProps(scope, rootBinding, scopeBinding);
601
- const kit = { ...standard };
717
+ const kit = {
718
+ ...standard,
719
+ renderFactorySlot: boundRenderFactorySlot(entry)
720
+ };
602
721
  if (entry.locale !== void 0) {
603
722
  const face = host.locale;
604
723
  if (face === void 0) throw new SlotAssemblyError(`entry declares locale namespace '${entry.locale}' but no locale face is installed (locale plugin missing from the composition?)`);
@@ -613,7 +732,7 @@ window.__ModuleLoader__.load({
613
732
  if (entry.children !== void 0) {
614
733
  kit["renderSlot"] = boundRenderSlot(host, entry);
615
734
  if (Object.values(entry.children).some((spec) => spec.kind === "chain")) kit["renderSlotChain"] = boundRenderSlotChain(host, entry);
616
- if (Object.values(entry.children).some((spec) => spec.scope === "session")) {
735
+ if (Object.values(entry.children).some((spec) => spec.scope !== "root")) {
617
736
  const adapter = host.scope("session");
618
737
  if (adapter === void 0) throw new SlotAssemblyError("entry declares a session child without an installed 'session' scope adapter");
619
738
  kit["SessionProvider"] = scopeAreaProvider(adapter);
@@ -697,22 +816,39 @@ window.__ModuleLoader__.load({
697
816
  */
698
817
  function SessionMaybeEntry({ entry, ownerProps, slotKey, slotInjected, hookContext, hasHookContext }) {
699
818
  const binding = useScopeBinding();
819
+ const epoch = useMaybeIncarnation(binding);
820
+ return (0, react_jsx_runtime.jsx)(SessionMaybeEntryBody, {
821
+ entry,
822
+ ownerProps,
823
+ binding,
824
+ slotKey,
825
+ slotInjected,
826
+ hookContext,
827
+ hasHookContext
828
+ }, epoch);
829
+ }
830
+ const FIRST_INCARNATION = {
831
+ adopted: void 0,
832
+ epoch: 0
833
+ };
834
+ function useMaybeIncarnation(binding) {
835
+ const identity = binding.key === void 0 ? void 0 : binding.ctx;
700
836
  const [state, setState] = (0, react.useState)(FIRST_INCARNATION);
701
837
  let { adopted, epoch } = state;
702
- if (binding.key !== void 0 && adopted === void 0) {
703
- adopted = binding.key;
838
+ if (identity !== void 0 && adopted === void 0) {
839
+ adopted = identity;
704
840
  setState({
705
841
  adopted,
706
842
  epoch
707
843
  });
708
- } else if (adopted !== void 0 && binding.key !== void 0 && binding.key !== adopted) {
709
- adopted = binding.key;
844
+ } else if (adopted !== void 0 && identity !== void 0 && identity !== adopted) {
845
+ adopted = identity;
710
846
  epoch += 1;
711
847
  setState({
712
848
  adopted,
713
849
  epoch
714
850
  });
715
- } else if (adopted !== void 0 && binding.key === void 0) {
851
+ } else if (adopted !== void 0 && identity === void 0) {
716
852
  adopted = void 0;
717
853
  epoch += 1;
718
854
  setState({
@@ -720,20 +856,8 @@ window.__ModuleLoader__.load({
720
856
  epoch
721
857
  });
722
858
  }
723
- return (0, react_jsx_runtime.jsx)(SessionMaybeEntryBody, {
724
- entry,
725
- ownerProps,
726
- binding,
727
- slotKey,
728
- slotInjected,
729
- hookContext,
730
- hasHookContext
731
- }, epoch);
859
+ return epoch;
732
860
  }
733
- const FIRST_INCARNATION = {
734
- adopted: void 0,
735
- epoch: 0
736
- };
737
861
  function RootEntry({ entry, ownerProps, slotKey, slotInjected, hookContext, hasHookContext }) {
738
862
  const host = useHost();
739
863
  const rootBinding = useRootBinding();
@@ -741,22 +865,225 @@ window.__ModuleLoader__.load({
741
865
  const { kit, standard, actions } = standardKit(host, entry, "root", rootBinding, void 0);
742
866
  return renderEntry(slotKey, Comp, kit, standard, cachedRootInject(entry, actions), slotInjected, ownerProps, hookContext, hasHookContext);
743
867
  }
868
+ const FactoryOccurrenceContext = (0, react.createContext)(null);
869
+ const FactoryAncestryContext = (0, react.createContext)(/* @__PURE__ */ new Set());
870
+ const EMPTY_FACTORY_SELECTION = {};
871
+ function useFactorySlotRuntime(name, fallback) {
872
+ const occurrence = (0, react.useContext)(FactoryOccurrenceContext);
873
+ if (occurrence === null) throw new SlotAssemblyError("useFactorySlot() called outside a Factory occurrence");
874
+ if (!occurrence.host.isFactoryLive(occurrence.definition)) throw new _x1a0f3n9_dsh_client_ui_slots.StaleAuthorizationError(`useFactorySlot('${name}') from a disposed Factory`);
875
+ const declared = occurrence.definition.slots?.[name];
876
+ if (declared === void 0) throw new _x1a0f3n9_dsh_client_ui_slots.SlotOwnershipError(`local slot '${name}' is not declared by factory '${occurrence.definition.name}'`);
877
+ const selected = occurrence.selected[name];
878
+ const Selected = selected ?? fallback;
879
+ const usesFallback = selected === void 0;
880
+ const { definition, host } = occurrence;
881
+ return (0, react.useMemo)(function bindFactoryLocalComponent() {
882
+ return function BoundFactoryLocalComponent(localProps) {
883
+ const current = (0, react.useContext)(FactoryOccurrenceContext);
884
+ const localScopeBinding = useScopeBinding();
885
+ const localMaybeEpoch = useMaybeIncarnation(localScopeBinding);
886
+ if (!host.isFactoryLive(definition)) throw new _x1a0f3n9_dsh_client_ui_slots.StaleAuthorizationError(`local slot '${name}' from a disposed Factory`);
887
+ if (current === null || current.definition !== definition) throw new _x1a0f3n9_dsh_client_ui_slots.SlotOwnershipError(`local slot '${name}' rendered outside factory '${definition.name}'`);
888
+ if (declared.scope === "session" && localScopeBinding.key === void 0) throw new SlotAssemblyError(`strict session local slot '${name}' from factory '${definition.name}' rendered without a scope binding`);
889
+ const localOwner = usesFallback ? definition : current.caller;
890
+ const localScopeIdentity = declared.scope === "root" ? "root" : declared.scope === "session" ? `session:${sessionGenerationKeyOf(localScopeBinding)}` : `session-maybe:${localMaybeEpoch}`;
891
+ const localStandard = standardProps(declared.scope, current.rootBinding, localScopeBinding);
892
+ const localRegistrationKit = {
893
+ ...current.registrationKit,
894
+ renderFactorySlot: boundRenderFactorySlot(localOwner)
895
+ };
896
+ assertNoPropOverlap(`factory '${definition.name}' local slot '${name}'`, localRegistrationKit, localStandard);
897
+ const provided = {
898
+ ...localRegistrationKit,
899
+ ...localStandard
900
+ };
901
+ assertNoPropOverlap(`factory '${definition.name}' local slot '${name}'`, provided, localProps);
902
+ return (0, react_jsx_runtime.jsx)(FactoryErrorBoundary, {
903
+ name: `${definition.name}:${name}`,
904
+ onEntryError: (error) => {
905
+ host.reportFactoryError(definition.name, localOwner, error);
906
+ },
907
+ children: (0, react_jsx_runtime.jsx)(Selected, {
908
+ ...provided,
909
+ ...localProps
910
+ })
911
+ }, `${definition.name}:${name}:${localScopeIdentity}`);
912
+ };
913
+ }, [
914
+ Selected,
915
+ declared.scope,
916
+ definition,
917
+ host,
918
+ name,
919
+ usesFallback
920
+ ]);
921
+ }
922
+ function assertNoPropOverlap(owner, provided, received) {
923
+ for (const name of Object.keys(received)) if (Object.hasOwn(provided, name)) throw new SlotAssemblyError(`${owner} received duplicate prop '${name}'`);
924
+ }
925
+ function factoryKit(host, definition, rootBinding, scopeBinding, occurrence) {
926
+ const standard = standardProps(definition.scope, rootBinding, scopeBinding);
927
+ const registrationKit = { renderFactorySlot: boundRenderFactorySlot(definition) };
928
+ if (definition.locale !== void 0) {
929
+ const face = host.locale;
930
+ if (face === void 0) throw new SlotAssemblyError(`factory declares locale namespace '${definition.locale}' but no locale face is installed`);
931
+ registrationKit["t"] = localeSeat(face, definition.locale);
932
+ }
933
+ const scoped = scopeBinding?.key === void 0 ? void 0 : scopeBinding;
934
+ const store = host.factoryStoreOf(definition, scoped, occurrence);
935
+ if (store !== void 0) {
936
+ registrationKit["useStore"] = observableHook(store);
937
+ registrationKit["actions"] = store.actions;
938
+ }
939
+ if (definition.children !== void 0) {
940
+ registrationKit["renderSlot"] = boundFactoryRenderSlot(host, definition);
941
+ if (Object.values(definition.children).some((spec) => spec.kind === "chain")) registrationKit["renderSlotChain"] = boundFactoryRenderSlotChain(host, definition);
942
+ if (Object.values(definition.children).some((spec) => spec.scope !== "root")) {
943
+ const sessionAdapter = host.scope("session");
944
+ if (sessionAdapter === void 0) throw new SlotAssemblyError("factory declares a session child without an installed 'session' scope adapter");
945
+ registrationKit["SessionProvider"] = scopeAreaProvider(sessionAdapter);
946
+ }
947
+ }
948
+ return {
949
+ kit: {
950
+ ...standard,
951
+ ...registrationKit
952
+ },
953
+ registrationKit,
954
+ actions: store?.actions
955
+ };
956
+ }
957
+ function FactoryOccurrence({ definition, inputProps, selected, caller, binding, maybeEpoch }) {
958
+ if (definition.scope === "root") return (0, react_jsx_runtime.jsx)(FactoryOccurrenceBody, {
959
+ definition,
960
+ inputProps,
961
+ selected,
962
+ caller
963
+ });
964
+ if (definition.scope === "session") {
965
+ if (binding.key === void 0) throw new SlotAssemblyError(`strict session factory '${definition.name}' rendered without a scope binding`);
966
+ return (0, react_jsx_runtime.jsx)(FactoryOccurrenceBody, {
967
+ definition,
968
+ inputProps,
969
+ selected,
970
+ caller,
971
+ scopeBinding: binding
972
+ }, sessionGenerationKeyOf(binding));
973
+ }
974
+ return (0, react_jsx_runtime.jsx)(FactoryOccurrenceBody, {
975
+ definition,
976
+ inputProps,
977
+ selected,
978
+ caller,
979
+ scopeBinding: binding
980
+ }, maybeEpoch);
981
+ }
982
+ function FactoryOccurrenceBody({ definition, inputProps, selected, caller, scopeBinding }) {
983
+ const host = useHost();
984
+ const rootBinding = useRootBinding();
985
+ const occurrence = (0, react.useRef)({}).current;
986
+ const localeRevision = useLocaleRevision(host.locale);
987
+ (0, react.useEffect)(() => host.retainFactoryOccurrence(definition, occurrence), [
988
+ definition,
989
+ host,
990
+ occurrence
991
+ ]);
992
+ const { kit, registrationKit, actions } = (0, react.useMemo)(() => factoryKit(host, definition, rootBinding, scopeBinding, occurrence), [
993
+ definition,
994
+ host,
995
+ localeRevision,
996
+ occurrence,
997
+ rootBinding,
998
+ scopeBinding
999
+ ]);
1000
+ const injected = (0, react.useMemo)(() => runInject(definition, scopeBinding, actions), [
1001
+ actions,
1002
+ definition,
1003
+ scopeBinding
1004
+ ]);
1005
+ assertNoPropOverlap(`factory '${definition.name}' inject`, kit, injected);
1006
+ const provided = {
1007
+ ...kit,
1008
+ ...injected,
1009
+ useFactorySlot: useFactorySlotRuntime
1010
+ };
1011
+ assertNoPropOverlap(`factory '${definition.name}' occurrence`, provided, inputProps);
1012
+ const context = (0, react.useMemo)(() => ({
1013
+ host,
1014
+ definition,
1015
+ selected,
1016
+ registrationKit: {
1017
+ ...registrationKit,
1018
+ ...injected
1019
+ },
1020
+ rootBinding,
1021
+ caller
1022
+ }), [
1023
+ caller,
1024
+ definition,
1025
+ host,
1026
+ injected,
1027
+ registrationKit,
1028
+ rootBinding,
1029
+ selected
1030
+ ]);
1031
+ const Comp = definition.component;
1032
+ return (0, react_jsx_runtime.jsx)(FactoryOccurrenceContext.Provider, {
1033
+ value: context,
1034
+ children: (0, react_jsx_runtime.jsx)(Comp, {
1035
+ ...provided,
1036
+ ...inputProps
1037
+ })
1038
+ });
1039
+ }
1040
+ function FactoryOutlet({ name, inputProps, slots: selected = EMPTY_FACTORY_SELECTION, fallback, caller }) {
1041
+ const host = useHost();
1042
+ const ancestors = (0, react.useContext)(FactoryAncestryContext);
1043
+ const binding = useScopeBinding();
1044
+ const maybeEpoch = useMaybeIncarnation(binding);
1045
+ const version = (0, react.useSyncExternalStore)((listener) => host.subscribeFactory(name, listener), () => host.getFactoryVersion(name));
1046
+ const definition = host.factoryOf(name);
1047
+ if (definition === void 0) return (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: fallback ?? null });
1048
+ if (ancestors.has(name)) throw new _x1a0f3n9_dsh_client_ui_slots.SlotOwnershipError(`recursive render of factory '${name}'`);
1049
+ for (const localName of Object.keys(selected)) if (definition.slots?.[localName] === void 0) throw new _x1a0f3n9_dsh_client_ui_slots.SlotOwnershipError(`local slot '${localName}' is not declared by factory '${name}'`);
1050
+ const nextAncestors = new Set(ancestors).add(name);
1051
+ const scopeIdentity = definition.scope === "root" ? "root" : definition.scope === "session" ? binding.key === void 0 ? "session:absent" : `session:${sessionGenerationKeyOf(binding)}` : `session-maybe:${maybeEpoch}`;
1052
+ return (0, react_jsx_runtime.jsx)(FactoryErrorBoundary, {
1053
+ name,
1054
+ onEntryError: (error) => {
1055
+ host.reportFactoryError(name, definition, error);
1056
+ },
1057
+ children: (0, react_jsx_runtime.jsx)(FactoryAncestryContext.Provider, {
1058
+ value: nextAncestors,
1059
+ children: (0, react_jsx_runtime.jsx)(FactoryOccurrence, {
1060
+ definition,
1061
+ inputProps,
1062
+ selected,
1063
+ caller,
1064
+ binding,
1065
+ maybeEpoch
1066
+ })
1067
+ })
1068
+ }, `${name}:${version}:${scopeIdentity}`);
1069
+ }
744
1070
  function StrictSessionEntry({ slotKey, entry, ownerProps, slotInjected, hookContext, hasHookContext, onEntryError }) {
745
1071
  const binding = useScopeBinding();
746
1072
  if (binding.key === void 0) throw new SlotAssemblyError(`strict session slot '${slotKey}' rendered without a scope binding`);
1073
+ const scopedBinding = binding;
747
1074
  return (0, react_jsx_runtime.jsx)(SlotErrorBoundary, {
748
1075
  slotKey,
749
1076
  onEntryError,
750
1077
  children: (0, react_jsx_runtime.jsx)(SessionEntry, {
751
1078
  entry,
752
1079
  ownerProps,
753
- binding,
1080
+ binding: scopedBinding,
754
1081
  slotKey,
755
1082
  slotInjected,
756
1083
  hookContext,
757
1084
  hasHookContext
758
1085
  })
759
- }, binding.key);
1086
+ }, sessionGenerationKeyOf(scopedBinding));
760
1087
  }
761
1088
  /**
762
1089
  * Anchor style shared by every outlet wrapper: `display:contents` keeps the
@@ -944,8 +1271,8 @@ window.__ModuleLoader__.load({
944
1271
  * declaration injection through the caller's ctx.effect (fiber unload
945
1272
  * collects both), the renderer installation contract (install()/renderSlot('root') +
946
1273
  * the SlotRendererHost face), and the store INSTANCE axis — handle x scope
947
- * key -> create/cache, dropped with the last holding entry, session instances
948
- * cleared (with persisted state) on scope death.
1274
+ * key -> create/cache, dropped with the last holding entry, and in-memory
1275
+ * session instances released without clearing persisted state on scope death.
949
1276
  */
950
1277
  /** Instance key for root-scoped store records (session records key by session id, so the literal cannot collide). */
951
1278
  const ROOT_INSTANCE_KEY = "root";
@@ -954,6 +1281,7 @@ window.__ModuleLoader__.load({
954
1281
  _core = new _x1a0f3n9_dsh_client_ui_slots.SlotCore();
955
1282
  /** Store-instance axis: handle -> mounted scope, refcount, resolved instances. */
956
1283
  _stores = /* @__PURE__ */ new Map();
1284
+ _factoryStores = /* @__PURE__ */ new Map();
957
1285
  /** Latest live Context generation for each scoped store key. */
958
1286
  _storeScopeOwners = /* @__PURE__ */ new Map();
959
1287
  _renderer;
@@ -1149,21 +1477,22 @@ window.__ModuleLoader__.load({
1149
1477
  }, `slots.installScope(${JSON.stringify(scope)})`);
1150
1478
  }
1151
1479
  /**
1152
- * Bind all scoped Store handles to one owner Context lifetime. The cleanup
1153
- * materializes an otherwise-unused handle before clearing it, because a
1154
- * previous application run may have persisted state for a Slot that this
1155
- * scope never rendered. Rebinding the same key transfers cleanup ownership
1156
- * to the newest Context generation.
1480
+ * Bind scoped Store instances to one Context generation. Rebinding the key
1481
+ * drops the previous generation's memory instances before the new owner can
1482
+ * resolve them. Cleanup never clears persisted state, which belongs to the
1483
+ * durable scope key, or drops a replacement generation's instances.
1157
1484
  *
1158
1485
  * @param binding - materialized scope identity and its owning Context.
1159
1486
  */
1160
1487
  bindStoreScope(binding) {
1161
- if (this._storeScopeOwners.get(binding.key) === binding.ctx) return;
1488
+ const current = this._storeScopeOwners.get(binding.key);
1489
+ if (current === binding.ctx) return;
1490
+ if (current !== void 0) this.releaseStoreScope(binding.key);
1162
1491
  this._storeScopeOwners.set(binding.key, binding.ctx);
1163
1492
  binding.ctx.effect(() => () => {
1164
1493
  if (this._storeScopeOwners.get(binding.key) !== binding.ctx) return;
1165
1494
  this._storeScopeOwners.delete(binding.key);
1166
- this.clearStoreScope(binding.key);
1495
+ this.releaseStoreScope(binding.key);
1167
1496
  }, `slots: store scope ${binding.key}`);
1168
1497
  }
1169
1498
  /**
@@ -1192,7 +1521,7 @@ window.__ModuleLoader__.load({
1192
1521
  * Shadowing winners per cell for a key: the first live (non-abdicated)
1193
1522
  * entry of each cell in priority order — what outlets render; chain keys
1194
1523
  * pass through unchanged (election consumes every entry). The raw
1195
- * {@link SlotsService.entries} view stays the inspection surface. Fresh
1524
+ * {@link SlotRegistry.entries} view stays the inspection surface. Fresh
1196
1525
  * array per call, not a uSES getSnapshot source.
1197
1526
  * @param key - SlotMap key.
1198
1527
  * @returns the winning entry per occupied cell.
@@ -1201,22 +1530,20 @@ window.__ModuleLoader__.load({
1201
1530
  return this._core.entriesOfSlot(key);
1202
1531
  }
1203
1532
  /**
1204
- * Export the current JSON-safe Slot declaration tree for read-only inspection.
1205
- * @param root - exact live Slot root; omitted returns all roots.
1206
- * @returns selected Slot trees.
1533
+ * Export the current JSON-safe Slot and Factory declaration trees for read-only inspection.
1534
+ * @param root - exact live Slot key or `factory:<name>`; omitted returns all roots.
1535
+ * @returns selected composition trees.
1207
1536
  */
1208
1537
  snapshot(root) {
1209
1538
  return this._core.snapshot(root);
1210
1539
  }
1211
1540
  /**
1212
- * Observe entry boundary crashes (every render-time entry failure the
1213
- * boundaries contain, abdicating or not) — the supervision seam for
1214
- * plugins mirroring contribution health. Fires synchronously per report,
1215
- * after the registry mutated for abdicating crashes. Callers own the
1216
- * disposer (wire it through ctx.effect for fiber-lifetime cleanup, as with
1217
- * {@link SlotsService.subscribe}).
1218
- * @param fn - called with the slot key, the crashed entry, the crash
1219
- * cause, and `abdicated`: whether the crash retired the entry from its cell.
1541
+ * Observe ordinary entry and Factory occurrence crashes through one
1542
+ * supervision channel. Fires synchronously after any ordinary-entry
1543
+ * abdication mutation. Callers own the disposer (wire it through ctx.effect
1544
+ * for fiber-lifetime cleanup, as with {@link SlotRegistry.subscribe}).
1545
+ * @param fn - called with the Slot or `factory:<name>` key, crashed
1546
+ * registration, cause, and whether an ordinary entry was retired.
1220
1547
  * @returns unsubscribe.
1221
1548
  */
1222
1549
  onEntryError(fn) {
@@ -1269,6 +1596,29 @@ window.__ModuleLoader__.load({
1269
1596
  if (store !== void 0) this._release(store);
1270
1597
  };
1271
1598
  }
1599
+ _registerFactory(options, component) {
1600
+ const registrant = this.ctx.fiber?.name;
1601
+ const erased = {
1602
+ ...options,
1603
+ ...registrant === void 0 ? {} : { registrant }
1604
+ };
1605
+ const dispose = this._core.registerFactory(erased, component);
1606
+ const definition = this._core.factory(options.name);
1607
+ if (definition === void 0) throw new Error(`slot factory "${options.name}" disappeared during registration`);
1608
+ if (definition.store !== void 0 && typeof definition.store !== "function") this._acquire(definition.store, definition.scope);
1609
+ else if (typeof definition.store === "function") this._factoryStores.set(definition, {
1610
+ occurrences: /* @__PURE__ */ new WeakMap(),
1611
+ mounted: /* @__PURE__ */ new Map()
1612
+ });
1613
+ let disposed = false;
1614
+ return () => {
1615
+ if (disposed) return;
1616
+ disposed = true;
1617
+ dispose();
1618
+ this._factoryStores.delete(definition);
1619
+ if (definition.store !== void 0 && typeof definition.store !== "function") this._release(definition.store);
1620
+ };
1621
+ }
1272
1622
  /** Build the domain-neutral host face once; installed adapters remain live through getters. */
1273
1623
  hostFace() {
1274
1624
  if (this._host !== void 0) return this._host;
@@ -1281,9 +1631,18 @@ window.__ModuleLoader__.load({
1281
1631
  reportEntryError: (key, entry, error, info) => {
1282
1632
  this._core.reportEntryError(key, entry, error, info);
1283
1633
  },
1634
+ reportFactoryError: (name, registration, error) => {
1635
+ this._core.reportFactoryError(name, registration, error);
1636
+ },
1284
1637
  specOf: (key) => this._core.specDynamic(key),
1285
1638
  isLive: (entry) => this._core.isLive(entry),
1286
1639
  storeOf: (entry, scopeBinding) => entry.store === void 0 ? void 0 : this.resolveStore(entry.store, scopeBinding),
1640
+ factoryStoreOf: (definition, scopeBinding, occurrence) => this.resolveFactoryStore(definition, scopeBinding, occurrence),
1641
+ retainFactoryOccurrence: (definition, occurrence) => this.retainFactoryOccurrence(definition, occurrence),
1642
+ subscribeFactory: (name, fn) => this._core.subscribeFactory(name, fn),
1643
+ getFactoryVersion: (name) => this._core.factoryVersion(name),
1644
+ factoryOf: (name) => this._core.factory(name),
1645
+ isFactoryLive: (definition) => this._core.isFactoryLive(definition),
1287
1646
  root: this._rootSource,
1288
1647
  scopeRevision: this._scopeRevisionSource,
1289
1648
  scope: (scope) => service._scopes.get(scope === "session-maybe" ? "session" : scope),
@@ -1343,13 +1702,54 @@ window.__ModuleLoader__.load({
1343
1702
  }
1344
1703
  return instance;
1345
1704
  }
1346
- /** Clear every live non-root Store handle for one dead scope key. */
1347
- clearStoreScope(key) {
1348
- for (const [handle, record] of this._stores) {
1705
+ resolveFactoryStore(definition, scopeBinding, occurrence) {
1706
+ if (!this._core.isFactoryLive(definition)) throw new _x1a0f3n9_dsh_client_ui_slots.StaleAuthorizationError(`slot factory "${definition.name}" is not registered`);
1707
+ const declaration = definition.store;
1708
+ if (declaration === void 0) return void 0;
1709
+ if (typeof declaration !== "function") return this.resolveStore(declaration, scopeBinding);
1710
+ const axis = this._factoryStores.get(definition);
1711
+ const scopeKey = definition.scope === "root" ? ROOT_INSTANCE_KEY : requireScopeKey(definition, scopeBinding);
1712
+ if (scopeBinding !== void 0 && definition.scope !== "root") this.bindStoreScope(scopeBinding);
1713
+ let record = axis.occurrences.get(occurrence);
1714
+ if (record === void 0) {
1715
+ const handle = declaration();
1716
+ if (handle.spec.persist !== void 0) throw new SlotAssemblyError(`exclusive store for factory "${definition.name}" cannot declare persistence`);
1717
+ record = {
1718
+ handle,
1719
+ instances: /* @__PURE__ */ new Map(),
1720
+ retainers: 0
1721
+ };
1722
+ axis.occurrences.set(occurrence, record);
1723
+ }
1724
+ const existing = record.instances.get(scopeKey);
1725
+ if (existing !== void 0) return existing;
1726
+ const instance = definition.scope === "root" || scopeBinding === void 0 ? record.handle.create() : record.handle.create(scopeBinding.key);
1727
+ record.instances.set(scopeKey, instance);
1728
+ return instance;
1729
+ }
1730
+ retainFactoryOccurrence(definition, occurrence) {
1731
+ if (!this._core.isFactoryLive(definition)) return () => {};
1732
+ if (typeof definition.store !== "function") return () => {};
1733
+ const axis = this._factoryStores.get(definition);
1734
+ const record = axis.occurrences.get(occurrence);
1735
+ record.retainers += 1;
1736
+ axis.mounted.set(occurrence, record);
1737
+ let released = false;
1738
+ return () => {
1739
+ if (released) return;
1740
+ released = true;
1741
+ record.retainers -= 1;
1742
+ if (record.retainers !== 0) return;
1743
+ axis.mounted.delete(occurrence);
1744
+ };
1745
+ }
1746
+ /** Drop every materialized non-root Store instance for one ended Context generation. */
1747
+ releaseStoreScope(key) {
1748
+ for (const record of this._stores.values()) {
1349
1749
  if (record.scope === "root") continue;
1350
- (record.instances.get(key) ?? handle.create(key)).clearPersisted();
1351
1750
  record.instances.delete(key);
1352
1751
  }
1752
+ for (const axis of this._factoryStores.values()) for (const record of axis.mounted.values()) record.instances.delete(key);
1353
1753
  }
1354
1754
  /** Bind (or re-reference) a handle on the axis; cross-scope conflicts already threw in the core. */
1355
1755
  _acquire(handle, scope) {
@@ -1389,6 +1789,14 @@ window.__ModuleLoader__.load({
1389
1789
  const options = rawOptions;
1390
1790
  return this.ctx.effect(() => this["_register"](options, component), "slots.register()");
1391
1791
  };
1792
+ SlotRegistry.prototype.registerFactory = function registerFactory(rawOptions, component) {
1793
+ const options = rawOptions;
1794
+ return this.ctx.effect(() => this["_registerFactory"](options, component), "slots.registerFactory()");
1795
+ };
1796
+ function requireScopeKey(definition, binding) {
1797
+ if (binding === void 0) throw new Error(`${definition.scope} factory store resolution requires a session id`);
1798
+ return binding.key;
1799
+ }
1392
1800
  //#endregion
1393
1801
  //#region lib/types/client/index.js
1394
1802
  /**
@@ -1,9 +1,6 @@
1
1
  /** Internal React bindings for renderer hosts and standard-source scopes. */
2
2
  import { type ReactNode } from 'react';
3
3
  import type { HostObservable, KeyedStandardSource, MaybeSnapshotSelectorHook, SlotRendererHost, SnapshotSelectorHook, StandardSourceBinding } from '@x1a0f3n9/dsh-client-ui-slots';
4
- /** Missing renderer assembly dependency. */
5
- export declare class SlotAssemblyError extends Error {
6
- }
7
4
  /** In-package renderer host context. */
8
5
  export declare const HostContext: import("react").Context<SlotRendererHost | null>;
9
6
  /**
@@ -21,6 +18,17 @@ export declare function useRootBinding(): StandardSourceBinding;
21
18
  * @returns a binding whose key is absent when no Session is selected.
22
19
  */
23
20
  export declare function useScopeBinding(): StandardSourceBinding;
21
+ /**
22
+ * Publish one resolved scope binding to a renderer subtree.
23
+ * @param props - provider inputs.
24
+ * @param props.binding - binding exposed to scoped entries.
25
+ * @param props.children - subtree that inherits the binding.
26
+ * @returns the scoped React provider.
27
+ */
28
+ export declare function ScopeBindingProvider({ binding, children }: {
29
+ binding: StandardSourceBinding;
30
+ children: ReactNode;
31
+ }): ReactNode;
24
32
  /**
25
33
  * Bind one observable source to an identity-stable selector Hook.
26
34
  * @param source - observable source.
@@ -34,13 +42,13 @@ export declare function observableHook<T>(source: HostObservable<T>): SnapshotSe
34
42
  */
35
43
  export declare function maybeObservableHook<T>(source: HostObservable<T> | undefined): MaybeSnapshotSelectorHook<T>;
36
44
  /** Erased open-key selector Hook synthesized from one keyed source family. */
37
- export type KeyedSnapshotHook = (key: string, selector?: (value: unknown) => unknown, equal?: (left: unknown, right: unknown) => boolean) => unknown;
45
+ export type KeyedSnapshotHook = (keyOrSelector: string | ((value: unknown) => unknown), selectorOrEqual?: ((value: unknown) => unknown) | ((left: unknown, right: unknown) => boolean), equal?: (left: unknown, right: unknown) => boolean) => unknown;
38
46
  /**
39
47
  * Bind an open-key source family.
40
48
  * @param source - keyed resolver, or absence for an optional scope.
41
49
  * @returns cached keyed selector Hook.
42
50
  */
43
- export declare function keyedObservableHook(source: KeyedStandardSource | undefined): KeyedSnapshotHook;
51
+ export declare function keyedObservableHook(source: KeyedStandardSource | undefined, defaultKey?: string): KeyedSnapshotHook;
44
52
  /** Subscribe the tree to the atomically assembled root standard-source roster. */
45
53
  export declare function RootStandardProvider({ children }: {
46
54
  children: ReactNode;
@@ -0,0 +1,5 @@
1
+ /** Shared renderer failure categories. @module */
2
+ /** Renderer assembly failures that must escape component error boundaries. */
3
+ export declare class SlotAssemblyError extends Error {
4
+ }
5
+ //# sourceMappingURL=errors.d.ts.map
@@ -15,7 +15,8 @@ export interface UiRendererService {
15
15
  declare module '@deepseek-ai/cordis' {
16
16
  interface Events {
17
17
  /**
18
- * A slot declaration or registration set changed.
18
+ * An ordinary Slot declaration or entry registration set changed. Factory
19
+ * definitions publish through `subscribeFactory()` instead.
19
20
  * @mode emit
20
21
  * @param key - mutated SlotMap key.
21
22
  */
@@ -6,13 +6,13 @@
6
6
  * declaration injection through the caller's ctx.effect (fiber unload
7
7
  * collects both), the renderer installation contract (install()/renderSlot('root') +
8
8
  * the SlotRendererHost face), and the store INSTANCE axis — handle x scope
9
- * key -> create/cache, dropped with the last holding entry, session instances
10
- * cleared (with persisted state) on scope death.
9
+ * key -> create/cache, dropped with the last holding entry, and in-memory
10
+ * session instances released without clearing persisted state on scope death.
11
11
  */
12
12
  import { Service } from '@deepseek-ai/cordis';
13
13
  import type { Context } from '@deepseek-ai/cordis';
14
14
  import { SlotCore } from '@x1a0f3n9/dsh-client-ui-slots';
15
- import type { LiveSlotNode, LocaleFace, OwnerOf, SlotMap, SlotRenderer, RootStandardSourceContribution, ScopedStandardSourceBinding, SlotScope, SlotScopeAdapter, SlotSpec, StoredEntry } from '@x1a0f3n9/dsh-client-ui-slots';
15
+ import type { LiveCompositionNode, LocaleFace, OwnerOf, RegisterFactory, SlotMap, SlotRenderer, RootStandardSourceContribution, ScopedStandardSourceBinding, SlotScope, SlotScopeAdapter, SlotSpec, StoredFactory, StoredEntry } from '@x1a0f3n9/dsh-client-ui-slots';
16
16
  declare module '@x1a0f3n9/dsh-client-ui-slots' {
17
17
  interface SlotMap {
18
18
  /**
@@ -47,6 +47,7 @@ export declare class SlotRegistry extends Service {
47
47
  private readonly _core;
48
48
  /** Store-instance axis: handle -> mounted scope, refcount, resolved instances. */
49
49
  private readonly _stores;
50
+ private readonly _factoryStores;
50
51
  /** Latest live Context generation for each scoped store key. */
51
52
  private readonly _storeScopeOwners;
52
53
  private _renderer;
@@ -65,7 +66,7 @@ export declare class SlotRegistry extends Service {
65
66
  */
66
67
  constructor(ctx: Context);
67
68
  /**
68
- * The single registration API. The typed face IS the core's register
69
+ * The ordinary Slot registration API. The typed face IS the core's register
69
70
  * (both overloads reused verbatim — one authority, no structural copy;
70
71
  * see SlotCore.register for children declaration, store seat, inject
71
72
  * face, load-time validation, and the unload cascade). This layer adds:
@@ -82,6 +83,16 @@ export declare class SlotRegistry extends Service {
82
83
  * own root ctx and silently break per-plugin disposal.
83
84
  */
84
85
  readonly register: SlotCore['register'];
86
+ /**
87
+ * Register one reusable Component Factory under the caller's effect lifetime.
88
+ * A Store factory mints one handle per rendered occurrence rather than per
89
+ * definition. Like {@link SlotRegistry.register}, this remains a prototype
90
+ * method so the Cordis proxy binds `this.ctx` to the caller's Context.
91
+ * @param options - runtime definition checked against `SlotFactoryMap`.
92
+ * @param component - reusable Factory Component.
93
+ * @returns the idempotent definition disposer.
94
+ */
95
+ readonly registerFactory: RegisterFactory;
85
96
  /**
86
97
  * Install an effect for each declaration lifetime of a slot. The callback
87
98
  * runs synchronously when the declaration already exists; otherwise it runs
@@ -128,11 +139,10 @@ export declare class SlotRegistry extends Service {
128
139
  */
129
140
  installScope(scope: Exclude<SlotScope, 'root' | 'session-maybe'>, adapter: SlotScopeAdapter): void;
130
141
  /**
131
- * Bind all scoped Store handles to one owner Context lifetime. The cleanup
132
- * materializes an otherwise-unused handle before clearing it, because a
133
- * previous application run may have persisted state for a Slot that this
134
- * scope never rendered. Rebinding the same key transfers cleanup ownership
135
- * to the newest Context generation.
142
+ * Bind scoped Store instances to one Context generation. Rebinding the key
143
+ * drops the previous generation's memory instances before the new owner can
144
+ * resolve them. Cleanup never clears persisted state, which belongs to the
145
+ * durable scope key, or drops a replacement generation's instances.
136
146
  *
137
147
  * @param binding - materialized scope identity and its owning Context.
138
148
  */
@@ -156,30 +166,28 @@ export declare class SlotRegistry extends Service {
156
166
  * Shadowing winners per cell for a key: the first live (non-abdicated)
157
167
  * entry of each cell in priority order — what outlets render; chain keys
158
168
  * pass through unchanged (election consumes every entry). The raw
159
- * {@link SlotsService.entries} view stays the inspection surface. Fresh
169
+ * {@link SlotRegistry.entries} view stays the inspection surface. Fresh
160
170
  * array per call, not a uSES getSnapshot source.
161
171
  * @param key - SlotMap key.
162
172
  * @returns the winning entry per occupied cell.
163
173
  */
164
174
  entriesOfSlot(key: keyof SlotMap & string): readonly StoredEntry[];
165
175
  /**
166
- * Export the current JSON-safe Slot declaration tree for read-only inspection.
167
- * @param root - exact live Slot root; omitted returns all roots.
168
- * @returns selected Slot trees.
176
+ * Export the current JSON-safe Slot and Factory declaration trees for read-only inspection.
177
+ * @param root - exact live Slot key or `factory:<name>`; omitted returns all roots.
178
+ * @returns selected composition trees.
169
179
  */
170
- snapshot(root?: string): LiveSlotNode[];
180
+ snapshot(root?: string): LiveCompositionNode[];
171
181
  /**
172
- * Observe entry boundary crashes (every render-time entry failure the
173
- * boundaries contain, abdicating or not) — the supervision seam for
174
- * plugins mirroring contribution health. Fires synchronously per report,
175
- * after the registry mutated for abdicating crashes. Callers own the
176
- * disposer (wire it through ctx.effect for fiber-lifetime cleanup, as with
177
- * {@link SlotsService.subscribe}).
178
- * @param fn - called with the slot key, the crashed entry, the crash
179
- * cause, and `abdicated`: whether the crash retired the entry from its cell.
182
+ * Observe ordinary entry and Factory occurrence crashes through one
183
+ * supervision channel. Fires synchronously after any ordinary-entry
184
+ * abdication mutation. Callers own the disposer (wire it through ctx.effect
185
+ * for fiber-lifetime cleanup, as with {@link SlotRegistry.subscribe}).
186
+ * @param fn - called with the Slot or `factory:<name>` key, crashed
187
+ * registration, cause, and whether an ordinary entry was retired.
180
188
  * @returns unsubscribe.
181
189
  */
182
- onEntryError(fn: (key: string, entry: StoredEntry, error: unknown, info: {
190
+ onEntryError(fn: (key: string, registration: StoredEntry | StoredFactory, error: unknown, info: {
183
191
  abdicated: boolean;
184
192
  }) => void): () => void;
185
193
  /**
@@ -203,6 +211,7 @@ export declare class SlotRegistry extends Service {
203
211
  getVersion(key: keyof SlotMap & string): number;
204
212
  /** Delegating registration path: factory minting + registrant stamp + core write + instance-axis bookkeeping. */
205
213
  private _register;
214
+ private _registerFactory;
206
215
  /** Build the domain-neutral host face once; installed adapters remain live through getters. */
207
216
  private hostFace;
208
217
  /** Validate and atomically publish the current root contribution roster. */
@@ -211,8 +220,10 @@ export declare class SlotRegistry extends Service {
211
220
  private publishScopeRevision;
212
221
  /** Resolve (create or reuse) the store instance for a registered handle under a scope key. */
213
222
  private resolveStore;
214
- /** Clear every live non-root Store handle for one dead scope key. */
215
- private clearStoreScope;
223
+ private resolveFactoryStore;
224
+ private retainFactoryOccurrence;
225
+ /** Drop every materialized non-root Store instance for one ended Context generation. */
226
+ private releaseStoreScope;
216
227
  /** Bind (or re-reference) a handle on the axis; cross-scope conflicts already threw in the core. */
217
228
  private _acquire;
218
229
  /** Drop one reference; the last holder's unload drops the record (instances go with it — engine stores need no explicit dispose). */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x1a0f3n9/dsh-client-ui-renderer",
3
3
  "description": "Browser UI renderer: React slot bindings, ctx.uiRenderer, and the assembled application root",
4
- "version": "0.1.5-rc.4",
4
+ "version": "0.1.7-rc.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "license": "MIT",
39
39
  "peerDependencies": {
40
- "@deepseek-ai/cordis": "^4.0.2"
40
+ "@deepseek-ai/cordis": "~4.0.4"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/react": "~18.3.1",
@@ -46,10 +46,10 @@
46
46
  "react": "^18.2.0",
47
47
  "react-dom": "^18.2.0",
48
48
  "use-sync-external-store": "1.2.0",
49
- "@deepseek-ai/cordis": "^4.0.2",
50
- "@x1a0f3n9/dsh-client-test-runtime": "^0.1.5-rc.4",
51
- "@x1a0f3n9/dsh-client-ui-slots": "^0.1.5-rc.4",
52
- "@x1a0f3n9/dsh-invariants": "^0.1.5-rc.4"
49
+ "@deepseek-ai/cordis": "~4.0.4",
50
+ "@x1a0f3n9/dsh-client-test-runtime": "0.1.7-rc.2",
51
+ "@x1a0f3n9/dsh-client-ui-slots": "0.1.7-rc.2",
52
+ "@x1a0f3n9/dsh-invariants": "0.1.7-rc.2"
53
53
  },
54
54
  "files": [
55
55
  "lib/index.js",