@deepseek-ai/dsh-client-ui-slots 0.1.6-alpha.1 → 0.1.6-alpha.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
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-slots/README.md
5
- README.md: d38fe059693f7b15d060b0a52c97edd0b5e050c9
6
- README.zh.md: 968470edd20d75b7c6bf8f7c8fca898764af757e
5
+ README.md: d82b303d5d6efaebbccaab00d06d874d79165fb1
6
+ README.zh.md: 2a9d0364eda148ddf8db8cc61daf1747381c4875
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Slot registry pure core for the dsh web client: SlotMap declaration merging, the single register composition API, four-share props types, store seats, and the renderer install contract."
2
+ description: "Slot registry pure core for the dsh web client: ordinary extension slots, reusable Component Factories, derived props types, store seats, and the renderer install contract."
3
3
  kind: "package-library"
4
4
  ---
5
5
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- `dsh-client-ui-slots` lets web client plugins define and compose typed UI regions. Callers can add components, declare nested regions, attach scoped state, and supply business props through one compile-time-checked API. It supports single, ordered-list, keyed, and self-selecting chain composition, and reports conflicting compositions during plugin loading. Choose it for framework-neutral slot composition; pair it with `ui-renderer` when the client needs React rendering.
12
+ `dsh-client-ui-slots` lets web client plugins define and compose typed UI regions. Ordinary Slots provide parent-owned extension positions; Component Factories provide reusable assemblies with caller-selected local Components. Both APIs derive scoped state, injection, locale, and child-render props from declaration-merged types and report conflicting definitions during plugin loading. Pair this React-free package with `ui-renderer` when the client needs rendering.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -27,9 +27,15 @@ English | [中文](README.zh.md)
27
27
 
28
28
  Compose UI through this package whenever you write a client plugin: register a component into a slot your parent declared, or declare child slots your component renders. The four kinds cover the composition shapes — `single` (one occupant), `list` (ordered entries), `keyed` (dispatch by a key), and `chain` (entries elect themselves).
29
29
 
30
- ### The four props shares
30
+ ### Reusable Component Factories
31
31
 
32
- Every registered component receives props composed from four shares: the runtime share (`owner` from the parent's renderSlot call site, plus the session standard kit and global seat), the child-render share (`renderSlot` statically narrowed to the declared children keys), the store share (the declared handle's selector hook and draft-stripped actions), and the business share (inferred from the `inject` factory's return). Components reference `ComposedProps`; they never re-type a share locally.
32
+ Use a Component Factory when one package defines an assembly that unrelated parents render independently. Declare its complete type in `SlotFactoryMap`, install the definition with `ctx.slots.registerFactory()`, render occurrences through the injected `renderFactorySlot()`, and select each declared local Component through the call's `slots` option. The definition reads that choice through `useFactorySlot(name, fallback)`.
33
+
34
+ Factory `children` remain ordinary global Slots and must match `SlotMap`, while local `slots` select one Component per occurrence. An occurrence inherits its render-position scope; `renderFactorySlot()` does not accept a Session identity. Shared Store handles use ordinary scope resolution. A Store factory stays lazy until an occurrence first materializes, then creates one handle for that render position and rejects a persistent Store spec whose key would collide across occurrences.
35
+
36
+ ### The five framework props shares
37
+
38
+ Every registered component receives props composed from five framework shares: the runtime share (`owner` from the parent's render call site, plus the session standard kit and global seat), the child-render share (`renderSlot` statically narrowed to declared children), the Factory-render share (`renderFactorySlot`), the store share (the declared handle's selector hook and draft-stripped actions), and the business share (inferred from `inject`). Components reference the derived props aliases; they never re-type a share locally.
33
39
 
34
40
  ### Store seats
35
41
 
@@ -47,15 +53,15 @@ Declaring a slot is claiming it: the registering entry becomes the only entry al
47
53
  <details>
48
54
  <summary>Implementation internals — click to expand</summary>
49
55
 
50
- The design is one table: declaration = render authorization = runtime spec. `SlotMap` is declared empty here and merged by consumers via `declare module` augmentation, exactly like the standard-kit interfaces (`SessionStandardProps`, `GlobalStandardProps`), which the runtime package merges with real members.
56
+ The ordinary Slot design is one table: declaration = render authorization = runtime spec. `SlotMap` is declared empty here and merged by consumers via `declare module` augmentation, exactly like `SlotFactoryMap` and the standard-kit interfaces (`SessionStandardProps`, `GlobalStandardProps`). Factory definitions use a separate single-definition ledger because their occurrences have no parent declaration.
51
57
 
52
58
  ### Registration and routing
53
59
 
54
- `SlotCore` seeds the a-priori `'root'` slot at construction and enforces load-time validation. `ChainSelect` selectors run in ascending `priority` order (ties in registration order); the first non-null return elects its entry and becomes the component's `matched` prop, and all-null falls to the owner's `renderSlotChain` fallback (`ChainRenderOpts`). Each key carries a declaration epoch that advances only on declaration and collapse; `ui-renderer` uses it for `ctx.slots.inject`, independently from ordinary entry versions.
60
+ `SlotCore` seeds the a-priori `'root'` slot at construction and enforces load-time validation. `ChainSelect` selectors run in ascending `priority` order (ties in registration order); the first non-null return elects its entry and becomes the component's `matched` prop, and all-null falls to the owner's `renderSlotChain` fallback (`ChainRenderOpts`). Each key carries a declaration epoch that advances only on declaration and collapse; `ui-renderer` uses it for `ctx.slots.inject`, independently from ordinary entry versions. Live inspection uses strict `type: 'slot' | 'factory'` nodes and nests Factory-owned child Slots under their definition.
55
61
 
56
62
  ### The renderer contract
57
63
 
58
- `renderer.ts` carries the installation contract (`SlotRenderer`, `SlotRendererHost`) plus `StaleAuthorizationError`/`SlotOwnershipError`; ui-renderer owns both the implementation and its plugin-lifecycle installation. Engine products and the renderer host contract carry bare snapshot sources (`getSnapshot`/`subscribe`), never React hooks — hook binding belongs to the render machinery.
64
+ `renderer.ts` carries the installation contract (`SlotRenderer`, `SlotRendererHost`) plus `StaleAuthorizationError`/`SlotOwnershipError`; ui-renderer owns both the implementation and its plugin-lifecycle installation. Engine products and the renderer host contract carry bare snapshot sources (`getSnapshot`/`subscribe`), never React hooks — hook binding belongs to the render machinery. Factory crashes use the ordinary supervision channel, and an idempotent effect retains per-position Store handles only after commit.
59
65
 
60
66
  </details>
61
67
 
@@ -68,6 +74,7 @@ These pages cover the engine, the renderer, and the composition model.
68
74
 
69
75
  - [ui-renderer](../ui-renderer/README.md) — the React slot renderer implementing this package's install contract.
70
76
  - [Slot system standard](../../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md) — the definitive composition model.
77
+ - [Component Factories](../../../.agents/notes/implemented/architecture/2026-09-10-component-factories-and-local-slots.md) — reusable definitions, local Component selection, and occurrence lifetimes.
71
78
  - [Web client architecture](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) — the loading chain and object layer this registry plugs into.
72
79
 
73
80
  -----
package/README.zh.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "dsh Web 客户端的 slot 注册表纯核心:SlotMap 声明合并、单一 register 组合 API、四 share props 类型、store 席位与渲染器安装约定。"
2
+ description: "dsh Web 客户端的 slot 注册表纯核心:普通扩展 slots、可复用 Component Factory、推导 props 类型、store 席位与渲染器安装约定。"
3
3
  kind: "package-library"
4
4
  ---
5
5
 
@@ -9,7 +9,7 @@ kind: "package-library"
9
9
 
10
10
  ## 概述
11
11
 
12
- `dsh-client-ui-slots` 让 Web 客户端插件定义并组合带类型检查的 UI 区域。调用方可以通过一个在编译期检查的 API 添加组件、声明嵌套区域、附加作用域状态并提供业务 props。它支持单项、有序列表、键控和自行选择的 chain 组合,并会在插件加载期间报告冲突组合。需要与框架无关的 slot 组合时选择本包;客户端需要 React 渲染时与 `ui-renderer` 配合使用。
12
+ `dsh-client-ui-slots` 让 Web 客户端插件定义并组合带类型检查的 UI 区域。普通 Slots 提供 parent-owned 扩展位置;Component Factory 提供带调用方所选局部 Component 的可复用装配。两套 API 都从声明合并类型推导 scoped state、injection、locale 与 child-render props,并在插件加载期间报告冲突 definition。客户端需要渲染时,将这个不依赖 React 的包与 `ui-renderer` 配合使用。
13
13
 
14
14
  ## 目录
15
15
 
@@ -27,9 +27,15 @@ kind: "package-library"
27
27
 
28
28
  编写客户端插件时都通过本包组合 UI:把组件注册进父级已声明的 slot,或声明组件将要渲染的子 slot。四种 kind 覆盖组合形态——`single`(单个占位者)、`list`(有序条目)、`keyed`(按键分派)与 `chain`(条目自行提名)。
29
29
 
30
- ### 四个 props share
30
+ ### 可复用 Component Factory
31
31
 
32
- 每个已注册组件都会收到由四个 share 组合而成的 props:运行时 share(父级 renderSlot 调用点的 `owner`,加上会话标准工具包与全局席位)、child render share(静态缩窄到已声明 children key 的 `renderSlot`)、store share(已声明句柄的 selector 钩子与移除 draft 的 actions),以及业务 share(从 `inject` factory 返回值推断)。组件引用 `ComposedProps`;它们绝不在本地重新定义任何 share 的类型。
32
+ 当一个包定义装配、而互不相关的 parents 需要独立渲染它时,使用 Component Factory。在 `SlotFactoryMap` 中声明完整类型,通过 `ctx.slots.registerFactory()` 安装 definition,通过注入的 `renderFactorySlot()` 渲染 occurrences,并通过调用的 `slots` 选项选择每个已声明的局部 Component。definition 通过 `useFactorySlot(name, fallback)` 读取该选择。
33
+
34
+ Factory `children` 仍是普通全局 Slots 且必须与 `SlotMap` 匹配,而局部 `slots` 为每个 occurrence 选择一个 Component。occurrence 继承其渲染位置的 scope;`renderFactorySlot()` 不接受 Session identity。共享 Store handle 使用普通 scope 解析。Store factory 保持 lazy,直到 occurrence 首次物化时才为该渲染位置创建一个 handle;若持久化 Store spec 会让 persistence key 在 occurrences 之间冲突,renderer 会拒绝它。
35
+
36
+ ### 五个框架 props share
37
+
38
+ 每个已注册组件都会收到由五个框架 share 组合而成的 props:运行时 share(父级 render 调用点的 `owner`,加上会话标准工具包与全局席位)、child render share(静态缩窄到已声明 children 的 `renderSlot`)、Factory render share(`renderFactorySlot`)、store share(已声明 handle 的 selector 钩子与移除 draft 的 actions),以及业务 share(从 `inject` 推导)。组件引用推导出的 props 别名;它们绝不在本地重新定义任何 share 的类型。
33
39
 
34
40
  ### Store 席位
35
41
 
@@ -47,15 +53,15 @@ register 调用可以用 `store: defineStore(...)` 声明 store 席位:`init`
47
53
  <details>
48
54
  <summary>实现细节——点击展开</summary>
49
55
 
50
- 设计就是一张表:声明 = 渲染授权 = 运行时规范。`SlotMap` 在这里声明为空,由消费方通过 `declare module` 增补合并,标准工具包接口(`SessionStandardProps`、`GlobalStandardProps`)也是如此,由运行时包以真实成员合并。
56
+ 普通 Slot 设计就是一张表:声明 = 渲染授权 = 运行时规范。`SlotMap` 在这里声明为空,由消费方通过 `declare module` 增补合并;`SlotFactoryMap` 和标准工具包接口(`SessionStandardProps`、`GlobalStandardProps`)也采用同一方式。Factory definition 使用独立的单 definition ledger,因为其 occurrences 没有 parent 声明。
51
57
 
52
58
  ### 注册与路由
53
59
 
54
- `SlotCore` 在构造时预置 `'root'` slot,并强制执行加载时验证。`ChainSelect` selector 按升序 `priority` 运行(相同值按注册顺序);第一个非 null 返回值选中其条目,并成为组件的 `matched` prop;全部返回 null 时使用 owner 的 `renderSlotChain` fallback(`ChainRenderOpts`)。每个 key 都携带一个 declaration epoch,它只在声明与移除时递增;`ui-renderer` 将其用于 `ctx.slots.inject`,且与普通条目版本相互独立。
60
+ `SlotCore` 在构造时预置 `'root'` slot,并强制执行加载时验证。`ChainSelect` selector 按升序 `priority` 运行(相同值按注册顺序);第一个非 null 返回值选中其条目,并成为组件的 `matched` prop;全部返回 null 时使用 owner 的 `renderSlotChain` fallback(`ChainRenderOpts`)。每个 key 都携带一个 declaration epoch,它只在声明与移除时递增;`ui-renderer` 将其用于 `ctx.slots.inject`,且与普通条目版本相互独立。实时检查使用严格的 `type: 'slot' | 'factory'` 节点,并把 Factory-owned child Slots 嵌套在其 definition 下。
55
61
 
56
62
  ### 渲染器约定
57
63
 
58
- `renderer.ts` 携带安装约定(`SlotRenderer`、`SlotRendererHost`)以及 `StaleAuthorizationError`/`SlotOwnershipError`;ui-renderer 负责实现,并在其插件生命周期中完成安装。引擎产物与渲染器宿主约定携带裸快照 source(`getSnapshot`/`subscribe`),绝不携带 React 钩子——钩子绑定属于渲染机制。
64
+ `renderer.ts` 携带安装约定(`SlotRenderer`、`SlotRendererHost`)以及 `StaleAuthorizationError`/`SlotOwnershipError`;ui-renderer 负责实现,并在其插件生命周期中完成安装。引擎产物与渲染器宿主约定携带裸快照 source(`getSnapshot`/`subscribe`),绝不携带 React 钩子——钩子绑定属于渲染机制。Factory 崩溃使用普通监督通道,幂等 effect 仅在 commit 后保留逐渲染位置 Store handle。
59
65
 
60
66
  </details>
61
67
 
@@ -68,6 +74,7 @@ register 调用可以用 `store: defineStore(...)` 声明 store 席位:`init`
68
74
 
69
75
  - [ui-renderer](../ui-renderer/README.zh.md)——实现本包安装约定的 React slot 渲染器。
70
76
  - [slot 系统标准](../../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md)——权威组合模型。
77
+ - [Component Factory](../../../.agents/notes/implemented/architecture/2026-09-10-component-factories-and-local-slots.zh.md)——可复用 definitions、局部 Component 选择与 occurrence 生命周期。
71
78
  - [Web 客户端架构](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md)——本注册表接入的加载链与对象层。
72
79
 
73
80
  -----
package/lib/index.js CHANGED
@@ -46,6 +46,7 @@ const NO_ENTRIES = Object.freeze([]);
46
46
  */
47
47
  var SlotCore = class {
48
48
  records = /* @__PURE__ */ new Map();
49
+ factories = /* @__PURE__ */ new Map();
49
50
  mutateListeners = /* @__PURE__ */ new Set();
50
51
  /** Shared-handle scope ledger: handle → the scope it first mounted under + live mount count. */
51
52
  handleScopes = /* @__PURE__ */ new Map();
@@ -69,6 +70,96 @@ var SlotCore = class {
69
70
  root.declaredBy = "(built-in)";
70
71
  root.declarationEpoch = 1;
71
72
  }
73
+ /** Register one reusable Factory definition. */
74
+ registerFactory = ((rawOptions, component) => {
75
+ const options = rawOptions;
76
+ const record = this.factoryRecord(options.name);
77
+ if (record.definition !== void 0) throw new Error(`slot factory "${options.name}" already has a definition`);
78
+ for (const childKey of Object.keys(options.children ?? {})) {
79
+ const childRecord = this.records.get(childKey);
80
+ if (childRecord?.spec !== void 0) throw new Error(`slot "${childKey}" is already declared (by ${childRecord.declaredBy ?? "an unknown entry"})`);
81
+ }
82
+ if (options.store !== void 0 && typeof options.store !== "function") {
83
+ const pinned = this.handleScopes.get(options.store);
84
+ if (pinned !== void 0 && pinned.scope !== options.scope) throw new Error(`store handle mounted under factory "${options.name}" (scope "${options.scope}") is already mounted under scope "${pinned.scope}" — one handle, one scope`);
85
+ if (pinned !== void 0) pinned.count += 1;
86
+ else this.handleScopes.set(options.store, {
87
+ scope: options.scope,
88
+ count: 1
89
+ });
90
+ }
91
+ const definition = {
92
+ name: options.name,
93
+ component,
94
+ scope: options.scope,
95
+ ...options.children === void 0 ? {} : { children: options.children },
96
+ ...options.store === void 0 ? {} : { store: options.store },
97
+ ...options.inject === void 0 ? {} : { inject: options.inject },
98
+ ...options.locale === void 0 ? {} : { locale: options.locale },
99
+ ...options.slots === void 0 ? {} : { slots: options.slots },
100
+ ...options.registrant === void 0 ? {} : { registrant: options.registrant }
101
+ };
102
+ record.definition = definition;
103
+ this.markFactoryDirty(record);
104
+ const declarations = [];
105
+ for (const [childKey, childSpec] of Object.entries(options.children ?? {})) {
106
+ const childRecord = this.record(childKey);
107
+ childRecord.spec = childSpec;
108
+ childRecord.declaredBy = `factory "${options.name}"${options.registrant ? ` (${options.registrant})` : ""}`;
109
+ childRecord.parent = `factory:${options.name}`;
110
+ childRecord.declarationEpoch += 1;
111
+ declarations.push([childKey, childRecord]);
112
+ }
113
+ for (const [childKey, childRecord] of declarations) this.markDirty(childKey, childRecord);
114
+ for (const [, childRecord] of declarations) this.notifyDeclaration(childRecord);
115
+ return () => {
116
+ if (record.definition !== definition) return;
117
+ record.definition = void 0;
118
+ this.markFactoryDirty(record);
119
+ if (definition.store !== void 0 && typeof definition.store !== "function") {
120
+ const pinned = this.handleScopes.get(definition.store);
121
+ if (pinned !== void 0 && --pinned.count === 0) this.handleScopes.delete(definition.store);
122
+ }
123
+ this.releaseChildren(definition.children);
124
+ };
125
+ });
126
+ /**
127
+ * Read one registered Factory definition.
128
+ * @param name - Factory name.
129
+ * @returns the live definition, or `undefined` when absent.
130
+ */
131
+ factory(name) {
132
+ return this.factories.get(name)?.definition;
133
+ }
134
+ /**
135
+ * Read the monotonic definition version for one Factory name.
136
+ * @param name - Factory name.
137
+ * @returns the current version.
138
+ */
139
+ factoryVersion(name) {
140
+ return this.factories.get(name)?.version ?? 0;
141
+ }
142
+ /**
143
+ * Subscribe to one Factory definition's registration lifetime.
144
+ * @param name - Factory name.
145
+ * @param listener - callback notified after a definition change.
146
+ * @returns the unsubscribe function.
147
+ */
148
+ subscribeFactory(name, listener) {
149
+ const record = this.factoryRecord(name);
150
+ record.listeners.add(listener);
151
+ return () => {
152
+ record.listeners.delete(listener);
153
+ };
154
+ }
155
+ /**
156
+ * Return whether a retained Factory definition is still registered.
157
+ * @param definition - retained definition identity.
158
+ * @returns whether that exact definition remains live.
159
+ */
160
+ isFactoryLive(definition) {
161
+ return this.factories.get(definition.name)?.definition === definition;
162
+ }
72
163
  register(options, component) {
73
164
  const rec = this.records.get(options.name);
74
165
  if (!rec?.spec) throw new Error(`slot "${options.name}" is not declared (a parent entry's children table must declare it)`);
@@ -220,21 +311,24 @@ var SlotCore = class {
220
311
  }
221
312
  /**
222
313
  * Export the current declaration topology without components or executable hooks.
223
- * @param root - exact Slot key to select; omitted returns every live root.
314
+ * Factory definitions appear as `factory:<name>` parents of their ordinary
315
+ * child Slots, matching the parent/child topology of ordinary registrations.
316
+ * @param root - exact Slot or `factory:<name>` key to select; omitted returns every live root.
224
317
  * @returns selected live Slot trees, or an empty array when `root` is unavailable.
225
318
  */
226
319
  snapshot(root) {
227
- const build = (name, seen) => {
320
+ const buildSlot = (name, seen) => {
228
321
  const record = this.records.get(name);
229
322
  if (record?.spec === void 0 || seen.has(name)) return void 0;
230
323
  const branch = new Set(seen);
231
324
  branch.add(name);
232
325
  const active = new Set(this.entriesOfSlot(name));
233
326
  const children = [...this.records.entries()].filter(([, candidate]) => candidate.spec !== void 0 && candidate.parent === name).flatMap(([child]) => {
234
- const node = build(child, branch);
327
+ const node = buildSlot(child, branch);
235
328
  return node === void 0 ? [] : [node];
236
329
  });
237
330
  return {
331
+ type: "slot",
238
332
  name,
239
333
  kind: record.spec.kind,
240
334
  scope: record.spec.scope,
@@ -250,14 +344,35 @@ var SlotCore = class {
250
344
  children
251
345
  };
252
346
  };
347
+ const buildFactory = (name) => {
348
+ const definition = this.factories.get(name)?.definition;
349
+ if (definition === void 0) return void 0;
350
+ const nodeName = `factory:${name}`;
351
+ const children = [...this.records.entries()].filter(([, candidate]) => candidate.spec !== void 0 && candidate.parent === nodeName).flatMap(([child]) => {
352
+ const node = buildSlot(child, new Set([nodeName]));
353
+ return node === void 0 ? [] : [node];
354
+ });
355
+ return {
356
+ type: "factory",
357
+ name,
358
+ scope: definition.scope,
359
+ ...definition.registrant === void 0 ? {} : { registrant: definition.registrant },
360
+ children
361
+ };
362
+ };
253
363
  if (root !== void 0) {
254
- const node = build(root, /* @__PURE__ */ new Set());
364
+ const node = root.startsWith("factory:") ? buildFactory(root.slice(8)) : buildSlot(root, /* @__PURE__ */ new Set());
255
365
  return node === void 0 ? [] : [node];
256
366
  }
257
- return [...this.records.entries()].filter(([, record]) => record.spec !== void 0 && (record.parent === void 0 || this.records.get(record.parent)?.spec === void 0)).flatMap(([name]) => {
258
- const node = build(name, /* @__PURE__ */ new Set());
367
+ const slots = [...this.records.entries()].filter(([, record]) => record.spec !== void 0 && record.parent === void 0).flatMap(([name]) => {
368
+ const node = buildSlot(name, /* @__PURE__ */ new Set());
369
+ return node === void 0 ? [] : [node];
370
+ });
371
+ const factories = [...this.factories.keys()].flatMap((name) => {
372
+ const node = buildFactory(name);
259
373
  return node === void 0 ? [] : [node];
260
374
  });
375
+ return [...slots, ...factories];
261
376
  }
262
377
  /**
263
378
  * Read the declaration lifetime of a key. Entry additions and removals do
@@ -347,13 +462,21 @@ var SlotCore = class {
347
462
  for (const fn of [...this.entryErrorListeners]) fn(key, entry, error, { abdicated: info.abdicate });
348
463
  }
349
464
  /**
350
- * Observe entry boundary crashes (every render-time entry failure the
351
- * boundaries contain, abdicating or not) — the supervision seam for hosts
352
- * mirroring contribution health. Fires synchronously per report, after the
353
- * registry mutated for abdicating crashes (same listener discipline as
354
- * {@link SlotCore.onMutate}).
355
- * @param fn - called with the slot key, the crashed entry, the crash
356
- * cause, and `abdicated`: whether the crash retired the entry from its cell.
465
+ * Report a contained Factory occurrence crash through the ordinary entry
466
+ * supervision channel without retiring the shared definition.
467
+ * @param name - Factory name whose occurrence crashed.
468
+ * @param registration - Factory definition or caller registration that owns the crashing Component.
469
+ * @param error - the crash cause, forwarded to listeners verbatim.
470
+ */
471
+ reportFactoryError(name, registration, error) {
472
+ for (const fn of [...this.entryErrorListeners]) fn(`factory:${name}`, registration, error, { abdicated: false });
473
+ }
474
+ /**
475
+ * Observe ordinary entry and Factory occurrence crashes. Fires synchronously
476
+ * per report, after any ordinary-entry abdication mutation. Factory failures
477
+ * never retire their shared definition.
478
+ * @param fn - called with the Slot or `factory:<name>` key, the crashed
479
+ * registration, the cause, and whether an ordinary entry was retired.
357
480
  * @returns unsubscribe.
358
481
  */
359
482
  onEntryError(fn) {
@@ -373,8 +496,11 @@ var SlotCore = class {
373
496
  const pinned = this.handleScopes.get(entry.store);
374
497
  if (pinned && --pinned.count === 0) this.handleScopes.delete(entry.store);
375
498
  }
376
- if (!entry.children) return;
377
- for (const childKey of Object.keys(entry.children)) {
499
+ this.releaseChildren(entry.children);
500
+ }
501
+ releaseChildren(children) {
502
+ if (children === void 0) return;
503
+ for (const childKey of Object.keys(children)) {
378
504
  const childRec = this.records.get(childKey);
379
505
  /* v8 ignore next -- defensive: declaring always creates the record */
380
506
  if (!childRec) continue;
@@ -406,6 +532,24 @@ var SlotCore = class {
406
532
  }
407
533
  return rec;
408
534
  }
535
+ factoryRecord(name) {
536
+ let record = this.factories.get(name);
537
+ if (record === void 0) {
538
+ record = {
539
+ definition: void 0,
540
+ version: 0,
541
+ listeners: /* @__PURE__ */ new Set()
542
+ };
543
+ this.factories.set(name, record);
544
+ }
545
+ return record;
546
+ }
547
+ markFactoryDirty(record) {
548
+ record.version += 1;
549
+ queueMicrotask(() => {
550
+ for (const listener of [...record.listeners]) listener();
551
+ });
552
+ }
409
553
  markDirty(key, rec) {
410
554
  rec.version += 1;
411
555
  for (const fn of [...this.mutateListeners]) fn(key);
@@ -16,6 +16,9 @@ export * from './renderer.ts';
16
16
  /** Slot contract table. Owners extend via declaration merging; entries are {@link SlotEntryDef}. */
17
17
  export interface SlotMap {
18
18
  }
19
+ /** Reusable Component Factory contract table, extended through declaration merging. */
20
+ export interface SlotFactoryMap {
21
+ }
19
22
  /**
20
23
  * Locale namespace table. Dictionary owners extend via declaration merging
21
24
  * (exactly like {@link SlotMap}, and declared in this entry module for the
@@ -80,12 +83,15 @@ export type PropsLocale<N> = N extends keyof LocaleNamespaceMap & string ? {
80
83
  export type SlotKind = 'single' | 'list' | 'keyed' | 'chain';
81
84
  /** Slot data context: global, current-session-optional, or strict session-bound. */
82
85
  export type SlotScope = 'root' | 'session-maybe' | 'session';
86
+ /** Declaration-merged explicit target types for non-root scope Providers. */
87
+ export interface SlotScopeTargetMap {
88
+ }
83
89
  /**
84
90
  * One SlotMap entry: kind/scope axes plus the optional owner-supplied props
85
91
  * share (`owner` is what the parent passes at its renderSlot call site; the
86
92
  * framework standard kit and the registrant's injected share never enter this
87
- * table — full component props compose at the component as the four-share
88
- * intersection, see {@link ComposedProps}).
93
+ * table — full component props compose at the component from the framework
94
+ * shares, see {@link ComposedProps}).
89
95
  */
90
96
  export interface SlotEntryDef {
91
97
  kind: SlotKind;
@@ -110,6 +116,21 @@ export interface SlotEntryDef {
110
116
  */
111
117
  inject?: object;
112
118
  }
119
+ /** One caller-selected Component position inside a reusable Factory. */
120
+ export interface FactoryLocalSlotDef {
121
+ scope: SlotScope;
122
+ props?: object;
123
+ }
124
+ /** Complete static definition of one reusable Component Factory. */
125
+ export interface SlotFactoryDef {
126
+ scope: SlotScope;
127
+ props?: object;
128
+ children?: ChildrenDecl;
129
+ store?: StoreDecl;
130
+ inject?: object;
131
+ locale?: keyof LocaleNamespaceMap & string;
132
+ slots?: Record<string, FactoryLocalSlotDef>;
133
+ }
113
134
  /**
114
135
  * Runtime dispatch spec for one slot, recorded from a register call's
115
136
  * `children` value. The literal is compile-time checked against the SlotMap
@@ -170,10 +191,10 @@ export type ScopeOf<K extends keyof SlotMap & string> = SlotMap[K]['scope'];
170
191
  export interface SessionStandardProps {
171
192
  }
172
193
  /**
173
- * Framework standard kit delivered to current-session-optional slots. Its
174
- * hooks stay callable while no session is selected and return `undefined`
175
- * until one becomes current; `ui-session` and domain UI adapters merge the
176
- * concrete members.
194
+ * Framework standard kit delivered to session-optional slots. Its hooks stay
195
+ * callable while no session is selected and return `undefined` until one
196
+ * becomes current; `ui-session` and domain UI adapters merge the concrete
197
+ * members.
177
198
  */
178
199
  export interface SessionMaybeStandardProps {
179
200
  }
@@ -192,11 +213,13 @@ export interface GlobalStandardProps {
192
213
  export type SessionIdOf = SessionStandardProps extends {
193
214
  sessionId: infer S;
194
215
  } ? S : string;
216
+ /** Standard props selected by one declared scope. */
217
+ export type ScopeStandardProps<S extends SlotScope> = (S extends 'session' ? SessionStandardProps : S extends 'session-maybe' ? SessionMaybeStandardProps : object) & GlobalStandardProps;
195
218
  /**
196
219
  * Runtime props share for a slot key: owner share (parent's renderSlot call
197
220
  * site) + session standard kit (session scope only) + the global seat.
198
221
  */
199
- export type PropsRuntime<K extends keyof SlotMap & string, EntryKey extends EntryKeyOf<K> = EntryKeyOf<K>> = OwnerOf<K> & KeyPropsOf<K, EntryKey> & SlotInjectFace<SlotInjectOf<K>> & (ScopeOf<K> extends 'session' ? SessionStandardProps : ScopeOf<K> extends 'session-maybe' ? SessionMaybeStandardProps : object) & GlobalStandardProps;
222
+ export type PropsRuntime<K extends keyof SlotMap & string, EntryKey extends EntryKeyOf<K> = EntryKeyOf<K>> = OwnerOf<K> & KeyPropsOf<K, EntryKey> & SlotInjectFace<SlotInjectOf<K>> & ScopeStandardProps<ScopeOf<K>>;
200
223
  /** renderSlot dispatch options: keyed dispatch key, list filtering, and empty fallback. */
201
224
  export interface RenderOpts<EntryKey extends string = string> {
202
225
  entryKey?: EntryKey;
@@ -262,15 +285,20 @@ export type MatchedShare<E extends SlotEntryDef, M> = E['kind'] extends 'chain'
262
285
  } : object;
263
286
  /** Props of the standard-kit SessionProvider seat. */
264
287
  export interface SessionAreaProps {
288
+ /**
289
+ * Explicit Session-scope target. Omit this property to inherit the surrounding
290
+ * binding; pass `undefined` to establish an explicitly absent binding.
291
+ */
292
+ readonly session?: SlotScopeTargetMap[keyof SlotScopeTargetMap & 'session'] | undefined;
265
293
  /** No-session body (also covers a current id whose session cannot be resolved). */
266
294
  empty?: (() => ReactNode) | undefined;
267
- /** Session body; the framework remounts it per session identity. */
295
+ /** Session body; scoped entries apply their declared remount semantics. */
268
296
  children: ReactNode;
269
297
  }
270
298
  /**
271
299
  * Framework-wired session area component. `ui-session` supplies the current
272
300
  * Controller binding through the renderer scope adapter; entries that declare
273
- * session-scoped children receive this component without importing it.
301
+ * `session` or `session-maybe` children receive this component without importing it.
274
302
  */
275
303
  export type SessionProviderComponent = (props: SessionAreaProps) => ReactNode;
276
304
  /**
@@ -304,15 +332,76 @@ export type PropsRenderSlots<S extends keyof SlotMap & string> = {
304
332
  * @returns rendered node(s).
305
333
  */
306
334
  renderSlotChain: <K extends ChainKeysOf<S>>(key: K, owner: OwnerOf<K>, opts?: ChainRenderOpts) => ReactNode;
307
- }) & ('session' extends ScopeOf<S> ? {
335
+ }) & ([Extract<ScopeOf<S>, 'session' | 'session-maybe'>] extends [never] ? object : {
308
336
  SessionProvider: SessionProviderComponent;
309
- } : object);
337
+ });
310
338
  /**
311
339
  * Registration-position component shape: the bare call signature, so composed
312
340
  * constraints check through clean parameter contravariance (FC statics add
313
341
  * covariant noise rejecting legitimate narrowings).
314
342
  */
315
343
  export type SlotComponent<P> = (props: P) => ReactNode;
344
+ type FactoryDefOf<F extends keyof SlotFactoryMap & string> = SlotFactoryMap[F] & SlotFactoryDef;
345
+ type FactoryInputPropsOf<F extends keyof SlotFactoryMap & string> = SlotFactoryMap[F] extends {
346
+ props: infer P extends object;
347
+ } ? P : object;
348
+ type FactoryChildrenOf<F extends keyof SlotFactoryMap & string> = SlotFactoryMap[F] extends {
349
+ children: infer D extends Record<string, unknown>;
350
+ } ? D : Record<never, never>;
351
+ type FactoryStoreOf<F extends keyof SlotFactoryMap & string> = SlotFactoryMap[F] extends {
352
+ store: infer H extends StoreDecl;
353
+ } ? HandleOf<H> : undefined;
354
+ type FactoryInjectOf<F extends keyof SlotFactoryMap & string> = SlotFactoryMap[F] extends {
355
+ inject: infer I extends object;
356
+ } ? I : object;
357
+ type FactoryLocaleOf<F extends keyof SlotFactoryMap & string> = SlotFactoryMap[F] extends {
358
+ locale: infer N extends keyof LocaleNamespaceMap & string;
359
+ } ? N : undefined;
360
+ type FactoryLocalSlotsOf<F extends keyof SlotFactoryMap & string> = SlotFactoryMap[F] extends {
361
+ slots: infer S extends Record<string, FactoryLocalSlotDef>;
362
+ } ? S : Record<never, never>;
363
+ type FactoryLocalNameOf<F extends keyof SlotFactoryMap & string> = keyof FactoryLocalSlotsOf<F> & string;
364
+ type FactoryLocalDefOf<F extends keyof SlotFactoryMap & string, N extends FactoryLocalNameOf<F>> = FactoryLocalSlotsOf<F>[N] & FactoryLocalSlotDef;
365
+ type FactoryLocalInputPropsOf<F extends keyof SlotFactoryMap & string, N extends FactoryLocalNameOf<F>> = FactoryLocalDefOf<F, N> extends {
366
+ props: infer P extends object;
367
+ } ? P : object;
368
+ type FactoryRenderPropsOf<F extends keyof SlotFactoryMap & string> = [
369
+ keyof FactoryChildrenOf<F> & keyof SlotMap & string
370
+ ] extends [never] ? object : PropsRenderSlots<keyof FactoryChildrenOf<F> & keyof SlotMap & string>;
371
+ /** Framework-derived props shared by a Factory definition and its local Components. */
372
+ export type FactoryRegistrationPropsOf<F extends keyof SlotFactoryMap & string> = FactoryRenderPropsOf<F> & PropsStore<FactoryStoreOf<F>> & InjectFace<FactoryInjectOf<F>> & PropsLocale<FactoryLocaleOf<F>> & PropsRenderFactories;
373
+ /** Complete props received by a registered Factory Component. */
374
+ export type FactoryComponentPropsOf<F extends keyof SlotFactoryMap & string> = FactoryInputPropsOf<F> & FactoryRegistrationPropsOf<F> & ScopeStandardProps<FactoryDefOf<F>['scope']> & {
375
+ useFactorySlot: UseFactorySlot<F>;
376
+ };
377
+ /** Complete props received by one caller-selected or fallback local Component. */
378
+ export type FactoryLocalComponentPropsOf<F extends keyof SlotFactoryMap & string, N extends FactoryLocalNameOf<F>> = FactoryLocalInputPropsOf<F, N> & FactoryRegistrationPropsOf<F> & ScopeStandardProps<FactoryLocalDefOf<F, N>['scope']>;
379
+ /** Component accepted for one declared local Factory position. */
380
+ export type FactoryLocalComponent<F extends keyof SlotFactoryMap & string, N extends FactoryLocalNameOf<F>> = SlotComponent<FactoryLocalComponentPropsOf<F, N>>;
381
+ /**
382
+ * Hook exposed only to a Factory definition for selecting a local Component.
383
+ * @param name - declared local position.
384
+ * @param fallback - Component used when the occurrence caller makes no selection.
385
+ * @returns a stable Component accepting only the local occurrence props.
386
+ */
387
+ export type UseFactorySlot<F extends keyof SlotFactoryMap & string> = <N extends FactoryLocalNameOf<F>>(name: N, fallback: FactoryLocalComponent<F, N>) => SlotComponent<FactoryLocalInputPropsOf<F, N>>;
388
+ /**
389
+ * Render one independently keyed occurrence of a registered Factory.
390
+ * @param name - declaration-merged Factory name.
391
+ * @param props - caller-owned occurrence input.
392
+ * @param options - local Component selections and the missing-definition fallback.
393
+ * @returns the Factory occurrence or fallback.
394
+ */
395
+ export type RenderFactorySlot = <F extends keyof SlotFactoryMap & string>(name: F, props: FactoryInputPropsOf<F>, options?: {
396
+ slots?: Partial<{
397
+ [N in FactoryLocalNameOf<F>]: FactoryLocalComponent<F, N>;
398
+ }>;
399
+ fallback?: ReactNode;
400
+ }) => ReactNode;
401
+ /** Factory rendering capability supplied to every renderer-created Component. */
402
+ export interface PropsRenderFactories {
403
+ renderFactorySlot: RenderFactorySlot;
404
+ }
316
405
  /**
317
406
  * Registrant hooks compartment: bare observable sources (getSnapshot +
318
407
  * subscribe pairs) supplied under the reserved `hooks` key of an entry's
@@ -382,7 +471,7 @@ export type InjectFace<I extends object> = I extends {
382
471
  * {@link PropsLocale}). Each share derives from its single source of truth;
383
472
  * components reference this composition, never re-type it.
384
473
  */
385
- export type ComposedProps<K extends keyof SlotMap & string, EntryKey extends EntryKeyOf<K>, S extends keyof SlotMap & string, H, I extends object, M = never, N = undefined> = PropsRuntime<K, EntryKey> & PropsRenderSlots<S> & PropsStore<H> & InjectFace<I> & MatchedShare<SlotMap[K], M> & PropsLocale<N>;
474
+ export type ComposedProps<K extends keyof SlotMap & string, EntryKey extends EntryKeyOf<K>, S extends keyof SlotMap & string, H, I extends object, M = never, N = undefined> = PropsRuntime<K, EntryKey> & PropsRenderSlots<S> & PropsRenderFactories & PropsStore<H> & InjectFace<I> & MatchedShare<SlotMap[K], M> & PropsLocale<N>;
386
475
  /**
387
476
  * Inject factory parameter list, derived from the registration's declaration:
388
477
  * strict session slots receive a definite framework-resolved `sessionId`;
@@ -392,6 +481,72 @@ export type ComposedProps<K extends keyof SlotMap & string, EntryKey extends Ent
392
481
  * object parameter exists.
393
482
  */
394
483
  export type InjectParams<K extends keyof SlotMap & string, H> = ScopeOf<K> extends 'session' ? ([H] extends [StoreDecl] ? [sessionId: SessionIdOf, actions: BoundActions<HandleOf<H>>] : [sessionId: SessionIdOf]) : ScopeOf<K> extends 'session-maybe' ? ([H] extends [StoreDecl] ? [sessionId: SessionIdOf | undefined, actions: BoundActions<HandleOf<H>> | undefined] : [sessionId: SessionIdOf | undefined]) : ([H] extends [StoreDecl] ? [actions: BoundActions<HandleOf<H>>] : []);
484
+ /** Factory inject parameters use the same scope/store matrix as ordinary registrations. */
485
+ export type FactoryInjectParams<F extends keyof SlotFactoryMap & string> = FactoryDefOf<F>['scope'] extends 'session' ? ([FactoryStoreOf<F>] extends [StoreDecl] ? [sessionId: SessionIdOf, actions: BoundActions<FactoryStoreOf<F>>] : [sessionId: SessionIdOf]) : FactoryDefOf<F>['scope'] extends 'session-maybe' ? ([FactoryStoreOf<F>] extends [StoreDecl] ? [sessionId: SessionIdOf | undefined, actions: BoundActions<FactoryStoreOf<F>> | undefined] : [sessionId: SessionIdOf | undefined]) : ([FactoryStoreOf<F>] extends [StoreDecl] ? [actions: BoundActions<FactoryStoreOf<F>>] : []);
486
+ type FactoryField<F extends keyof SlotFactoryMap & string, K extends keyof SlotFactoryDef, Value> = K extends keyof SlotFactoryMap[F] ? {
487
+ [P in K]-?: Value;
488
+ } : {
489
+ [P in K]?: never;
490
+ };
491
+ type RuntimeFactorySlots<F extends keyof SlotFactoryMap & string> = {
492
+ [N in FactoryLocalNameOf<F>]: {
493
+ scope: FactoryLocalDefOf<F, N>['scope'];
494
+ };
495
+ };
496
+ type FactoryInjectCollisionKeys<F extends keyof SlotFactoryMap & string> = Extract<keyof InjectFace<FactoryInjectOf<F>>, keyof (FactoryRenderPropsOf<F> & PropsStore<FactoryStoreOf<F>> & PropsLocale<FactoryLocaleOf<F>> & PropsRenderFactories & ScopeStandardProps<FactoryDefOf<F>['scope']> & {
497
+ useFactorySlot: UseFactorySlot<F>;
498
+ })>;
499
+ type FactoryInputCollisionKeys<F extends keyof SlotFactoryMap & string> = Extract<keyof FactoryInputPropsOf<F>, keyof (FactoryRegistrationPropsOf<F> & ScopeStandardProps<FactoryDefOf<F>['scope']> & {
500
+ useFactorySlot: UseFactorySlot<F>;
501
+ })>;
502
+ type FactoryLocalCollisionKeys<F extends keyof SlotFactoryMap & string> = {
503
+ [N in FactoryLocalNameOf<F>]: Extract<keyof FactoryLocalInputPropsOf<F, N>, keyof (FactoryRegistrationPropsOf<F> & ScopeStandardProps<FactoryLocalDefOf<F, N>['scope']>)>;
504
+ }[FactoryLocalNameOf<F>];
505
+ type FactoryRegistrationLocalScopeCollisionKeys<F extends keyof SlotFactoryMap & string> = {
506
+ [N in FactoryLocalNameOf<F>]: Extract<keyof FactoryRegistrationPropsOf<F>, keyof ScopeStandardProps<FactoryLocalDefOf<F, N>['scope']>>;
507
+ }[FactoryLocalNameOf<F>];
508
+ type FactoryCollisionCheck<F extends keyof SlotFactoryMap & string> = [
509
+ FactoryInjectCollisionKeys<F> | FactoryInputCollisionKeys<F> | FactoryLocalCollisionKeys<F> | FactoryRegistrationLocalScopeCollisionKeys<F>
510
+ ] extends [never] ? unknown : {
511
+ 'Factory declaration has overlapping prop ownership': FactoryInjectCollisionKeys<F> | FactoryInputCollisionKeys<F> | FactoryLocalCollisionKeys<F> | FactoryRegistrationLocalScopeCollisionKeys<F>;
512
+ };
513
+ type FactoryChildMismatchKeys<F extends keyof SlotFactoryMap & string> = {
514
+ [K in keyof FactoryChildrenOf<F>]: K extends keyof SlotMap ? FactoryChildrenOf<F>[K] extends SlotSpec<SlotMap[K]> ? never : K : K;
515
+ }[keyof FactoryChildrenOf<F>];
516
+ type FactoryChildrenCheck<F extends keyof SlotFactoryMap & string> = [
517
+ FactoryChildMismatchKeys<F>
518
+ ] extends [never] ? unknown : {
519
+ 'Factory children must match SlotMap': FactoryChildMismatchKeys<F>;
520
+ };
521
+ /** Registration options checked against the complete declaration-merged Factory definition. */
522
+ export type RegisterFactoryOptions<F extends keyof SlotFactoryMap & string> = {
523
+ name: F;
524
+ scope: FactoryDefOf<F>['scope'];
525
+ } & FactoryField<F, 'children', FactoryChildrenOf<F>> & FactoryField<F, 'store', FactoryStoreOf<F> | (() => FactoryStoreOf<F>)> & FactoryField<F, 'inject', (...args: FactoryInjectParams<F>) => FactoryInjectOf<F>> & FactoryField<F, 'locale', FactoryLocaleOf<F>> & FactoryField<F, 'slots', RuntimeFactorySlots<F>> & FactoryCollisionCheck<F> & FactoryChildrenCheck<F>;
526
+ /**
527
+ * Typed registration method implemented by the renderer-owned SlotRegistry service.
528
+ * @param options - runtime values checked against the Factory declaration.
529
+ * @param component - reusable definition Component.
530
+ * @returns an idempotent definition disposer.
531
+ */
532
+ export interface RegisterFactory {
533
+ <F extends keyof SlotFactoryMap & string>(options: RegisterFactoryOptions<F>, component: SlotComponent<FactoryComponentPropsOf<F>>): () => void;
534
+ }
535
+ /** Type-erased Factory definition stored by the runtime registry. */
536
+ export interface StoredFactory {
537
+ readonly name: string;
538
+ readonly component: unknown;
539
+ readonly scope: SlotScope;
540
+ readonly children?: Readonly<Record<string, SlotSpec<SlotEntryDef>>> | undefined;
541
+ readonly store?: StoreDecl | undefined;
542
+ readonly inject?: ((...args: never[]) => Record<string, unknown>) | undefined;
543
+ readonly locale?: string | undefined;
544
+ readonly slots?: Readonly<Record<string, {
545
+ scope: SlotScope;
546
+ }>> | undefined;
547
+ /** Diagnostics label of who registered the definition. */
548
+ readonly registrant?: string | undefined;
549
+ }
395
550
  /**
396
551
  * A list-entry display label: a plain string, or a thunk re-evaluated per
397
552
  * read so registration-time text (nav rows, tabs) follows the active locale
@@ -506,8 +661,10 @@ export interface LiveSlotOccupant {
506
661
  /** Whether the renderer currently selects this entry. */
507
662
  active: boolean;
508
663
  }
509
- /** JSON-safe live slot declaration tree. */
664
+ /** JSON-safe live Slot declaration tree. */
510
665
  export interface LiveSlotNode {
666
+ /** Discriminant for an ordinary Slot declaration. */
667
+ type: 'slot';
511
668
  /** Exact SlotMap key. */
512
669
  name: string;
513
670
  /** Slot cardinality. */
@@ -521,6 +678,21 @@ export interface LiveSlotNode {
521
678
  /** Slots declared by entries mounted in this slot. */
522
679
  children: LiveSlotNode[];
523
680
  }
681
+ /** JSON-safe live Factory definition with its ordinary child Slot tree. */
682
+ export interface LiveFactoryNode {
683
+ /** Discriminant for a reusable Factory definition. */
684
+ type: 'factory';
685
+ /** Exact SlotFactoryMap key. */
686
+ name: string;
687
+ /** Runtime data scope inherited by each occurrence. */
688
+ scope: SlotScope;
689
+ /** Plugin or package that registered the definition, when known. */
690
+ registrant?: string;
691
+ /** Ordinary Slots declared by the Factory definition. */
692
+ children: LiveSlotNode[];
693
+ }
694
+ /** One root in the live Slot/Factory composition topology. */
695
+ export type LiveCompositionNode = LiveSlotNode | LiveFactoryNode;
524
696
  /**
525
697
  * Pure slot registry (no cordis; event emission and the renderer installation contract
526
698
  * live in the runtime Service wrapper).
@@ -539,6 +711,7 @@ export interface LiveSlotNode {
539
711
  */
540
712
  export declare class SlotCore {
541
713
  private records;
714
+ private factories;
542
715
  private mutateListeners;
543
716
  /** Shared-handle scope ledger: handle → the scope it first mounted under + live mount count. */
544
717
  private handleScopes;
@@ -554,6 +727,33 @@ export declare class SlotCore {
554
727
  private abdicated;
555
728
  private entryErrorListeners;
556
729
  constructor();
730
+ /** Register one reusable Factory definition. */
731
+ readonly registerFactory: RegisterFactory;
732
+ /**
733
+ * Read one registered Factory definition.
734
+ * @param name - Factory name.
735
+ * @returns the live definition, or `undefined` when absent.
736
+ */
737
+ factory(name: string): StoredFactory | undefined;
738
+ /**
739
+ * Read the monotonic definition version for one Factory name.
740
+ * @param name - Factory name.
741
+ * @returns the current version.
742
+ */
743
+ factoryVersion(name: string): number;
744
+ /**
745
+ * Subscribe to one Factory definition's registration lifetime.
746
+ * @param name - Factory name.
747
+ * @param listener - callback notified after a definition change.
748
+ * @returns the unsubscribe function.
749
+ */
750
+ subscribeFactory(name: string, listener: () => void): () => void;
751
+ /**
752
+ * Return whether a retained Factory definition is still registered.
753
+ * @param definition - retained definition identity.
754
+ * @returns whether that exact definition remains live.
755
+ */
756
+ isFactoryLive(definition: StoredFactory): boolean;
557
757
  /**
558
758
  * Contribute a component to a declared slot and (optionally) declare child
559
759
  * slots, a store seat, and the registrant's business face.
@@ -581,7 +781,7 @@ export declare class SlotCore {
581
781
  * @param options - registration options: target `name`, `children`
582
782
  * declaration table, `store` seat, `inject` business-face factory, kind
583
783
  * shape fields (keyed `key`; list `id`/`order`/`label`).
584
- * @param component - component honoring the four-share composed props
784
+ * @param component - component honoring the five-share composed props
585
785
  * contract ({@link ComposedProps}); checked at this call site.
586
786
  * @returns disposer removing the registration and its declarations
587
787
  * (idempotent; stale disposers after a cascade are no-ops).
@@ -595,7 +795,7 @@ export declare class SlotCore {
595
795
  * factory's return and joins the component's composed-props constraint
596
796
  * (factory parameters derive from the declaration, {@link InjectParams}).
597
797
  * @param options - registration options plus the `inject` business-face factory.
598
- * @param component - component honoring the four-share composed props
798
+ * @param component - component honoring the five-share composed props
599
799
  * contract including the inject share `I`.
600
800
  * @returns disposer removing the registration and its declarations.
601
801
  */
@@ -648,10 +848,12 @@ export declare class SlotCore {
648
848
  specDynamic(key: string): SlotSpec<SlotEntryDef> | undefined;
649
849
  /**
650
850
  * Export the current declaration topology without components or executable hooks.
651
- * @param root - exact Slot key to select; omitted returns every live root.
851
+ * Factory definitions appear as `factory:<name>` parents of their ordinary
852
+ * child Slots, matching the parent/child topology of ordinary registrations.
853
+ * @param root - exact Slot or `factory:<name>` key to select; omitted returns every live root.
652
854
  * @returns selected live Slot trees, or an empty array when `root` is unavailable.
653
855
  */
654
- snapshot(root?: string): LiveSlotNode[];
856
+ snapshot(root?: string): LiveCompositionNode[];
655
857
  /**
656
858
  * Read the declaration lifetime of a key. Entry additions and removals do
657
859
  * not change it; declaration creation and collapse each advance it.
@@ -713,16 +915,22 @@ export declare class SlotCore {
713
915
  abdicate: boolean;
714
916
  }): void;
715
917
  /**
716
- * Observe entry boundary crashes (every render-time entry failure the
717
- * boundaries contain, abdicating or not) — the supervision seam for hosts
718
- * mirroring contribution health. Fires synchronously per report, after the
719
- * registry mutated for abdicating crashes (same listener discipline as
720
- * {@link SlotCore.onMutate}).
721
- * @param fn - called with the slot key, the crashed entry, the crash
722
- * cause, and `abdicated`: whether the crash retired the entry from its cell.
918
+ * Report a contained Factory occurrence crash through the ordinary entry
919
+ * supervision channel without retiring the shared definition.
920
+ * @param name - Factory name whose occurrence crashed.
921
+ * @param registration - Factory definition or caller registration that owns the crashing Component.
922
+ * @param error - the crash cause, forwarded to listeners verbatim.
923
+ */
924
+ reportFactoryError(name: string, registration: StoredEntry | StoredFactory, error: unknown): void;
925
+ /**
926
+ * Observe ordinary entry and Factory occurrence crashes. Fires synchronously
927
+ * per report, after any ordinary-entry abdication mutation. Factory failures
928
+ * never retire their shared definition.
929
+ * @param fn - called with the Slot or `factory:<name>` key, the crashed
930
+ * registration, the cause, and whether an ordinary entry was retired.
723
931
  * @returns unsubscribe.
724
932
  */
725
- onEntryError(fn: (key: string, entry: StoredEntry, error: unknown, info: {
933
+ onEntryError(fn: (key: string, registration: StoredEntry | StoredFactory, error: unknown, info: {
726
934
  abdicated: boolean;
727
935
  }) => void): () => void;
728
936
  /**
@@ -732,7 +940,10 @@ export declare class SlotCore {
732
940
  * axis: ledger rows, slots, contributions, and store mounts die together.
733
941
  */
734
942
  private releaseEntry;
943
+ private releaseChildren;
735
944
  private record;
945
+ private factoryRecord;
946
+ private markFactoryDirty;
736
947
  private markDirty;
737
948
  private notifyDeclaration;
738
949
  private flush;
@@ -2,7 +2,7 @@
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
3
  import type { ReactNode } from 'react';
4
4
  import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
5
- import type { SessionAreaProps, SlotEntryDef, SlotScope, SlotSpec, StoredEntry, Translate } from './index.ts';
5
+ import type { SessionAreaProps, SlotEntryDef, SlotScope, SlotSpec, StoredEntry, StoredFactory, Translate } from './index.ts';
6
6
  /**
7
7
  * The locale face the render machinery consumes: namespace binding plus an
8
8
  * observable revision (getSnapshot/subscribe pair — the same HostObservable
@@ -78,14 +78,14 @@ export interface ScopedStandardSourceBinding extends StandardSourceBinding {
78
78
  }
79
79
  /** One installed source of bindings for a non-root Slot scope. */
80
80
  export interface SlotScopeAdapter {
81
- /** Binding that follows the current selection, including its absent projection. */
81
+ /** Default binding inherited by scoped entries, including its absent projection. */
82
82
  readonly current: HostObservable<StandardSourceBinding>;
83
83
  /**
84
- * Resolve an already-materialized binding.
85
- * @param key - scope identity.
86
- * @returns the binding, or `undefined` when the identity is unavailable.
84
+ * Resolve a stable observable for an explicit scope target or explicit absence.
85
+ * @param target - domain-owned Provider target, or absence.
86
+ * @returns the target's current standard-source binding.
87
87
  */
88
- resolve(key: string): ScopedStandardSourceBinding | undefined;
88
+ bindingSource(target: SessionAreaProps['session']): HostObservable<StandardSourceBinding>;
89
89
  /**
90
90
  * Render the scope owner's area seat over the current binding. The renderer
91
91
  * binds this function to the standard `SessionProvider` prop without owning
@@ -154,6 +154,13 @@ export interface SlotRendererHost {
154
154
  reportEntryError(key: string, entry: StoredEntry, error: unknown, info: {
155
155
  abdicate: boolean;
156
156
  }): void;
157
+ /**
158
+ * Report a contained Factory occurrence crash without retiring its shared definition.
159
+ * @param name - Factory name whose occurrence crashed.
160
+ * @param registration - Factory definition or caller registration that owns the crashing Component.
161
+ * @param error - the crash cause.
162
+ */
163
+ reportFactoryError(name: string, registration: StoredEntry | StoredFactory, error: unknown): void;
157
164
  /**
158
165
  * Declared runtime spec from the declarations ledger.
159
166
  * @param key - slot key.
@@ -174,6 +181,47 @@ export interface SlotRendererHost {
174
181
  * @returns the instance, or undefined when the entry declares no store.
175
182
  */
176
183
  storeOf(entry: StoredEntry, scopeBinding: ScopedStandardSourceBinding | undefined): StoreInstanceLike | undefined;
184
+ /**
185
+ * Resolve a Factory Store for one render occurrence and inherited scope.
186
+ * @param definition - live Factory definition.
187
+ * @param scopeBinding - exact Session binding for scoped Factories, undefined for root scope.
188
+ * @param occurrence - identity token owned by the render position.
189
+ * @returns the occurrence Store instance, or undefined without a Store declaration.
190
+ */
191
+ factoryStoreOf(definition: StoredFactory, scopeBinding: ScopedStandardSourceBinding | undefined, occurrence: object): StoreInstanceLike | undefined;
192
+ /**
193
+ * Retain an exclusive Factory Store occurrence after React commits it.
194
+ * Repeated setup and cleanup preserve the occurrence's Store identity.
195
+ * @param definition - the live Factory definition.
196
+ * @param occurrence - identity token owned by the mounted render position.
197
+ * @returns an idempotent cleanup function.
198
+ */
199
+ retainFactoryOccurrence(definition: StoredFactory, occurrence: object): () => void;
200
+ /**
201
+ * Subscribe to one Factory definition's registration lifetime.
202
+ * @param name - Factory name.
203
+ * @param fn - change callback.
204
+ * @returns the unsubscribe function.
205
+ */
206
+ subscribeFactory(name: string, fn: () => void): () => void;
207
+ /**
208
+ * Read the monotonic version for one Factory definition.
209
+ * @param name - Factory name.
210
+ * @returns the current definition version.
211
+ */
212
+ getFactoryVersion(name: string): number;
213
+ /**
214
+ * Read one live Factory definition.
215
+ * @param name - Factory name.
216
+ * @returns the definition, or undefined while unregistered.
217
+ */
218
+ factoryOf(name: string): StoredFactory | undefined;
219
+ /**
220
+ * Check retained Factory render authority.
221
+ * @param definition - a previously resolved Factory definition.
222
+ * @returns whether that exact definition remains live.
223
+ */
224
+ isFactoryLive(definition: StoredFactory): boolean;
177
225
  /** Root standard data assembled from domain-owned contributions. */
178
226
  readonly root: HostObservable<StandardSourceBinding>;
179
227
  /** Monotonic source updated whenever the installed scope-adapter roster changes. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-slots",
3
- "description": "Slot registry pure core: SlotMap declaration merging, single register composition API, four-share props types, store-seat types, renderer install seam",
4
- "version": "0.1.6-alpha.1",
3
+ "description": "Slot registry pure core: typed ordinary Slots and reusable Component Factories, derived props, Store seats, and renderer installation",
4
+ "version": "0.1.6-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -25,7 +25,7 @@
25
25
  "devDependencies": {
26
26
  "@types/react": "~18.3.1",
27
27
  "@deepseek-ai/cordis": "^4.0.2",
28
- "@deepseek-ai/dsh-client-store": "^0.1.6-alpha.1"
28
+ "@deepseek-ai/dsh-client-store": "^0.1.6-alpha.2"
29
29
  },
30
30
  "files": [
31
31
  "lib/index.js",