@x1a0f3n9/dsh-client-ui-slots 0.1.5-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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +104 -0
- package/README.zh.md +104 -0
- package/lib/index.js +433 -0
- package/lib/types/index.d.ts +740 -0
- package/lib/types/renderer.d.ts +215 -0
- package/lib/types/store.d.ts +3 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
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:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/client/ui-slots/README.md
|
|
5
|
+
README.md: d38fe059693f7b15d060b0a52c97edd0b5e050c9
|
|
6
|
+
README.zh.md: 968470edd20d75b7c6bf8f7c8fca898764af757e
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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."
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @x1a0f3n9/dsh-client-ui-slots
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
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.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
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
|
+
|
|
30
|
+
### The four props shares
|
|
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.
|
|
33
|
+
|
|
34
|
+
### Store seats
|
|
35
|
+
|
|
36
|
+
A register call may declare a store seat with `store: defineStore(...)`: `init` infers the state schema and `actions` is the complete draft-transform write set. Components read through the selector hook and write through the baked callbacks; the engine implementation of `defineStore` lives in the runtime package and satisfies the `DefineStore` contract exported here.
|
|
37
|
+
|
|
38
|
+
### Declaration discipline
|
|
39
|
+
|
|
40
|
+
Declaring a slot is claiming it: the registering entry becomes the only entry allowed to render that key, and registering into an undeclared slot, declaring an already-declared child, mounting one shared handle under two scopes, or registering a chain without `select` throws at load. An entry's disposer collapses its declared child slots recursively — ledger rows, contributions, and store mounts die on one lifecycle axis.
|
|
41
|
+
|
|
42
|
+
-----
|
|
43
|
+
|
|
44
|
+
<a id="understand-the-implementation"></a>
|
|
45
|
+
## Understand the implementation
|
|
46
|
+
|
|
47
|
+
<details>
|
|
48
|
+
<summary>Implementation internals — click to expand</summary>
|
|
49
|
+
|
|
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.
|
|
51
|
+
|
|
52
|
+
### Registration and routing
|
|
53
|
+
|
|
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.
|
|
55
|
+
|
|
56
|
+
### The renderer contract
|
|
57
|
+
|
|
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.
|
|
59
|
+
|
|
60
|
+
</details>
|
|
61
|
+
|
|
62
|
+
-----
|
|
63
|
+
|
|
64
|
+
<a id="further-exploration"></a>
|
|
65
|
+
## Further Exploration
|
|
66
|
+
|
|
67
|
+
These pages cover the engine, the renderer, and the composition model.
|
|
68
|
+
|
|
69
|
+
- [ui-renderer](../ui-renderer/README.md) — the React slot renderer implementing this package's install contract.
|
|
70
|
+
- [Slot system standard](../../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md) — the definitive composition model.
|
|
71
|
+
- [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
|
+
|
|
73
|
+
-----
|
|
74
|
+
|
|
75
|
+
<a id="model-experience"></a>
|
|
76
|
+
## Model Experience
|
|
77
|
+
|
|
78
|
+
None, as the package is a browser-side UI plugin layer that registers nothing model-facing.
|
|
79
|
+
|
|
80
|
+
#### KV Cache effect
|
|
81
|
+
|
|
82
|
+
None; this package neither assembles nor sends a provider request.
|
|
83
|
+
|
|
84
|
+
## Known Limitations and Deferred Work
|
|
85
|
+
|
|
86
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
These limits define the registry's scaling behavior and accepted type noise; they are current package constraints.
|
|
90
|
+
|
|
91
|
+
- **`isLive` scans all records linearly** — fine at UI-plugin registration counts (tens); revisit with an entry→record backref if ledgers ever grow hot.
|
|
92
|
+
- **The `__renders` phantom anchor is visible on `PropsRenderSlots`** — the same accepted noise as the type-chain design's `__accepts`: generic method signatures compare loosely across key unions, so the contravariant marker is what enforces "component key set ⊆ children declaration".
|
|
93
|
+
|
|
94
|
+
<a id="dev-note"></a>
|
|
95
|
+
### Dev Note
|
|
96
|
+
|
|
97
|
+
<details>
|
|
98
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
99
|
+
|
|
100
|
+
None.
|
|
101
|
+
|
|
102
|
+
</details>
|
|
103
|
+
|
|
104
|
+
**Runtime invariant:** No companion is published. This is a zero-dependency pure registry core; it emits no Cordis events itself (the `ui-renderer` SlotRegistry owns the event bridge and its invariants); define/register/dispose sequencing is asserted directly by this package's behavior specs.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "dsh Web 客户端的 slot 注册表纯核心:SlotMap 声明合并、单一 register 组合 API、四 share props 类型、store 席位与渲染器安装约定。"
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @x1a0f3n9/dsh-client-ui-slots
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`dsh-client-ui-slots` 让 Web 客户端插件定义并组合带类型检查的 UI 区域。调用方可以通过一个在编译期检查的 API 添加组件、声明嵌套区域、附加作用域状态并提供业务 props。它支持单项、有序列表、键控和自行选择的 chain 组合,并会在插件加载期间报告冲突组合。需要与框架无关的 slot 组合时选择本包;客户端需要 React 渲染时与 `ui-renderer` 配合使用。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
编写客户端插件时都通过本包组合 UI:把组件注册进父级已声明的 slot,或声明组件将要渲染的子 slot。四种 kind 覆盖组合形态——`single`(单个占位者)、`list`(有序条目)、`keyed`(按键分派)与 `chain`(条目自行提名)。
|
|
29
|
+
|
|
30
|
+
### 四个 props share
|
|
31
|
+
|
|
32
|
+
每个已注册组件都会收到由四个 share 组合而成的 props:运行时 share(父级 renderSlot 调用点的 `owner`,加上会话标准工具包与全局席位)、child render share(静态缩窄到已声明 children key 的 `renderSlot`)、store share(已声明句柄的 selector 钩子与移除 draft 的 actions),以及业务 share(从 `inject` factory 返回值推断)。组件引用 `ComposedProps`;它们绝不在本地重新定义任何 share 的类型。
|
|
33
|
+
|
|
34
|
+
### Store 席位
|
|
35
|
+
|
|
36
|
+
register 调用可以用 `store: defineStore(...)` 声明 store 席位:`init` 推断状态 schema,`actions` 是完整的 draft-transform 写入集合。组件经 selector 钩子读取、经烘焙回调写入;`defineStore` 的引擎实现位于运行时包,并满足这里导出的 `DefineStore` 约定。
|
|
37
|
+
|
|
38
|
+
### 声明纪律
|
|
39
|
+
|
|
40
|
+
声明即认领:注册条目成为唯一被允许渲染该键的条目;注册未声明 slot、声明已声明过的子项、在两个 scope 下挂载同一个共享句柄、或注册缺少 `select` 的 chain,都会在加载时抛出。条目的 disposer 会递归移除其声明的子 slot——账本行、贡献与 store 挂载都随同一生命周期结束而移除。
|
|
41
|
+
|
|
42
|
+
-----
|
|
43
|
+
|
|
44
|
+
<a id="understand-the-implementation"></a>
|
|
45
|
+
## 理解实现
|
|
46
|
+
|
|
47
|
+
<details>
|
|
48
|
+
<summary>实现细节——点击展开</summary>
|
|
49
|
+
|
|
50
|
+
设计就是一张表:声明 = 渲染授权 = 运行时规范。`SlotMap` 在这里声明为空,由消费方通过 `declare module` 增补合并,标准工具包接口(`SessionStandardProps`、`GlobalStandardProps`)也是如此,由运行时包以真实成员合并。
|
|
51
|
+
|
|
52
|
+
### 注册与路由
|
|
53
|
+
|
|
54
|
+
`SlotCore` 在构造时预置 `'root'` slot,并强制执行加载时验证。`ChainSelect` selector 按升序 `priority` 运行(相同值按注册顺序);第一个非 null 返回值选中其条目,并成为组件的 `matched` prop;全部返回 null 时使用 owner 的 `renderSlotChain` fallback(`ChainRenderOpts`)。每个 key 都携带一个 declaration epoch,它只在声明与移除时递增;`ui-renderer` 将其用于 `ctx.slots.inject`,且与普通条目版本相互独立。
|
|
55
|
+
|
|
56
|
+
### 渲染器约定
|
|
57
|
+
|
|
58
|
+
`renderer.ts` 携带安装约定(`SlotRenderer`、`SlotRendererHost`)以及 `StaleAuthorizationError`/`SlotOwnershipError`;ui-renderer 负责实现,并在其插件生命周期中完成安装。引擎产物与渲染器宿主约定携带裸快照 source(`getSnapshot`/`subscribe`),绝不携带 React 钩子——钩子绑定属于渲染机制。
|
|
59
|
+
|
|
60
|
+
</details>
|
|
61
|
+
|
|
62
|
+
-----
|
|
63
|
+
|
|
64
|
+
<a id="further-exploration"></a>
|
|
65
|
+
## 进一步探索
|
|
66
|
+
|
|
67
|
+
以下页面覆盖引擎、渲染器与组合模型。
|
|
68
|
+
|
|
69
|
+
- [ui-renderer](../ui-renderer/README.zh.md)——实现本包安装约定的 React slot 渲染器。
|
|
70
|
+
- [slot 系统标准](../../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md)——权威组合模型。
|
|
71
|
+
- [Web 客户端架构](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md)——本注册表接入的加载链与对象层。
|
|
72
|
+
|
|
73
|
+
-----
|
|
74
|
+
|
|
75
|
+
<a id="model-experience"></a>
|
|
76
|
+
## 模型体验
|
|
77
|
+
|
|
78
|
+
无。该包是浏览器端 UI 接线层,不注册任何面向模型的内容。
|
|
79
|
+
|
|
80
|
+
#### KV Cache 影响
|
|
81
|
+
|
|
82
|
+
无;该包既不组装也不发送提供方请求。
|
|
83
|
+
|
|
84
|
+
## 已知限制与延期工作
|
|
85
|
+
|
|
86
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
这些限制定义注册表的规模扩展特性与已接受的类型噪声;它们是当前包约束。
|
|
90
|
+
|
|
91
|
+
- **`isLive` 会线性扫描所有记录**:在 UI 插件的注册规模(数十项)下没有问题;如果账本变得频繁访问,再使用条目→记录反向引用改进。
|
|
92
|
+
- **`__renders` 幻象锚点在 `PropsRenderSlots` 上可见**:这是与类型链设计的 `__accepts` 相同且已接受的噪声;泛型方法签名在 key 联合之间比较宽松,因此必须依靠逆变标记强制执行「组件 key 集合 ⊆ children 声明」。
|
|
93
|
+
|
|
94
|
+
<a id="dev-note"></a>
|
|
95
|
+
### 开发备注
|
|
96
|
+
|
|
97
|
+
<details>
|
|
98
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
99
|
+
|
|
100
|
+
无。
|
|
101
|
+
|
|
102
|
+
</details>
|
|
103
|
+
|
|
104
|
+
**运行时不变式:** 不发布伴生入口。这是零依赖的纯注册表核心,本身不发出 Cordis 事件;`ui-renderer` SlotRegistry 负责事件桥及其不变式。本包的行为规范直接断言 define/register/dispose 的执行顺序。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
//#region lib/types/renderer.js
|
|
2
|
+
/**
|
|
3
|
+
* Convert one standard source name to its rendered Hook prop name.
|
|
4
|
+
* @param name - registered fixed or keyed source name.
|
|
5
|
+
* @returns the `use<Name>` prop exposed to Slot components.
|
|
6
|
+
*/
|
|
7
|
+
function standardHookPropName(name) {
|
|
8
|
+
return `use${name[0]?.toUpperCase() ?? ""}${name.slice(1)}`;
|
|
9
|
+
}
|
|
10
|
+
/** Thrown when a retained renderSlot binding is invoked after its declaring entry was disposed. */
|
|
11
|
+
var StaleAuthorizationError = class extends Error {};
|
|
12
|
+
/**
|
|
13
|
+
* Thrown when a renderSlot binding is invoked for a key outside its entry's
|
|
14
|
+
* children declaration (plain-JS backstop; typed callers are narrowed
|
|
15
|
+
* statically).
|
|
16
|
+
*/
|
|
17
|
+
var SlotOwnershipError = class extends Error {};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region lib/types/index.js
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a possibly-thunked list label at read time (thunks follow the
|
|
22
|
+
* active locale; owners projecting ledger rows call this instead of reading
|
|
23
|
+
* `options.label` raw).
|
|
24
|
+
* @param label - the stored label.
|
|
25
|
+
* @returns the display string, or undefined when the entry declared none.
|
|
26
|
+
*/
|
|
27
|
+
function resolveSlotLabel(label) {
|
|
28
|
+
return typeof label === "function" ? label() : label;
|
|
29
|
+
}
|
|
30
|
+
const NO_ENTRIES = Object.freeze([]);
|
|
31
|
+
/**
|
|
32
|
+
* Pure slot registry (no cordis; event emission and the renderer installation contract
|
|
33
|
+
* live in the runtime Service wrapper).
|
|
34
|
+
*
|
|
35
|
+
* The 'root' slot is the one a-priori declaration, seeded at construction
|
|
36
|
+
* (single/root, declared by the framework) — the render tree's root hole.
|
|
37
|
+
*
|
|
38
|
+
* Change propagation contract: versions bump and {@link SlotCore.onMutate}
|
|
39
|
+
* fires synchronously per mutation (registry state is consistent when they
|
|
40
|
+
* fire); {@link SlotCore.subscribeDeclaration} fires synchronously for each
|
|
41
|
+
* declaration lifetime boundary; {@link SlotCore.subscribe} notifications
|
|
42
|
+
* batch per microtask, so N same-tick mutations produce one notification per
|
|
43
|
+
* touched key. Entry crash reports ({@link SlotCore.reportEntryError}) ride
|
|
44
|
+
* the same mutation channel when they abdicate, then notify
|
|
45
|
+
* {@link SlotCore.onEntryError} synchronously.
|
|
46
|
+
*/
|
|
47
|
+
var SlotCore = class {
|
|
48
|
+
records = /* @__PURE__ */ new Map();
|
|
49
|
+
mutateListeners = /* @__PURE__ */ new Set();
|
|
50
|
+
/** Shared-handle scope ledger: handle → the scope it first mounted under + live mount count. */
|
|
51
|
+
handleScopes = /* @__PURE__ */ new Map();
|
|
52
|
+
dirty = /* @__PURE__ */ new Set();
|
|
53
|
+
flushScheduled = false;
|
|
54
|
+
/**
|
|
55
|
+
* Entries retired by an abdicating crash report
|
|
56
|
+
* ({@link SlotCore.reportEntryError}): excluded from
|
|
57
|
+
* {@link SlotCore.entriesOfSlot} projections for the rest of their
|
|
58
|
+
* registration's life, while the registration itself stays on the ledger
|
|
59
|
+
* (disposal authority remains with the registrant).
|
|
60
|
+
*/
|
|
61
|
+
abdicated = /* @__PURE__ */ new WeakSet();
|
|
62
|
+
entryErrorListeners = /* @__PURE__ */ new Set();
|
|
63
|
+
constructor() {
|
|
64
|
+
const root = this.record("root");
|
|
65
|
+
root.spec = {
|
|
66
|
+
kind: "single",
|
|
67
|
+
scope: "root"
|
|
68
|
+
};
|
|
69
|
+
root.declaredBy = "(built-in)";
|
|
70
|
+
root.declarationEpoch = 1;
|
|
71
|
+
}
|
|
72
|
+
register(options, component) {
|
|
73
|
+
const rec = this.records.get(options.name);
|
|
74
|
+
if (!rec?.spec) throw new Error(`slot "${options.name}" is not declared (a parent entry's children table must declare it)`);
|
|
75
|
+
const spec = rec.spec;
|
|
76
|
+
const priority = options.priority ?? 0;
|
|
77
|
+
const occupantHint = (occupant) => `at priority ${priority}${occupant.registrant !== void 0 ? ` (registered by ${occupant.registrant})` : ""} — register at a different priority to shadow it (lowest renders)`;
|
|
78
|
+
switch (spec.kind) {
|
|
79
|
+
case "single": {
|
|
80
|
+
const occupant = rec.entries.find((e) => (e.options.priority ?? 0) === priority);
|
|
81
|
+
if (occupant) throw new Error(`single slot "${options.name}" already has a registration ${occupantHint(occupant)}`);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "keyed": {
|
|
85
|
+
if (options.key === void 0) throw new Error(`keyed slot "${options.name}" requires options.key`);
|
|
86
|
+
const occupant = rec.entries.find((e) => e.options.key === options.key && (e.options.priority ?? 0) === priority);
|
|
87
|
+
if (occupant) throw new Error(`keyed slot "${options.name}" already has an entry for key "${options.key}" ${occupantHint(occupant)}`);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "list": {
|
|
91
|
+
if (options.id === void 0) throw new Error(`list slot "${options.name}" requires options.id`);
|
|
92
|
+
const occupant = rec.entries.find((e) => e.options.id === options.id && (e.options.priority ?? 0) === priority);
|
|
93
|
+
if (occupant) throw new Error(`list slot "${options.name}" already has an entry with id "${options.id}" ${occupantHint(occupant)}`);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "chain":
|
|
97
|
+
if (options.select === void 0) throw new Error(`chain slot "${options.name}" requires options.select`);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (options.children) for (const childKey of Object.keys(options.children)) {
|
|
101
|
+
const childRec = this.records.get(childKey);
|
|
102
|
+
if (childRec?.spec) throw new Error(`slot "${childKey}" is already declared (by ${childRec.declaredBy ?? "an unknown entry"})`);
|
|
103
|
+
}
|
|
104
|
+
if (options.store !== void 0 && typeof options.store !== "function") {
|
|
105
|
+
const pinned = this.handleScopes.get(options.store);
|
|
106
|
+
if (pinned && pinned.scope !== spec.scope) throw new Error(`store handle mounted under "${options.name}" (scope "${spec.scope}") is already mounted under scope "${pinned.scope}" — one handle, one scope`);
|
|
107
|
+
if (pinned) pinned.count += 1;
|
|
108
|
+
else this.handleScopes.set(options.store, {
|
|
109
|
+
scope: spec.scope,
|
|
110
|
+
count: 1
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const entry = {
|
|
114
|
+
component,
|
|
115
|
+
options: {
|
|
116
|
+
...options.key !== void 0 ? { key: options.key } : {},
|
|
117
|
+
...options.id !== void 0 ? { id: options.id } : {},
|
|
118
|
+
...options.order !== void 0 ? { order: options.order } : {},
|
|
119
|
+
...options.label !== void 0 ? { label: options.label } : {},
|
|
120
|
+
...options.priority !== void 0 ? { priority: options.priority } : {}
|
|
121
|
+
},
|
|
122
|
+
...options.select !== void 0 ? { select: options.select } : {},
|
|
123
|
+
...options.inject !== void 0 ? { inject: options.inject } : {},
|
|
124
|
+
...options.children !== void 0 ? { children: options.children } : {},
|
|
125
|
+
...options.store !== void 0 ? { store: options.store } : {},
|
|
126
|
+
...options.locale !== void 0 ? { locale: options.locale } : {},
|
|
127
|
+
...options.registrant !== void 0 ? { registrant: options.registrant } : {}
|
|
128
|
+
};
|
|
129
|
+
const next = [...rec.entries, entry];
|
|
130
|
+
next.sort(spec.kind === "list" ? (a, b) => (a.options.priority ?? 0) - (b.options.priority ?? 0) || (a.options.order ?? 0) - (b.options.order ?? 0) : (a, b) => (a.options.priority ?? 0) - (b.options.priority ?? 0));
|
|
131
|
+
rec.entries = next;
|
|
132
|
+
this.markDirty(options.name, rec);
|
|
133
|
+
if (options.children) {
|
|
134
|
+
const declarations = [];
|
|
135
|
+
for (const [childKey, childSpec] of Object.entries(options.children)) {
|
|
136
|
+
const childRec = this.record(childKey);
|
|
137
|
+
childRec.spec = childSpec;
|
|
138
|
+
childRec.declaredBy = `an entry in "${options.name}"${options.registrant ? ` (${options.registrant})` : ""}`;
|
|
139
|
+
childRec.parent = options.name;
|
|
140
|
+
childRec.declarationEpoch += 1;
|
|
141
|
+
declarations.push([childKey, childRec]);
|
|
142
|
+
}
|
|
143
|
+
for (const [childKey, childRec] of declarations) this.markDirty(childKey, childRec);
|
|
144
|
+
for (const [, childRec] of declarations) this.notifyDeclaration(childRec);
|
|
145
|
+
}
|
|
146
|
+
return () => {
|
|
147
|
+
if (!rec.entries.includes(entry)) return;
|
|
148
|
+
rec.entries = rec.entries.filter((e) => e !== entry);
|
|
149
|
+
this.markDirty(options.name, rec);
|
|
150
|
+
this.releaseEntry(entry);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Whether a previously obtained entry is still registered (the render
|
|
155
|
+
* machinery's stale-authorization probe: a retained renderSlot binding
|
|
156
|
+
* whose entry left the ledger must not render).
|
|
157
|
+
* @param entry - a previously read entry.
|
|
158
|
+
* @returns false once the entry's registration was disposed.
|
|
159
|
+
*/
|
|
160
|
+
isLive(entry) {
|
|
161
|
+
for (const rec of this.records.values()) if (rec.entries.includes(entry)) return true;
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Snapshot the registered entries for a key. Returns the cached array
|
|
166
|
+
* reference (stable between mutations — safe as a uSES getSnapshot source);
|
|
167
|
+
* empty for keys not (or no longer) declared, so renderers may probe ahead
|
|
168
|
+
* of plugin load order.
|
|
169
|
+
* @param key - slot key (dynamic: the render machinery holds keys as strings).
|
|
170
|
+
* @returns entries in registration (list: order) sequence.
|
|
171
|
+
*/
|
|
172
|
+
entries(key) {
|
|
173
|
+
return this.records.get(key)?.entries ?? NO_ENTRIES;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Project a key's entries to its shadowing winners: the first live
|
|
177
|
+
* (non-abdicated) entry of each cell in priority order — single: the slot
|
|
178
|
+
* is one cell; keyed: one cell per `key`; list: one cell per `id` (winners
|
|
179
|
+
* keep ledger sequence; list renderers still refine display by `order`).
|
|
180
|
+
* Chain keys return the raw entries unchanged: election consumes every
|
|
181
|
+
* entry, shadowing does not apply. The raw {@link SlotCore.entries} view
|
|
182
|
+
* stays the inspection surface. Builds a fresh array per call — a render
|
|
183
|
+
* body read, not a uSES getSnapshot source.
|
|
184
|
+
* @param key - slot key (dynamic: the render machinery holds keys as strings).
|
|
185
|
+
* @returns the winning entry per occupied cell (empty while undeclared).
|
|
186
|
+
*/
|
|
187
|
+
entriesOfSlot(key) {
|
|
188
|
+
const rec = this.records.get(key);
|
|
189
|
+
if (!rec?.spec) return NO_ENTRIES;
|
|
190
|
+
const kind = rec.spec.kind;
|
|
191
|
+
if (kind === "chain") return rec.entries;
|
|
192
|
+
const heads = [];
|
|
193
|
+
const seenCells = /* @__PURE__ */ new Set();
|
|
194
|
+
for (const entry of rec.entries) {
|
|
195
|
+
if (this.abdicated.has(entry)) continue;
|
|
196
|
+
const cell = kind === "keyed" ? entry.options.key : kind === "list" ? entry.options.id : void 0;
|
|
197
|
+
if (seenCells.has(cell)) continue;
|
|
198
|
+
seenCells.add(cell);
|
|
199
|
+
heads.push(entry);
|
|
200
|
+
}
|
|
201
|
+
return heads;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Look up a slot's declared spec, narrowed by the SlotMap key.
|
|
205
|
+
* @param key - SlotMap key.
|
|
206
|
+
* @returns the spec, or undefined while undeclared.
|
|
207
|
+
*/
|
|
208
|
+
spec(key) {
|
|
209
|
+
return this.records.get(key)?.spec;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Dynamic-key escape hatch for spec lookup — renderers resolving keys they
|
|
213
|
+
* only hold as strings (generic dispatch) use this wide form; statically
|
|
214
|
+
* keyed callers use {@link SlotCore.spec}.
|
|
215
|
+
* @param key - candidate slot key.
|
|
216
|
+
* @returns the wide-typed spec, or undefined while undeclared.
|
|
217
|
+
*/
|
|
218
|
+
specDynamic(key) {
|
|
219
|
+
return this.records.get(key)?.spec;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Export the current declaration topology without components or executable hooks.
|
|
223
|
+
* @param root - exact Slot key to select; omitted returns every live root.
|
|
224
|
+
* @returns selected live Slot trees, or an empty array when `root` is unavailable.
|
|
225
|
+
*/
|
|
226
|
+
snapshot(root) {
|
|
227
|
+
const build = (name, seen) => {
|
|
228
|
+
const record = this.records.get(name);
|
|
229
|
+
if (record?.spec === void 0 || seen.has(name)) return void 0;
|
|
230
|
+
const branch = new Set(seen);
|
|
231
|
+
branch.add(name);
|
|
232
|
+
const active = new Set(this.entriesOfSlot(name));
|
|
233
|
+
const children = [...this.records.entries()].filter(([, candidate]) => candidate.spec !== void 0 && candidate.parent === name).flatMap(([child]) => {
|
|
234
|
+
const node = build(child, branch);
|
|
235
|
+
return node === void 0 ? [] : [node];
|
|
236
|
+
});
|
|
237
|
+
return {
|
|
238
|
+
name,
|
|
239
|
+
kind: record.spec.kind,
|
|
240
|
+
scope: record.spec.scope,
|
|
241
|
+
...record.declaredBy === void 0 ? {} : { declaredBy: record.declaredBy },
|
|
242
|
+
occupants: record.entries.map((entry) => ({
|
|
243
|
+
...entry.registrant === void 0 ? {} : { registrant: entry.registrant },
|
|
244
|
+
...entry.options.key === void 0 ? {} : { key: entry.options.key },
|
|
245
|
+
...entry.options.id === void 0 ? {} : { id: entry.options.id },
|
|
246
|
+
...entry.options.order === void 0 ? {} : { order: entry.options.order },
|
|
247
|
+
priority: entry.options.priority ?? 0,
|
|
248
|
+
active: active.has(entry)
|
|
249
|
+
})),
|
|
250
|
+
children
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
if (root !== void 0) {
|
|
254
|
+
const node = build(root, /* @__PURE__ */ new Set());
|
|
255
|
+
return node === void 0 ? [] : [node];
|
|
256
|
+
}
|
|
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());
|
|
259
|
+
return node === void 0 ? [] : [node];
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Read the declaration lifetime of a key. Entry additions and removals do
|
|
264
|
+
* not change it; declaration creation and collapse each advance it.
|
|
265
|
+
* @param key - slot key.
|
|
266
|
+
* @returns monotonic epoch (0 before the first declaration).
|
|
267
|
+
*/
|
|
268
|
+
declarationEpoch(key) {
|
|
269
|
+
return this.records.get(key)?.declarationEpoch ?? 0;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Subscribe to registration changes for a key (microtask-batched).
|
|
273
|
+
* Subscribing ahead of declaration is allowed; the declaration notifies.
|
|
274
|
+
* @param key - slot key.
|
|
275
|
+
* @param fn - change callback.
|
|
276
|
+
* @returns unsubscribe.
|
|
277
|
+
*/
|
|
278
|
+
subscribe(key, fn) {
|
|
279
|
+
const rec = this.record(key);
|
|
280
|
+
rec.listeners.add(fn);
|
|
281
|
+
return () => {
|
|
282
|
+
rec.listeners.delete(fn);
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Subscribe to declaration lifetime boundaries for a key. Notifications
|
|
287
|
+
* are synchronous so declaration teardown finishes before a subsequent
|
|
288
|
+
* same-tick registration can observe stale resources. Ordinary entry
|
|
289
|
+
* mutations do not notify this surface. A children table commits every
|
|
290
|
+
* sibling declaration before its first notification.
|
|
291
|
+
* @param key - slot key.
|
|
292
|
+
* @param fn - declaration or collapse callback.
|
|
293
|
+
* @returns unsubscribe.
|
|
294
|
+
*/
|
|
295
|
+
subscribeDeclaration(key, fn) {
|
|
296
|
+
const rec = this.record(key);
|
|
297
|
+
rec.declarationListeners.add(fn);
|
|
298
|
+
return () => {
|
|
299
|
+
rec.declarationListeners.delete(fn);
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Monotonic version for a key, bumped synchronously per mutation so a
|
|
304
|
+
* uSES getSnapshot read is never stale when its batched notification lands.
|
|
305
|
+
* @param key - slot key.
|
|
306
|
+
* @returns current version (0 for untouched keys).
|
|
307
|
+
*/
|
|
308
|
+
getVersion(key) {
|
|
309
|
+
return this.records.get(key)?.version ?? 0;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Hook every mutation (the runtime Service wrapper bridges this to ctx.emit).
|
|
313
|
+
* Fires synchronously per mutation, unbatched — event semantics need one
|
|
314
|
+
* emission per change.
|
|
315
|
+
* @param fn - called with the mutated key.
|
|
316
|
+
* @returns unsubscribe.
|
|
317
|
+
*/
|
|
318
|
+
onMutate(fn) {
|
|
319
|
+
this.mutateListeners.add(fn);
|
|
320
|
+
return () => {
|
|
321
|
+
this.mutateListeners.delete(fn);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Renderer crash report from an entry boundary. Always notifies
|
|
326
|
+
* {@link SlotCore.onEntryError} listeners; with `info.abdicate` set (the
|
|
327
|
+
* shadowing kinds — single/keyed/list) it first retires the entry from its
|
|
328
|
+
* cell, one-shot: the record's version bumps through the ordinary mutation
|
|
329
|
+
* channel so outlets re-project onto the cell's next survivor, and a
|
|
330
|
+
* repeat abdicating report no-ops entirely. Chain crashes report with
|
|
331
|
+
* `abdicate: false` — election alternatives resolve at select time, so the
|
|
332
|
+
* entry keeps its cell and only the notification fires. The registration
|
|
333
|
+
* itself stays on the ledger either way — raw {@link SlotCore.entries}
|
|
334
|
+
* still lists the entry and its disposer keeps working.
|
|
335
|
+
* @param key - slot key the entry rendered under.
|
|
336
|
+
* @param entry - the crashed entry.
|
|
337
|
+
* @param error - the crash cause, forwarded to listeners verbatim.
|
|
338
|
+
* @param info - `abdicate`: whether the crash retires the entry from its cell.
|
|
339
|
+
*/
|
|
340
|
+
reportEntryError(key, entry, error, info) {
|
|
341
|
+
if (info.abdicate) {
|
|
342
|
+
if (this.abdicated.has(entry)) return;
|
|
343
|
+
this.abdicated.add(entry);
|
|
344
|
+
const rec = this.records.get(key);
|
|
345
|
+
if (rec !== void 0) this.markDirty(key, rec);
|
|
346
|
+
}
|
|
347
|
+
for (const fn of [...this.entryErrorListeners]) fn(key, entry, error, { abdicated: info.abdicate });
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
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.
|
|
357
|
+
* @returns unsubscribe.
|
|
358
|
+
*/
|
|
359
|
+
onEntryError(fn) {
|
|
360
|
+
this.entryErrorListeners.add(fn);
|
|
361
|
+
return () => {
|
|
362
|
+
this.entryErrorListeners.delete(fn);
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Cascade for a removed entry: release its store mount and collapse every
|
|
367
|
+
* child slot it declared — specs clear, contributions empty (their stale
|
|
368
|
+
* disposers no-op), recursively down the declaration tree. One lifecycle
|
|
369
|
+
* axis: ledger rows, slots, contributions, and store mounts die together.
|
|
370
|
+
*/
|
|
371
|
+
releaseEntry(entry) {
|
|
372
|
+
if (entry.store !== void 0 && typeof entry.store !== "function") {
|
|
373
|
+
const pinned = this.handleScopes.get(entry.store);
|
|
374
|
+
if (pinned && --pinned.count === 0) this.handleScopes.delete(entry.store);
|
|
375
|
+
}
|
|
376
|
+
if (!entry.children) return;
|
|
377
|
+
for (const childKey of Object.keys(entry.children)) {
|
|
378
|
+
const childRec = this.records.get(childKey);
|
|
379
|
+
/* v8 ignore next -- defensive: declaring always creates the record */
|
|
380
|
+
if (!childRec) continue;
|
|
381
|
+
const doomed = childRec.entries;
|
|
382
|
+
childRec.spec = void 0;
|
|
383
|
+
childRec.declaredBy = void 0;
|
|
384
|
+
childRec.parent = void 0;
|
|
385
|
+
childRec.declarationEpoch += 1;
|
|
386
|
+
childRec.entries = NO_ENTRIES;
|
|
387
|
+
this.markDirty(childKey, childRec);
|
|
388
|
+
this.notifyDeclaration(childRec);
|
|
389
|
+
for (const dead of doomed) this.releaseEntry(dead);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
record(key) {
|
|
393
|
+
let rec = this.records.get(key);
|
|
394
|
+
if (!rec) {
|
|
395
|
+
rec = {
|
|
396
|
+
spec: void 0,
|
|
397
|
+
declaredBy: void 0,
|
|
398
|
+
parent: void 0,
|
|
399
|
+
declarationEpoch: 0,
|
|
400
|
+
entries: NO_ENTRIES,
|
|
401
|
+
version: 0,
|
|
402
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
403
|
+
declarationListeners: /* @__PURE__ */ new Set()
|
|
404
|
+
};
|
|
405
|
+
this.records.set(key, rec);
|
|
406
|
+
}
|
|
407
|
+
return rec;
|
|
408
|
+
}
|
|
409
|
+
markDirty(key, rec) {
|
|
410
|
+
rec.version += 1;
|
|
411
|
+
for (const fn of [...this.mutateListeners]) fn(key);
|
|
412
|
+
this.dirty.add(rec);
|
|
413
|
+
if (!this.flushScheduled) {
|
|
414
|
+
this.flushScheduled = true;
|
|
415
|
+
queueMicrotask(() => {
|
|
416
|
+
this.flush();
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
notifyDeclaration(rec) {
|
|
421
|
+
for (const fn of [...rec.declarationListeners]) fn();
|
|
422
|
+
}
|
|
423
|
+
flush() {
|
|
424
|
+
this.flushScheduled = false;
|
|
425
|
+
const dirty = [...this.dirty];
|
|
426
|
+
this.dirty.clear();
|
|
427
|
+
for (const rec of dirty) for (const fn of [...rec.listeners]) fn();
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
//#endregion
|
|
431
|
+
export { SlotCore, SlotOwnershipError, StaleAuthorizationError, resolveSlotLabel, standardHookPropName };
|
|
432
|
+
|
|
433
|
+
//# sourceMappingURL=index.js.map
|