pane-wrapper: a pending link is also retried by a bounded rAF with an observer present, and by every settled scroll

From the 5B: an observer fires only when the COLUMN resizes. A pane whose height arrives later (styles adopted late
in a shadow root) would have left the link pending, and a pending link blocks the settle's write. check:view-address
covers it: a fresh load whose observer never fires again still opens the link (43 poisons).
This commit is contained in:
Doxa Maps 2026-10-03 06:42:17 +00:00
parent 2b62cc0120
commit 6f94ff0e30
2 changed files with 24 additions and 2 deletions

View file

@ -296,6 +296,24 @@ async function arms({ va, sv, counts, pw, src }, { skipF = false } = {}) {
c.destroy()
delete globalThis.ResizeObserver
}
if (!skipF) {
// The same fresh load, where the observer NEVER fires again after layout (a pane's height arrived after the
// column's last resize): the bounded rAF loop must still open the link.
const ros = []
globalThis.ResizeObserver = class { constructor(cb) { this.cb = cb; ros.push(this) } observe() {} disconnect() { this.off = true } }
globalThis.requestAnimationFrame = (cb) => setTimeout(cb, 5)
const b = browser('https://x.test/upg-100-mfe?pane=dashboard')
const host = new FakeEl()
FakeEl.h0 = 0
const c = pw.buildPaneColumn(SPEC, { host, address: { owner: () => 'app' } })
FakeEl.h0 = undefined
const col = host.children[0]
col.clientHeight = 100; await settle()
if (col.scrollTop !== 100) fails.push(`F: with an observer that never fires again, the bounded rAF did not open the link (scrollTop ${col.scrollTop})`)
if (b.writes.push + b.writes.replace) fails.push('F: the rAF-opened link wrote the address')
c.destroy()
delete globalThis.ResizeObserver; delete globalThis.requestAnimationFrame
}
if (!skipF) {
// A KEPT-ALIVE column whose tab is hidden and shown again (ResizeObserver: no height, then height).
const ros = []
@ -442,6 +460,7 @@ if (process.argv.includes('--self-test')) {
['pane-wrapper writing on Back (the pane it is leaving)', { ...real, pw: await pwVariant("show(known(pane) ? pane : column[0].id, 'smooth')", "(claimPane(current(), true), show(known(pane) ? pane : column[0].id, 'smooth'))") }, /^F: Back did not show the address's pane, or it WROTE/],
['pane-wrapper opening a link at build time only (before the column has a layout)', { ...real, pw: await pwVariant("let pending = known(linked) && linked !== column[0].id ? linked : ''", "let pending = ''; if (known(linked)) show(linked, 'instant')") }, /^F: a FRESH LOAD with \?pane=dashboard did not open/],
['pane-wrapper whose settle overwrites a link still waiting to open', { ...real, pw: await pwVariant('if (col.clientHeight && !pending) claimPane(current(), false)', 'if (!pending || true) claimPane(current(), false)') }, /^F: before its layout, the column overwrote the link/],
['pane-wrapper retrying a pending link by rAF only when there is no observer', { ...real, pw: await pwVariant("if (pending && typeof requestAnimationFrame !== 'undefined') {", "if (pending && typeof ResizeObserver === 'undefined' && typeof requestAnimationFrame !== 'undefined') {") }, /^F: with an observer that never fires again/],
['pane-wrapper ignoring a link', { ...real, pw: await pwVariant("let pending = known(linked) && linked !== column[0].id ? linked : ''", "let pending = ''") }, /^F: a link to \?pane=dashboard did not open/],
['pane-wrapper writing without the opt-in', { ...real, pw: await pwVariant("=> address && claimView({ pane: id === column[0].id ? '' : id, ...changes }, { owner: address.owner(),", "=> claimView({ pane: id === column[0].id ? '' : id, ...changes }, { owner: address ? address.owner() : 'app',") }, /^F: a column WITHOUT the address opt-in/],
['ownAddress that lets a nested element switch the tab', { ...real, va: { ...va, ownAddress: (el, g) => { const h = (e) => { const o = g(); if (o) { e.stopPropagation(); va.writeViewKeys(e.detail.changes, { owner: o, el, push: e.detail.push }) } }; el.addEventListener(va.VIEW_EVENT, h); return () => el.removeEventListener(va.VIEW_EVENT, h) } } }, /^E: a nested announcement switched the OWNER's tab/],

View file

@ -201,7 +201,10 @@ export function buildPaneColumn(spec, { host, mountPane, address }) {
pending = ''
}
openLink()
if (pending && typeof ResizeObserver === 'undefined' && typeof requestAnimationFrame !== 'undefined') {
// Retried by the ResizeObserver below, by every settled scroll, and by a bounded rAF loop that runs even WITH an
// observer: the observer fires only when the COLUMN resizes, and a pane's height can arrive later (styles adopted
// late in a shadow root).
if (pending && typeof requestAnimationFrame !== 'undefined') {
let n = 0
const tick = () => { openLink(); if (pending && ++n < 120) requestAnimationFrame(tick) }
requestAnimationFrame(tick)
@ -210,7 +213,7 @@ export function buildPaneColumn(spec, { host, mountPane, address }) {
// step per scroll. A column with no height (a hidden tab) has no "where", and a link still waiting to open must
// not be overwritten, so neither writes.
let t = null
const settle = () => { if (t) clearTimeout(t); t = setTimeout(() => { if (col.clientHeight && !pending) claimPane(current(), false) }, 120) }
const settle = () => { if (t) clearTimeout(t); t = setTimeout(() => { openLink(); if (col.clientHeight && !pending) claimPane(current(), false) }, 120) }
col.addEventListener('scroll', settle, { passive: true })
offs.push(() => { if (t) clearTimeout(t); col.removeEventListener('scroll', settle) })
offs.push(onAddressChange(({ pane }) => show(known(pane) ? pane : column[0].id, 'smooth')))