@qilin-agent/client-ui-slots 0.0.0-stage → 3.1.4
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 +55 -0
- package/README.md +108 -2
- package/README.zh.md +109 -0
- package/lib/index.js +587 -0
- package/lib/types/index.d.ts +972 -0
- package/lib/types/renderer.d.ts +262 -0
- package/lib/types/store.d.ts +3 -0
- package/package.json +37 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
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
|
+
factories = /* @__PURE__ */ new Map();
|
|
50
|
+
mutateListeners = /* @__PURE__ */ new Set();
|
|
51
|
+
/** Shared-handle scope ledger: handle → the scope it first mounted under + live mount count. */
|
|
52
|
+
handleScopes = /* @__PURE__ */ new Map();
|
|
53
|
+
dirty = /* @__PURE__ */ new Set();
|
|
54
|
+
flushScheduled = false;
|
|
55
|
+
/**
|
|
56
|
+
* Entries retired by an abdicating crash report
|
|
57
|
+
* ({@link SlotCore.reportEntryError}): excluded from
|
|
58
|
+
* {@link SlotCore.entriesOfSlot} projections for the rest of their
|
|
59
|
+
* registration's life, while the registration itself stays on the ledger
|
|
60
|
+
* (disposal authority remains with the registrant).
|
|
61
|
+
*/
|
|
62
|
+
abdicated = /* @__PURE__ */ new WeakSet();
|
|
63
|
+
entryErrorListeners = /* @__PURE__ */ new Set();
|
|
64
|
+
constructor() {
|
|
65
|
+
const root = this.record("root");
|
|
66
|
+
root.spec = {
|
|
67
|
+
kind: "single",
|
|
68
|
+
scope: "root"
|
|
69
|
+
};
|
|
70
|
+
root.declaredBy = "(built-in)";
|
|
71
|
+
root.declarationEpoch = 1;
|
|
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
|
+
}
|
|
163
|
+
register(options, component) {
|
|
164
|
+
const rec = this.records.get(options.name);
|
|
165
|
+
if (!rec?.spec) throw new Error(`slot "${options.name}" is not declared (a parent entry's children table must declare it)`);
|
|
166
|
+
const spec = rec.spec;
|
|
167
|
+
const priority = options.priority ?? 0;
|
|
168
|
+
const occupantHint = (occupant) => `at priority ${priority}${occupant.registrant !== void 0 ? ` (registered by ${occupant.registrant})` : ""} — register at a different priority to shadow it (lowest renders)`;
|
|
169
|
+
switch (spec.kind) {
|
|
170
|
+
case "single": {
|
|
171
|
+
const occupant = rec.entries.find((e) => (e.options.priority ?? 0) === priority);
|
|
172
|
+
if (occupant) throw new Error(`single slot "${options.name}" already has a registration ${occupantHint(occupant)}`);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case "keyed": {
|
|
176
|
+
if (options.key === void 0) throw new Error(`keyed slot "${options.name}" requires options.key`);
|
|
177
|
+
const occupant = rec.entries.find((e) => e.options.key === options.key && (e.options.priority ?? 0) === priority);
|
|
178
|
+
if (occupant) throw new Error(`keyed slot "${options.name}" already has an entry for key "${options.key}" ${occupantHint(occupant)}`);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case "list": {
|
|
182
|
+
if (options.id === void 0) throw new Error(`list slot "${options.name}" requires options.id`);
|
|
183
|
+
const occupant = rec.entries.find((e) => e.options.id === options.id && (e.options.priority ?? 0) === priority);
|
|
184
|
+
if (occupant) throw new Error(`list slot "${options.name}" already has an entry with id "${options.id}" ${occupantHint(occupant)}`);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "chain":
|
|
188
|
+
if (options.select === void 0) throw new Error(`chain slot "${options.name}" requires options.select`);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
const rendersExisting = options.rendersExistingChildren === true;
|
|
192
|
+
if (options.children) for (const childKey of Object.keys(options.children)) {
|
|
193
|
+
const childRec = this.records.get(childKey);
|
|
194
|
+
if (childRec?.spec) {
|
|
195
|
+
if (rendersExisting) continue;
|
|
196
|
+
throw new Error(`slot "${childKey}" is already declared (by ${childRec.declaredBy ?? "an unknown entry"})`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (options.store !== void 0 && typeof options.store !== "function") {
|
|
200
|
+
const pinned = this.handleScopes.get(options.store);
|
|
201
|
+
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`);
|
|
202
|
+
if (pinned) pinned.count += 1;
|
|
203
|
+
else this.handleScopes.set(options.store, {
|
|
204
|
+
scope: spec.scope,
|
|
205
|
+
count: 1
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const entry = {
|
|
209
|
+
component,
|
|
210
|
+
options: {
|
|
211
|
+
...options.key !== void 0 ? { key: options.key } : {},
|
|
212
|
+
...options.id !== void 0 ? { id: options.id } : {},
|
|
213
|
+
...options.order !== void 0 ? { order: options.order } : {},
|
|
214
|
+
...options.label !== void 0 ? { label: options.label } : {},
|
|
215
|
+
...options.section !== void 0 ? { section: options.section } : {},
|
|
216
|
+
...options.priority !== void 0 ? { priority: options.priority } : {}
|
|
217
|
+
},
|
|
218
|
+
...options.select !== void 0 ? { select: options.select } : {},
|
|
219
|
+
...options.inject !== void 0 ? { inject: options.inject } : {},
|
|
220
|
+
...options.children !== void 0 ? { children: options.children } : {},
|
|
221
|
+
...options.store !== void 0 ? { store: options.store } : {},
|
|
222
|
+
...options.locale !== void 0 ? { locale: options.locale } : {},
|
|
223
|
+
...options.registrant !== void 0 ? { registrant: options.registrant } : {}
|
|
224
|
+
};
|
|
225
|
+
const next = [...rec.entries, entry];
|
|
226
|
+
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));
|
|
227
|
+
rec.entries = next;
|
|
228
|
+
this.markDirty(options.name, rec);
|
|
229
|
+
if (options.children) {
|
|
230
|
+
const declarations = [];
|
|
231
|
+
const owned = /* @__PURE__ */ new Set();
|
|
232
|
+
for (const [childKey, childSpec] of Object.entries(options.children)) {
|
|
233
|
+
const childRec = this.record(childKey);
|
|
234
|
+
if (rendersExisting && childRec.spec !== void 0) continue;
|
|
235
|
+
childRec.spec = childSpec;
|
|
236
|
+
childRec.declaredBy = `an entry in "${options.name}"${options.registrant ? ` (${options.registrant})` : ""}`;
|
|
237
|
+
childRec.parent = options.name;
|
|
238
|
+
childRec.declarationEpoch += 1;
|
|
239
|
+
declarations.push([childKey, childRec]);
|
|
240
|
+
owned.add(childKey);
|
|
241
|
+
}
|
|
242
|
+
if (rendersExisting) entry.ownedChildren = owned;
|
|
243
|
+
for (const [childKey, childRec] of declarations) this.markDirty(childKey, childRec);
|
|
244
|
+
for (const [, childRec] of declarations) this.notifyDeclaration(childRec);
|
|
245
|
+
}
|
|
246
|
+
return () => {
|
|
247
|
+
if (!rec.entries.includes(entry)) return;
|
|
248
|
+
rec.entries = rec.entries.filter((e) => e !== entry);
|
|
249
|
+
this.markDirty(options.name, rec);
|
|
250
|
+
this.releaseEntry(entry);
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Whether a previously obtained entry is still registered (the render
|
|
255
|
+
* machinery's stale-authorization probe: a retained renderSlot binding
|
|
256
|
+
* whose entry left the ledger must not render).
|
|
257
|
+
* @param entry - a previously read entry.
|
|
258
|
+
* @returns false once the entry's registration was disposed.
|
|
259
|
+
*/
|
|
260
|
+
isLive(entry) {
|
|
261
|
+
for (const rec of this.records.values()) if (rec.entries.includes(entry)) return true;
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Snapshot the registered entries for a key. Returns the cached array
|
|
266
|
+
* reference (stable between mutations — safe as a uSES getSnapshot source);
|
|
267
|
+
* empty for keys not (or no longer) declared, so renderers may probe ahead
|
|
268
|
+
* of plugin load order.
|
|
269
|
+
* @param key - slot key (dynamic: the render machinery holds keys as strings).
|
|
270
|
+
* @returns entries in registration (list: order) sequence.
|
|
271
|
+
*/
|
|
272
|
+
entries(key) {
|
|
273
|
+
return this.records.get(key)?.entries ?? NO_ENTRIES;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Project a key's entries to its shadowing winners: the first live
|
|
277
|
+
* (non-abdicated) entry of each cell in priority order — single: the slot
|
|
278
|
+
* is one cell; keyed: one cell per `key`; list: one cell per `id` (winners
|
|
279
|
+
* keep ledger sequence; list renderers still refine display by `order`).
|
|
280
|
+
* Chain keys return the raw entries unchanged: election consumes every
|
|
281
|
+
* entry, shadowing does not apply. The raw {@link SlotCore.entries} view
|
|
282
|
+
* stays the inspection surface. Builds a fresh array per call — a render
|
|
283
|
+
* body read, not a uSES getSnapshot source.
|
|
284
|
+
* @param key - slot key (dynamic: the render machinery holds keys as strings).
|
|
285
|
+
* @returns the winning entry per occupied cell (empty while undeclared).
|
|
286
|
+
*/
|
|
287
|
+
entriesOfSlot(key) {
|
|
288
|
+
const rec = this.records.get(key);
|
|
289
|
+
if (!rec?.spec) return NO_ENTRIES;
|
|
290
|
+
const kind = rec.spec.kind;
|
|
291
|
+
if (kind === "chain") return rec.entries;
|
|
292
|
+
const heads = [];
|
|
293
|
+
const seenCells = /* @__PURE__ */ new Set();
|
|
294
|
+
for (const entry of rec.entries) {
|
|
295
|
+
if (this.abdicated.has(entry)) continue;
|
|
296
|
+
const cell = kind === "keyed" ? entry.options.key : kind === "list" ? entry.options.id : void 0;
|
|
297
|
+
if (seenCells.has(cell)) continue;
|
|
298
|
+
seenCells.add(cell);
|
|
299
|
+
heads.push(entry);
|
|
300
|
+
}
|
|
301
|
+
return heads;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Look up a slot's declared spec, narrowed by the SlotMap key.
|
|
305
|
+
* @param key - SlotMap key.
|
|
306
|
+
* @returns the spec, or undefined while undeclared.
|
|
307
|
+
*/
|
|
308
|
+
spec(key) {
|
|
309
|
+
return this.records.get(key)?.spec;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Dynamic-key escape hatch for spec lookup — renderers resolving keys they
|
|
313
|
+
* only hold as strings (generic dispatch) use this wide form; statically
|
|
314
|
+
* keyed callers use {@link SlotCore.spec}.
|
|
315
|
+
* @param key - candidate slot key.
|
|
316
|
+
* @returns the wide-typed spec, or undefined while undeclared.
|
|
317
|
+
*/
|
|
318
|
+
specDynamic(key) {
|
|
319
|
+
return this.records.get(key)?.spec;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Export the current declaration topology without components or executable hooks.
|
|
323
|
+
* Factory definitions appear as `factory:<name>` parents of their ordinary
|
|
324
|
+
* child Slots, matching the parent/child topology of ordinary registrations.
|
|
325
|
+
* @param root - exact Slot or `factory:<name>` key to select; omitted returns every live root.
|
|
326
|
+
* @returns selected live Slot trees, or an empty array when `root` is unavailable.
|
|
327
|
+
*/
|
|
328
|
+
snapshot(root) {
|
|
329
|
+
const buildSlot = (name, seen) => {
|
|
330
|
+
const record = this.records.get(name);
|
|
331
|
+
if (record?.spec === void 0 || seen.has(name)) return void 0;
|
|
332
|
+
const branch = new Set(seen);
|
|
333
|
+
branch.add(name);
|
|
334
|
+
const active = new Set(this.entriesOfSlot(name));
|
|
335
|
+
const children = [...this.records.entries()].filter(([, candidate]) => candidate.spec !== void 0 && candidate.parent === name).flatMap(([child]) => {
|
|
336
|
+
const node = buildSlot(child, branch);
|
|
337
|
+
return node === void 0 ? [] : [node];
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
type: "slot",
|
|
341
|
+
name,
|
|
342
|
+
kind: record.spec.kind,
|
|
343
|
+
scope: record.spec.scope,
|
|
344
|
+
...record.declaredBy === void 0 ? {} : { declaredBy: record.declaredBy },
|
|
345
|
+
occupants: record.entries.map((entry) => ({
|
|
346
|
+
...entry.registrant === void 0 ? {} : { registrant: entry.registrant },
|
|
347
|
+
...entry.options.key === void 0 ? {} : { key: entry.options.key },
|
|
348
|
+
...entry.options.id === void 0 ? {} : { id: entry.options.id },
|
|
349
|
+
...entry.options.order === void 0 ? {} : { order: entry.options.order },
|
|
350
|
+
priority: entry.options.priority ?? 0,
|
|
351
|
+
active: active.has(entry)
|
|
352
|
+
})),
|
|
353
|
+
children
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
const buildFactory = (name) => {
|
|
357
|
+
const definition = this.factories.get(name)?.definition;
|
|
358
|
+
if (definition === void 0) return void 0;
|
|
359
|
+
const nodeName = `factory:${name}`;
|
|
360
|
+
const children = [...this.records.entries()].filter(([, candidate]) => candidate.spec !== void 0 && candidate.parent === nodeName).flatMap(([child]) => {
|
|
361
|
+
const node = buildSlot(child, new Set([nodeName]));
|
|
362
|
+
return node === void 0 ? [] : [node];
|
|
363
|
+
});
|
|
364
|
+
return {
|
|
365
|
+
type: "factory",
|
|
366
|
+
name,
|
|
367
|
+
scope: definition.scope,
|
|
368
|
+
...definition.registrant === void 0 ? {} : { registrant: definition.registrant },
|
|
369
|
+
children
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
if (root !== void 0) {
|
|
373
|
+
const node = root.startsWith("factory:") ? buildFactory(root.slice(8)) : buildSlot(root, /* @__PURE__ */ new Set());
|
|
374
|
+
return node === void 0 ? [] : [node];
|
|
375
|
+
}
|
|
376
|
+
const slots = [...this.records.entries()].filter(([, record]) => record.spec !== void 0 && record.parent === void 0).flatMap(([name]) => {
|
|
377
|
+
const node = buildSlot(name, /* @__PURE__ */ new Set());
|
|
378
|
+
return node === void 0 ? [] : [node];
|
|
379
|
+
});
|
|
380
|
+
const factories = [...this.factories.keys()].flatMap((name) => {
|
|
381
|
+
const node = buildFactory(name);
|
|
382
|
+
return node === void 0 ? [] : [node];
|
|
383
|
+
});
|
|
384
|
+
return [...slots, ...factories];
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Read the declaration lifetime of a key. Entry additions and removals do
|
|
388
|
+
* not change it; declaration creation and collapse each advance it.
|
|
389
|
+
* @param key - slot key.
|
|
390
|
+
* @returns monotonic epoch (0 before the first declaration).
|
|
391
|
+
*/
|
|
392
|
+
declarationEpoch(key) {
|
|
393
|
+
return this.records.get(key)?.declarationEpoch ?? 0;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Subscribe to registration changes for a key (microtask-batched).
|
|
397
|
+
* Subscribing ahead of declaration is allowed; the declaration notifies.
|
|
398
|
+
* @param key - slot key.
|
|
399
|
+
* @param fn - change callback.
|
|
400
|
+
* @returns unsubscribe.
|
|
401
|
+
*/
|
|
402
|
+
subscribe(key, fn) {
|
|
403
|
+
const rec = this.record(key);
|
|
404
|
+
rec.listeners.add(fn);
|
|
405
|
+
return () => {
|
|
406
|
+
rec.listeners.delete(fn);
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Subscribe to declaration lifetime boundaries for a key. Notifications
|
|
411
|
+
* are synchronous so declaration teardown finishes before a subsequent
|
|
412
|
+
* same-tick registration can observe stale resources. Ordinary entry
|
|
413
|
+
* mutations do not notify this surface. A children table commits every
|
|
414
|
+
* sibling declaration before its first notification.
|
|
415
|
+
* @param key - slot key.
|
|
416
|
+
* @param fn - declaration or collapse callback.
|
|
417
|
+
* @returns unsubscribe.
|
|
418
|
+
*/
|
|
419
|
+
subscribeDeclaration(key, fn) {
|
|
420
|
+
const rec = this.record(key);
|
|
421
|
+
rec.declarationListeners.add(fn);
|
|
422
|
+
return () => {
|
|
423
|
+
rec.declarationListeners.delete(fn);
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Monotonic version for a key, bumped synchronously per mutation so a
|
|
428
|
+
* uSES getSnapshot read is never stale when its batched notification lands.
|
|
429
|
+
* @param key - slot key.
|
|
430
|
+
* @returns current version (0 for untouched keys).
|
|
431
|
+
*/
|
|
432
|
+
getVersion(key) {
|
|
433
|
+
return this.records.get(key)?.version ?? 0;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Hook every mutation (the runtime Service wrapper bridges this to ctx.emit).
|
|
437
|
+
* Fires synchronously per mutation, unbatched — event semantics need one
|
|
438
|
+
* emission per change.
|
|
439
|
+
* @param fn - called with the mutated key.
|
|
440
|
+
* @returns unsubscribe.
|
|
441
|
+
*/
|
|
442
|
+
onMutate(fn) {
|
|
443
|
+
this.mutateListeners.add(fn);
|
|
444
|
+
return () => {
|
|
445
|
+
this.mutateListeners.delete(fn);
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Renderer crash report from an entry boundary. Always notifies
|
|
450
|
+
* {@link SlotCore.onEntryError} listeners; with `info.abdicate` set (the
|
|
451
|
+
* shadowing kinds — single/keyed/list) it first retires the entry from its
|
|
452
|
+
* cell, one-shot: the record's version bumps through the ordinary mutation
|
|
453
|
+
* channel so outlets re-project onto the cell's next survivor, and a
|
|
454
|
+
* repeat abdicating report no-ops entirely. Chain crashes report with
|
|
455
|
+
* `abdicate: false` — election alternatives resolve at select time, so the
|
|
456
|
+
* entry keeps its cell and only the notification fires. The registration
|
|
457
|
+
* itself stays on the ledger either way — raw {@link SlotCore.entries}
|
|
458
|
+
* still lists the entry and its disposer keeps working.
|
|
459
|
+
* @param key - slot key the entry rendered under.
|
|
460
|
+
* @param entry - the crashed entry.
|
|
461
|
+
* @param error - the crash cause, forwarded to listeners verbatim.
|
|
462
|
+
* @param info - `abdicate`: whether the crash retires the entry from its cell.
|
|
463
|
+
*/
|
|
464
|
+
reportEntryError(key, entry, error, info) {
|
|
465
|
+
if (info.abdicate) {
|
|
466
|
+
if (this.abdicated.has(entry)) return;
|
|
467
|
+
this.abdicated.add(entry);
|
|
468
|
+
const rec = this.records.get(key);
|
|
469
|
+
if (rec !== void 0) this.markDirty(key, rec);
|
|
470
|
+
}
|
|
471
|
+
for (const fn of [...this.entryErrorListeners]) fn(key, entry, error, { abdicated: info.abdicate });
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Report a contained Factory occurrence crash through the ordinary entry
|
|
475
|
+
* supervision channel without retiring the shared definition.
|
|
476
|
+
* @param name - Factory name whose occurrence crashed.
|
|
477
|
+
* @param registration - Factory definition or caller registration that owns the crashing Component.
|
|
478
|
+
* @param error - the crash cause, forwarded to listeners verbatim.
|
|
479
|
+
*/
|
|
480
|
+
reportFactoryError(name, registration, error) {
|
|
481
|
+
for (const fn of [...this.entryErrorListeners]) fn(`factory:${name}`, registration, error, { abdicated: false });
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Observe ordinary entry and Factory occurrence crashes. Fires synchronously
|
|
485
|
+
* per report, after any ordinary-entry abdication mutation. Factory failures
|
|
486
|
+
* never retire their shared definition.
|
|
487
|
+
* @param fn - called with the Slot or `factory:<name>` key, the crashed
|
|
488
|
+
* registration, the cause, and whether an ordinary entry was retired.
|
|
489
|
+
* @returns unsubscribe.
|
|
490
|
+
*/
|
|
491
|
+
onEntryError(fn) {
|
|
492
|
+
this.entryErrorListeners.add(fn);
|
|
493
|
+
return () => {
|
|
494
|
+
this.entryErrorListeners.delete(fn);
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Cascade for a removed entry: release its store mount and collapse every
|
|
499
|
+
* child slot it declared — specs clear, contributions empty (their stale
|
|
500
|
+
* disposers no-op), recursively down the declaration tree. One lifecycle
|
|
501
|
+
* axis: ledger rows, slots, contributions, and store mounts die together.
|
|
502
|
+
*/
|
|
503
|
+
releaseEntry(entry) {
|
|
504
|
+
if (entry.store !== void 0 && typeof entry.store !== "function") {
|
|
505
|
+
const pinned = this.handleScopes.get(entry.store);
|
|
506
|
+
if (pinned && --pinned.count === 0) this.handleScopes.delete(entry.store);
|
|
507
|
+
}
|
|
508
|
+
this.releaseChildren(entry.children, entry.ownedChildren);
|
|
509
|
+
}
|
|
510
|
+
releaseChildren(children, ownedChildren) {
|
|
511
|
+
if (children === void 0) return;
|
|
512
|
+
for (const childKey of Object.keys(children)) {
|
|
513
|
+
if (ownedChildren !== void 0 && !ownedChildren.has(childKey)) continue;
|
|
514
|
+
const childRec = this.records.get(childKey);
|
|
515
|
+
/* v8 ignore next -- defensive: declaring always creates the record */
|
|
516
|
+
if (!childRec) continue;
|
|
517
|
+
const doomed = childRec.entries;
|
|
518
|
+
childRec.spec = void 0;
|
|
519
|
+
childRec.declaredBy = void 0;
|
|
520
|
+
childRec.parent = void 0;
|
|
521
|
+
childRec.declarationEpoch += 1;
|
|
522
|
+
childRec.entries = NO_ENTRIES;
|
|
523
|
+
this.markDirty(childKey, childRec);
|
|
524
|
+
this.notifyDeclaration(childRec);
|
|
525
|
+
for (const dead of doomed) this.releaseEntry(dead);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
record(key) {
|
|
529
|
+
let rec = this.records.get(key);
|
|
530
|
+
if (!rec) {
|
|
531
|
+
rec = {
|
|
532
|
+
spec: void 0,
|
|
533
|
+
declaredBy: void 0,
|
|
534
|
+
parent: void 0,
|
|
535
|
+
declarationEpoch: 0,
|
|
536
|
+
entries: NO_ENTRIES,
|
|
537
|
+
version: 0,
|
|
538
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
539
|
+
declarationListeners: /* @__PURE__ */ new Set()
|
|
540
|
+
};
|
|
541
|
+
this.records.set(key, rec);
|
|
542
|
+
}
|
|
543
|
+
return rec;
|
|
544
|
+
}
|
|
545
|
+
factoryRecord(name) {
|
|
546
|
+
let record = this.factories.get(name);
|
|
547
|
+
if (record === void 0) {
|
|
548
|
+
record = {
|
|
549
|
+
definition: void 0,
|
|
550
|
+
version: 0,
|
|
551
|
+
listeners: /* @__PURE__ */ new Set()
|
|
552
|
+
};
|
|
553
|
+
this.factories.set(name, record);
|
|
554
|
+
}
|
|
555
|
+
return record;
|
|
556
|
+
}
|
|
557
|
+
markFactoryDirty(record) {
|
|
558
|
+
record.version += 1;
|
|
559
|
+
queueMicrotask(() => {
|
|
560
|
+
for (const listener of [...record.listeners]) listener();
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
markDirty(key, rec) {
|
|
564
|
+
rec.version += 1;
|
|
565
|
+
for (const fn of [...this.mutateListeners]) fn(key);
|
|
566
|
+
this.dirty.add(rec);
|
|
567
|
+
if (!this.flushScheduled) {
|
|
568
|
+
this.flushScheduled = true;
|
|
569
|
+
queueMicrotask(() => {
|
|
570
|
+
this.flush();
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
notifyDeclaration(rec) {
|
|
575
|
+
for (const fn of [...rec.declarationListeners]) fn();
|
|
576
|
+
}
|
|
577
|
+
flush() {
|
|
578
|
+
this.flushScheduled = false;
|
|
579
|
+
const dirty = [...this.dirty];
|
|
580
|
+
this.dirty.clear();
|
|
581
|
+
for (const rec of dirty) for (const fn of [...rec.listeners]) fn();
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
//#endregion
|
|
585
|
+
export { SlotCore, SlotOwnershipError, StaleAuthorizationError, resolveSlotLabel, standardHookPropName };
|
|
586
|
+
|
|
587
|
+
//# sourceMappingURL=index.js.map
|