@webjsdev/core 0.7.50 → 0.7.53

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.
Files changed (64) hide show
  1. package/dist/webjs-core-browser.js +5 -5
  2. package/dist/webjs-core-browser.js.map +3 -3
  3. package/dist/webjs-core-testing.js +20 -20
  4. package/dist/webjs-core-testing.js.map +3 -3
  5. package/dist/webjs-core.js +7 -7
  6. package/dist/webjs-core.js.map +3 -3
  7. package/index-browser.js +1 -1
  8. package/index.d.ts +3 -3
  9. package/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/component/lifecycle.js +1481 -0
  12. package/src/component/reactive.js +171 -0
  13. package/src/component/server-element.js +248 -0
  14. package/src/component.js +2 -1896
  15. package/src/css.d.ts +9 -0
  16. package/src/escape.d.ts +2 -0
  17. package/src/html.d.ts +9 -0
  18. package/src/render-client/parts.js +2039 -0
  19. package/src/render-client/reconciler.js +388 -0
  20. package/src/render-client/template-compiler.js +525 -0
  21. package/src/render-client.js +1 -2938
  22. package/src/render-server/attrs.js +160 -0
  23. package/src/render-server/dsd.js +445 -0
  24. package/src/render-server/html-scan.js +402 -0
  25. package/src/render-server/slots.js +213 -0
  26. package/src/render-server/stream.js +163 -0
  27. package/src/render-server/template-renderer.js +938 -0
  28. package/src/render-server/text.js +163 -0
  29. package/src/render-server.js +6 -2405
  30. package/src/repeat.d.ts +15 -0
  31. package/src/rich-fetch.d.ts +4 -0
  32. package/src/router-client/anchors.js +44 -0
  33. package/src/router-client/boundaries.js +221 -0
  34. package/src/router-client/constants.js +206 -0
  35. package/src/router-client/diagnostics.js +377 -0
  36. package/src/router-client/dom-differ.js +709 -0
  37. package/src/router-client/dom-parse.js +150 -0
  38. package/src/router-client/events.js +182 -0
  39. package/src/router-client/fetch-apply.js +432 -0
  40. package/src/router-client/form-encoder.js +135 -0
  41. package/src/router-client/frames.js +329 -0
  42. package/src/router-client/head-merge.js +344 -0
  43. package/src/router-client/nav-error.js +105 -0
  44. package/src/router-client/navigator.js +952 -0
  45. package/src/router-client/prefetch.js +831 -0
  46. package/src/router-client/scroll.js +308 -0
  47. package/src/router-client/snapshot-cache.js +113 -0
  48. package/src/router-client/state.js +155 -0
  49. package/src/router-client/stream.js +206 -0
  50. package/src/router-client/swap.js +454 -0
  51. package/src/router-client/upgrade.js +261 -0
  52. package/src/router-client/view-transition.js +223 -0
  53. package/src/router-client.d.ts +11 -2
  54. package/src/router-client.js +78 -5345
  55. package/src/slot/assignment.js +304 -0
  56. package/src/slot/interception.js +558 -0
  57. package/src/slot/polyfills.js +365 -0
  58. package/src/slot/project.js +432 -0
  59. package/src/slot/sensors.js +303 -0
  60. package/src/slot/state.js +245 -0
  61. package/src/slot/symbols.js +85 -0
  62. package/src/slot.js +7 -2199
  63. package/src/suspense.d.ts +9 -0
  64. package/src/websocket-client.d.ts +22 -0
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Projection: placing authored children into their slots, and resyncing
3
+ * after a re-render.
4
+ *
5
+ * Moved verbatim out of the pre-split `slot.js`; see that barrel for the
6
+ * runtime's full contract.
7
+ *
8
+ * @module
9
+ */
10
+ import { applyActualAssignment, applyFallback, arraysEqual, isOwnSlot, queueSlotChange } from './assignment.js';
11
+ import { EMPTY_NODE_SET, isInsideAuthored, parkFor } from './interception.js';
12
+ import { inBrowser, manualSlotFor } from './polyfills.js';
13
+ import { N_appendChild, N_removeChild, processBackstop, withRendererWrites } from './sensors.js';
14
+ import { effectiveKeyOf, ensureSlotState, keyOfName, repartition } from './state.js';
15
+ import { FRAMEWORK_DETACHED, LIGHT_SLOT_ATTR, PARK, PROJECTION_ACTUAL, PROJECTION_ATTR, SLOT_FALLBACK_FRAG, SLOT_STATE } from './symbols.js';
16
+
17
+ /**
18
+ * Replace the authored content assigned to slot `name` with `nodes`, in place
19
+ * of the old slice's position (a new name appends). The ONE public seam the
20
+ * client router uses to reconcile a reused light host's projected content
21
+ * during a same-route morph (replacing the deleted `setSlotContent`): the
22
+ * router never touches `authored` / `assignedByName` / `lastSnapshot` directly,
23
+ * and this is the same record-then-place primitive the interception layer runs.
24
+ *
25
+ * @param {Element} host
26
+ * @param {string | null} name
27
+ * @param {Node[] | Node | null} nodes
28
+ */
29
+ export function projectAuthored(host, name, nodes) {
30
+ const state = ensureSlotState(host);
31
+ const key = keyOfName(name);
32
+ const list = Array.isArray(nodes) ? nodes.filter(Boolean) : nodes ? [nodes] : [];
33
+ // Evict the old slice by the node's EFFECTIVE key (the manual assign()
34
+ // overlay when present, else the slot= attribute): filtering on the raw
35
+ // attribute would evict a manually-assigned attribute-less node from the
36
+ // default slice and silently drop another slot's content.
37
+ let at = state.authored.findIndex((n) => effectiveKeyOf(state, n) === key);
38
+ const evicted = state.authored.filter((n) => effectiveKeyOf(state, n) === key);
39
+ state.authored = state.authored.filter((n) => effectiveKeyOf(state, n) !== key);
40
+ if (at === -1 || at > state.authored.length) at = state.authored.length;
41
+ for (const n of list) {
42
+ // Stamp the slice key onto element nodes so the derived partition
43
+ // matches the projection, EXCEPT nodes with a live manual assign()
44
+ // entry: their routing is element-bound (the overlay outranks the
45
+ // attribute), and the attribute is the author's latent intent that must
46
+ // survive the overlay's release.
47
+ if (n.nodeType === 1 && !manualSlotFor(state, n)) {
48
+ if (key == null) /** @type {Element} */ (n).removeAttribute('slot');
49
+ else /** @type {Element} */ (n).setAttribute('slot', key);
50
+ }
51
+ FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
52
+ }
53
+ state.authored.splice(at, 0, ...list);
54
+ if (state.applying && state.pendingRecordNodes) {
55
+ for (const n of evicted) state.pendingRecordNodes.add(n);
56
+ for (const n of list) state.pendingRecordNodes.add(n);
57
+ } else {
58
+ state.pendingRecordNodes = new Set([...evicted, ...list]);
59
+ }
60
+ // Scope the adoption-clear to THIS call's nodes: under the latch the
61
+ // pending set may carry a co-pending nested op's nodes, whose adoptions
62
+ // are not this projection's to end.
63
+ if (state.adoptedKey) {
64
+ for (const n of evicted) state.adoptedKey.delete(n);
65
+ for (const n of list) state.adoptedKey.delete(n);
66
+ }
67
+ repartition(state);
68
+ applySlotAssignments(host);
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Native-write window + host interception (native slot-API liveness)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * Place the slot record into the host's OWN light-DOM slots (#1015). The
77
+ * renderer's slot parts call this after the template commits, and the
78
+ * interception + sensors call it after an authored mutation. Idempotent and cheap on
79
+ * no-change passes.
80
+ *
81
+ * 1. Collect the host's OWN slots (no other custom element between the
82
+ * slot and the host; a nested component's slots belong to it).
83
+ * 2. Group by `name`, first-wins: the first slot of each name shows the
84
+ * record content, later duplicates show fallback.
85
+ * 3. `data-projection` is stamped "actual" or "fallback" accordingly;
86
+ * fallback content swaps through the part-owned holding fragment.
87
+ * 4. `slotchange` fires on slots whose assigned set actually changed.
88
+ *
89
+ * @param {Element} host
90
+ */
91
+ export function applySlotAssignments(host) {
92
+ if (!inBrowser) return;
93
+ const state = /** @type {SlotState | undefined} */ (
94
+ /** @type {any} */ (host)[SLOT_STATE]
95
+ );
96
+ if (!state) return;
97
+
98
+ // Re-entrancy latch: a custom-element reaction fired by a placement or
99
+ // fallback removal (a disconnectedCallback doing host.appendChild) can
100
+ // author-write while THIS pass is mid-flight; running a nested pass
101
+ // would let the outer loop destroy the inner placements and desync
102
+ // data-projection from the snapshot. The nested call returns after
103
+ // flagging a re-run (its record splice already happened in the
104
+ // interceptor), and the outer pass repeats until a full pass runs with
105
+ // no re-entrant write. The end-of-pass source drain also rides this
106
+ // loop instead of recursing.
107
+ if (state.applying) {
108
+ state.reapply = true;
109
+ return;
110
+ }
111
+ state.applying = true;
112
+ try {
113
+ do {
114
+ state.reapply = false;
115
+
116
+ // 0. Self-heal, prune, re-derive, in that order. First fold in what a
117
+ // NON-record writer wrote inside our own actual slots (a parent
118
+ // component's hole committed within projected content, or a library
119
+ // operating on the assigned container): the apply step is the only
120
+ // record writer, so physical-vs-snapshot divergence means someone else
121
+ // wrote there, and destroying their nodes on this pass would be the
122
+ // one-writer violation in reverse. Then prune the record of nodes the
123
+ // author detached out from under us (an `el.remove()` on a projected
124
+ // child, or a re-parent elsewhere): their parent is no longer one of our
125
+ // own slots / the park, and we did not detach them ourselves. Then
126
+ // re-derive from the surviving `authored` so a `slot=` change (or any
127
+ // authored mutation) is reflected before placement.
128
+ const pendingNodes = state.pendingRecordNodes || EMPTY_NODE_SET;
129
+ state.pendingRecordNodes = undefined;
130
+ resyncActualSlots(host, state, pendingNodes);
131
+ pruneAuthored(host, state);
132
+ repartition(state);
133
+
134
+ // 1. The host's own slots, document order. A slot that is BOUND but not yet
135
+ // FINALIZED (its slot-part deferred finalize to a microtask: it carries
136
+ // data-webjs-light from compile time but has neither a data-projection
137
+ // stamp nor a harvested fallback frag) is EXCLUDED from placement this
138
+ // pass: treating it as fallback-mode would destroy its un-harvested
139
+ // fallback clone, and the finalize's own queued apply covers it one
140
+ // microtask later. Its NAME still counts as rendered (pendingNames) so
141
+ // the park step does not spuriously park (and bounce) its content.
142
+ /** @type {HTMLSlotElement[]} */
143
+ const slots = [];
144
+ /** @type {Set<string|null>} */
145
+ const pendingNames = new Set();
146
+ for (const el of host.querySelectorAll(`slot[${LIGHT_SLOT_ATTR}]`)) {
147
+ if (!isOwnSlot(host, el)) continue;
148
+ // An own slot can NEVER live inside AUTHORED content: a slot element the
149
+ // author moved or wrote into the host (another component's chunk, a
150
+ // spoofed stamp) is inert content, exactly like a <slot> outside a
151
+ // shadow tree natively. Collecting it would hand it assignments whose
152
+ // nodes can CONTAIN it (HierarchyRequestError at placement).
153
+ if (isInsideAuthored(state, host, el)) continue;
154
+ if (
155
+ !el.hasAttribute(PROJECTION_ATTR) &&
156
+ !(SLOT_FALLBACK_FRAG in /** @type {any} */ (el))
157
+ ) {
158
+ pendingNames.add(keyOfName(el.getAttribute('name')));
159
+ continue;
160
+ }
161
+ slots.push(/** @type {HTMLSlotElement} */ (el));
162
+ }
163
+
164
+ // 2. Group by current `name` attribute in document order.
165
+ /** @type {Map<string|null, HTMLSlotElement[]>} */
166
+ const groups = new Map();
167
+ for (const slot of slots) {
168
+ const name = keyOfName(slot.getAttribute('name'));
169
+ let arr = groups.get(name);
170
+ if (!arr) {
171
+ arr = [];
172
+ groups.set(name, arr);
173
+ }
174
+ arr.push(slot);
175
+ }
176
+
177
+ // 3. Assign per the first-wins rule; a node manually bound via `assign()`
178
+ // routes to ITS slot element (native binds slottables to the receiving
179
+ // element), everything else to the first slot of its name.
180
+ /** @type {HTMLSlotElement[]} */
181
+ const slotsChanged = [];
182
+ for (const [name, group] of groups) {
183
+ const assigned = state.assignedByName.get(name) || [];
184
+ for (let i = 0; i < group.length; i++) {
185
+ const slot = group[i];
186
+ let own = assigned.filter((n) => {
187
+ const m = manualSlotFor(state, n);
188
+ return m ? m === slot : i === 0;
189
+ });
190
+ // Placement-time cycle shield (skip, never throw): a reaction
191
+ // fired by an EARLIER slice's departure removal can reparent the
192
+ // HOST into a node this slice is about to place; inserting it
193
+ // would throw a native HierarchyRequestError out of the whole
194
+ // apply pass. The filter runs BEFORE the actual/fallback
195
+ // decision, so a fully poisoned slice correctly degrades to
196
+ // FALLBACK instead of an actual-stamped empty slot, and the
197
+ // poisoned nodes' prune exemptions are CLEARED so the next pass's
198
+ // prune reaps them by their (outside-the-host) parent chain.
199
+ const poisoned = own.filter(
200
+ (n) => n === host || (n.nodeType === 1 && /** @type {Element} */ (n).contains(host)),
201
+ );
202
+ if (poisoned.length) {
203
+ for (const n of poisoned) FRAMEWORK_DETACHED.delete(n);
204
+ own = own.filter((n) => poisoned.indexOf(n) === -1);
205
+ }
206
+ if (own.length > 0) {
207
+ if (applyActualAssignment(state, slot, own)) {
208
+ slotsChanged.push(slot);
209
+ }
210
+ } else {
211
+ if (applyFallback(state, slot)) slotsChanged.push(slot);
212
+ }
213
+ }
214
+ }
215
+
216
+ // 4. Queue slotchange on slots whose assignment actually changed. Native
217
+ // timing: assignment recomputes synchronously (placement above already
218
+ // ran) but the slotchange EVENT is async and coalesced (one per slot per
219
+ // microtask). Synchronous dispatch here would let an author mutation
220
+ // inside a slotchange handler recurse into this writer mid-loop, and
221
+ // would fire N events for an N-node loop; coalescing matches the spec.
222
+ for (const slot of slotsChanged) queueSlotChange(state, slot);
223
+
224
+ // 5. Park authored nodes whose name matches no rendered own-slot. Native
225
+ // shadow keeps an unassigned child connected but unrendered (a nested
226
+ // custom element still upgrades and runs connectedCallback); a hidden
227
+ // holding element inside the host reproduces that. Parked nodes have
228
+ // parentNode === park, so the prune rule keeps them. The park is
229
+ // RECONCILED to exactly the current unmatched set: a node that left the
230
+ // record (or now matches a slot) is detached from the park, so a removed
231
+ // parked child ends up isConnected === false like native removeChild.
232
+ const matched = new Set(groups.keys());
233
+ for (const name of pendingNames) matched.add(name);
234
+ const shouldPark = new Set();
235
+ for (const n of state.authored) {
236
+ if (!matched.has(effectiveKeyOf(state, n))) shouldPark.add(n);
237
+ }
238
+ const existingPark = /** @type {any} */ (host)[PARK];
239
+ if (existingPark) {
240
+ for (const n of Array.from(existingPark.childNodes)) {
241
+ if (!shouldPark.has(n)) N_removeChild.call(existingPark, n);
242
+ }
243
+ }
244
+ if (shouldPark.size) {
245
+ const park = parkFor(host);
246
+ for (const n of shouldPark) {
247
+ if (n.parentNode !== park) {
248
+ FRAMEWORK_DETACHED.delete(n);
249
+ withRendererWrites(host, () => N_appendChild.call(park, n));
250
+ }
251
+ }
252
+ }
253
+
254
+ // LAST step: consume the records this apply's own placements generated,
255
+ // while their containment evidence is still fresh. A host-to-slot
256
+ // placement removal processed now sees contains === true and is retained
257
+ // trivially, so it can never age into a stale record that a later rescue
258
+ // makes indistinguishable from an author removal (the retention conjuncts
259
+ // in processBackstop stay as a second line for any record that still
260
+ // straddles). Running at the END keeps any recursive fold's inner apply
261
+ // from racing this pass's park bookkeeping; recursion terminates because
262
+ // a pure-placement batch marks nothing dirty and an inner apply leaves an
263
+ // empty queue behind.
264
+ // DEFENSE-IN-DEPTH, empirically not required by any suite-constructible
265
+ // interleaving at this head (deleting it stays green): the window-close
266
+ // drains and the retention conjuncts cover every reproduced sequence.
267
+ // It stays because the stale-placement-record race was independently
268
+ // traced by two reviews, is O(pending records) cheap, provably
269
+ // terminates under the latch, and processing placement records with
270
+ // fresh containment can only ever retain correctly.
271
+ if (state.backstop) {
272
+ const placementRecords = state.backstop.takeRecords();
273
+ if (placementRecords.length) processBackstop(host, state, placementRecords);
274
+ }
275
+ } while (state.reapply);
276
+ } finally {
277
+ state.applying = false;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Self-heal `authored` against a NON-record writer that wrote INSIDE one of
283
+ * the host's own actual slots. Two such writers are legitimate: a parent
284
+ * component whose hole was authored as this host's content (its child-part
285
+ * marker projects into the slot, so a later array / template commit inserts
286
+ * or removes nodes there with no interceptor in the way), and a third-party
287
+ * library operating on the assigned container (the documented target for
288
+ * generic DOM code). The apply step is the ONLY record-driven DOM writer, so
289
+ * a slot whose physical childNodes diverge from its `lastSnapshot` was
290
+ * written by someone else since the last apply; destroying those nodes on
291
+ * this pass (the pre-fix behaviour) is the one-writer violation in reverse
292
+ * and detaches DOM a live renderer part still points at.
293
+ *
294
+ * Reconciliation rule, per diverged slot. Order authority is NODE-scoped,
295
+ * never pass-scoped (a pass-scoped rule made the outcome depend on which
296
+ * trigger happened to run the apply):
297
+ * - The PHYSICAL order of the slice is the base: a renderer part reordering
298
+ * a keyed list inside the slot is never fought back, regardless of what
299
+ * triggered this apply.
300
+ * - Nodes the current record op TOUCHED (`pendingNodes`: inserted, moved, or
301
+ * removed by the interceptor / assign() / router splice that triggered
302
+ * this apply) are taken OUT of the base and re-anchored at their
303
+ * record-implied position, so an author's expressed move (appendChild of
304
+ * an existing child = move to end) is honoured; an op-REMOVED node is in
305
+ * `pendingNodes` but no longer in the record, so it simply drops.
306
+ * - Record nodes missing from the slot (a bypass move onto the host, a
307
+ * genuine author detach) are also re-anchored; `pruneAuthored`, which runs
308
+ * right after, decides their fate structurally by their current parent
309
+ * (re-place vs drop), so no zombie is resurrected.
310
+ *
311
+ * @param {Element} host
312
+ * @param {SlotState} state
313
+ * @param {Set<Node>} pendingNodes
314
+ */
315
+ function resyncActualSlots(host, state, pendingNodes) {
316
+ // lastSnapshot is a WeakMap (not iterable): walk the host's own APPLIED
317
+ // actual slots instead, which are exactly the elements a snapshot exists
318
+ // for.
319
+ for (const el of host.querySelectorAll(
320
+ `slot[${LIGHT_SLOT_ATTR}][${PROJECTION_ATTR}="${PROJECTION_ACTUAL}"]`,
321
+ )) {
322
+ if (!isOwnSlot(host, el)) continue;
323
+ if (isInsideAuthored(state, host, el)) continue; // authored content, not a slot
324
+ const slot = /** @type {HTMLSlotElement} */ (el);
325
+ const snapshot = state.lastSnapshot.get(slot);
326
+ if (!snapshot) continue;
327
+ const physical = Array.from(slot.childNodes);
328
+ if (arraysEqual(physical, snapshot)) continue;
329
+ const a = state.authored;
330
+ const key = keyOfName(slot.getAttribute('name'));
331
+
332
+ // Base: the slice in PHYSICAL order, minus op-touched nodes (re-anchored
333
+ // below or op-removed). A physical node unknown to the record is a true
334
+ // addition (renderer hole / library write) and folds in at its physical
335
+ // position; when the container is a NAMED slot and the node's own
336
+ // attribute would key it elsewhere, remember the container's key as the
337
+ // node's ADOPTED key so repartition does not teleport it out of the
338
+ // container it was written into.
339
+ const merged = physical.filter((n) => !pendingNodes.has(n));
340
+ for (const n of merged) {
341
+ if (a.indexOf(n) !== -1) continue; // known to the record already
342
+ if (effectiveKeyOf(state, n) !== key) {
343
+ if (!state.adoptedKey) state.adoptedKey = new WeakMap();
344
+ state.adoptedKey.set(n, key);
345
+ }
346
+ }
347
+
348
+ // Re-anchor every record node of this slice that is not already in the
349
+ // base, in record order, each after the LAST base member that precedes
350
+ // it in the record (start when none). Covers op-inserted/moved nodes and
351
+ // record nodes missing from the slot (prune settles those right after).
352
+ for (const n of a) {
353
+ if (merged.indexOf(n) !== -1) continue;
354
+ if (effectiveKeyOf(state, n) !== key) continue;
355
+ const nIdx = a.indexOf(n);
356
+ let at = 0;
357
+ for (let i = 0; i < merged.length; i++) {
358
+ const mIdx = a.indexOf(merged[i]);
359
+ if (mIdx !== -1 && mIdx < nIdx) at = i + 1;
360
+ }
361
+ merged.splice(at, 0, n);
362
+ }
363
+
364
+ // Replace the old slice with the merged one at the old slice's position.
365
+ const involved = new Set(merged);
366
+ for (const n of snapshot) involved.add(n);
367
+ let at = -1;
368
+ let seen = 0;
369
+ for (const n of a) {
370
+ if (involved.has(n)) {
371
+ if (at === -1) at = seen;
372
+ continue;
373
+ }
374
+ seen += 1;
375
+ }
376
+ const rest = a.filter((n) => !involved.has(n));
377
+ if (at === -1 || at > rest.length) at = rest.length;
378
+ rest.splice(at, 0, ...merged);
379
+ state.authored = rest;
380
+ // The snapshot itself is settled by the placement step this pass.
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Prune `authored` of nodes the author detached out from under the record: a
386
+ * node whose parent is neither one of the host's own actual slots nor the park,
387
+ * and which the framework did not itself detach (capture / teardown rescue mark
388
+ * such nodes so they survive the parentless window before (re)placement). This
389
+ * closes the zombie-child resurrection (`el.remove()` on a projected node) and
390
+ * cross-host theft: the ownership question is answered structurally by the
391
+ * node's real parent, never by stale bookkeeping.
392
+ *
393
+ * @param {Element} host
394
+ * @param {SlotState} state
395
+ */
396
+ function pruneAuthored(host, state) {
397
+ const park = /** @type {any} */ (host)[PARK];
398
+ state.authored = state.authored.filter((n) => {
399
+ if (FRAMEWORK_DETACHED.has(n)) return true;
400
+ const p = n.parentNode;
401
+ if (p == null) return false;
402
+ // A DIRECT host child is still ours: a raw bypass move pulled it out of
403
+ // its slot onto the host (native: a host child stays assigned), and the
404
+ // physically-verifying placement step re-places it this same pass.
405
+ if (p === host) return true;
406
+ if (p === park) return true;
407
+ // The slot-parent keep requires the slot to be recognizably OURS.
408
+ // lastSnapshot membership is DIRECT proof this host applied that slot,
409
+ // so it stands alone (covering our own detached slots: a cache()-stashed
410
+ // branch, a torn-down conditional, or a FORWARDED slot the re-render
411
+ // detached, whose structural isOwnSlot walk would wrongly hit the child
412
+ // component between it and this host). A merely-CONTAINED slot still
413
+ // needs isOwnSlot, so the apply never steals a node back from an
414
+ // unrelated component's torn-down slot the author moved it into (the
415
+ // bare isOwnSlot walk is vacuously true on any fully detached chain).
416
+ if (
417
+ p.nodeType === 1 &&
418
+ /** @type {Element} */ (p).tagName === 'SLOT' &&
419
+ /** @type {Element} */ (p).hasAttribute(LIGHT_SLOT_ATTR) &&
420
+ (state.lastSnapshot.has(/** @type {HTMLSlotElement} */ (p)) ||
421
+ (host.contains(p) && isOwnSlot(host, /** @type {Element} */ (p))))
422
+ ) {
423
+ return true;
424
+ }
425
+ return false;
426
+ });
427
+ // The manual-assignment overlay needs no pruning here: it holds nodes via
428
+ // WeakRef (native holds manually assigned slottables weakly), so a removed
429
+ // node is not leaked, and an entry for a node assigned BEFORE it becomes a
430
+ // host child stays honoured when the node is later appended (the native
431
+ // assign-first, append-later ordering).
432
+ }