frontend: guard hive-menu/hive-agent-menu connectedCallback against reconnect
Fixes hyperhive#2893: 'Element.attachShadow: Unable to re-attach to existing ShadowDOM', crashing swarm.js's live-update render path. A custom element's connectedCallback fires again on a same-document *move* (insertBefore/append repositioning an already-connected node runs the removal + insertion steps for its whole subtree), not just on a fresh mount. swarm.js's row-fingerprint cache reuses + reorders existing <li> subtrees on live updates -- reordering an unchanged, cached row moves its already-initialised <hive-agent-menu>/<hive-menu> without ever really detaching it from the document, so connectedCallback re-runs full setup on an instance that's already set up. attachShadow() throws unconditionally if the host already has a shadow root, and HiveAgentMenu's unconditional child-menu creation would have appended a second <hive-menu> on top of the first, doubling the dropdown, once the shadow-attach crash itself was out of the way. Both connectedCallbacks now bail early if already initialised (shadowRoot present / _menu already built). Reproduced the crash and duplicate-menu bug with an unguarded control copy of both files driven via headless Chromium (simulating the exact row-reorder move), then confirmed the guarded version throws nothing, keeps the same shadowRoot object identity across the move, and doesn't duplicate the dropdown.
This commit is contained in:
parent
d50bea588a
commit
38aa5f77f4
2 changed files with 17 additions and 0 deletions
|
|
@ -49,6 +49,13 @@ async function agentMenuPost(actionPath, name, body, graceful) {
|
|||
|
||||
class HiveAgentMenu extends HTMLElement {
|
||||
connectedCallback() {
|
||||
// Same reconnect-without-detach hazard as <hive-menu> (see its
|
||||
// connectedCallback comment) — swarm.js's row cache can move an
|
||||
// already-built <li> subtree without a real detach. Without this
|
||||
// guard a reconnect would append a second <hive-menu> on top of the
|
||||
// first, doubling the dropdown.
|
||||
if (this._menu) return;
|
||||
|
||||
const { c, forgeBase } = this._opts || {};
|
||||
|
||||
const btn = el('button', {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,16 @@ document.addEventListener('keydown', (e) => {
|
|||
|
||||
class HiveMenu extends HTMLElement {
|
||||
connectedCallback() {
|
||||
// A custom element's connectedCallback fires again on a same-document
|
||||
// *move* (insertBefore/append repositioning an already-connected node
|
||||
// runs the removal + insertion steps for its whole subtree), not just
|
||||
// on a fresh mount — swarm.js's row-fingerprint cache reuses/reorders
|
||||
// existing <li> subtrees, which can move an already-initialised
|
||||
// <hive-menu> without ever detaching it from the document. Guard
|
||||
// against re-running setup (attachShadow throws if the host already
|
||||
// has a shadow root) on that reconnect.
|
||||
if (this.shadowRoot) return;
|
||||
|
||||
const { trigger, content } = this._opts || {};
|
||||
const root = attachShadowCss(this, hiveMenuCss);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue