Skip to content

fix(viewer): keep surface frames sized when two viewers share one realm#228

Open
benvinegar wants to merge 1 commit into
mainfrom
fix/surface-frame-registry-multi-instance
Open

fix(viewer): keep surface frames sized when two viewers share one realm#228
benvinegar wants to merge 1 commit into
mainfrom
fix/surface-frame-registry-multi-instance

Conversation

@benvinegar

Copy link
Copy Markdown
Member

Problem

An embedding host can mount two viewer instances into one JS realm at once — e.g. sideshow cloud's double-buffered in-place workspace switch keeps the outgoing viewer mounted (cross-faded) until the incoming one is ready. The engine loads as a single cached ES module, so both instances share the viewer's module scope.

Two module-level singletons weren't safe under that:

  1. cardEls (the surface-frame registry that sizes iframes) was keyed by post id. Both instances route to the same URL and render a Card for the same post, so they clobbered each other's entry — and when the outgoing instance tore down, its cardEls.delete(id) removed the entry the still-visible instance needed. frameForSource then returned null, the surface's resize postMessages were dropped, and the frames stayed at their seed height until a full reload.
  2. The bridge message listener was one module-level function added/removed per mount. addEventListener(sameRef) dedups to a single listener, so the first teardown's removeEventListener tore it out from under the survivor.

Downstream symptom (sideshow cloud): clicking a post in the org/multiuser feed crossed into another member's workspace and left every surface iframe stuck at ~120px until a hard refresh.

Fix

  • Token-key cardEls: each Card registers under a unique token, not the post id. frameForSource already resolves by contentWindow (key-agnostic); a new cardForPost(id) handles the "new post" scroll-to-card pill.
  • Reference-count the bridge listener: alive while ≥1 viewer is mounted, removed only when the last unmounts. Single registration throughout, so no message is handled twice.

Self-hosted single-instance behavior is unchanged.

Testing

  • npm test (425) + typecheck + lint green.
  • Verified end-to-end from the sideshow cloud host: a new cross-workspace-switch e2e (deep-linking into another workspace's session) fails on the old engine (frame at 120px seed) and passes with this fix. Root cause was confirmed with instrumentation (surface posted {h:932} but frameForSource resolved false; the same post mounted twice / cleaned up once; cardEls emptied after promotion).

🤖 Generated with Claude Code

An embedding host can mount two viewer instances into one JS realm at once
(cross-fading an outgoing viewer into an incoming one during an in-place
navigation). The engine loads as a single shared module, so both instances
shared the module-level surface-frame registry (cardEls) and the single
message bridge listener.

Both instances route to the same URL and render a Card for the same post, so
keying cardEls by post id let them clobber each other's entry — and the
outgoing instance's teardown deleted the entry the still-visible instance
needed, dropping its surface resize messages so the frames stayed at their
seed height until a full reload. The shared listener had the same hazard:
the first teardown's removeEventListener tore it out from under the survivor.

Key cardEls by a per-Card token instead (frameForSource resolves by
contentWindow; new cardForPost handles the scroll-to-card pill), and
reference-count the bridge listener so it lives while any viewer is mounted.
Self-hosted single-instance behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant