Skip to content

fix(ui-components): defer object URL revoke in panel downloads - #1031

Merged
EdwardMoyse merged 1 commit into
HSF:mainfrom
Shivansh1205:fix/defer-object-url-revoke-in-panels
Sep 16, 2026
Merged

EdwardMoyse merged 1 commit into
HSF:mainfrom
Shivansh1205:fix/defer-object-url-revoke-in-panels

Conversation

@Shivansh1205

@Shivansh1205 Shivansh1205 commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1030

Problem

a.click() does not perform a download, it schedules one. The browser fetches the
blob: URL afterwards, on its own timeline. Revoking the URL on the next line destroys
the handle immediately, so whether the export works is a race between the browser
starting its fetch and JavaScript tearing the blob down.

Three panels hand-roll the download and revoke synchronously:

Component Button
kinematics-panel-overlay Export TSV
masterclass-panel-overlay Export results
session-pill .phnxreplay download

The user clicks export and silently gets nothing — no file, no error, no console
message. Larger blobs lose the race more often, so long masterclass sessions and big
track collections are hit hardest.

Why this shape of fix

The repo already solved this once. saveFile defers its revoke to the next tick and
carries a comment explaining why (helpers/file.ts#L18-L23). These three panels never
call saveFile; they reimplement it and reintroduce the bug it fixed.

So the fix is to match the established pattern rather than invent one — defer the
revoke with setTimeout, exactly as saveFile does. The URL is still revoked, so
nothing leaks; it just happens once the browser holds its own reference.

The kinematics and masterclass anchors are additionally hidden (display: none) and
.remove()d after the click, again mirroring saveFile, so an unstyled anchor never
lands in the layout. session-pill already appended and removed its anchor, so only
its revoke changed.

I deliberately kept this to the revoke race and did not refactor the three call sites
onto saveFile itself — saveFile takes a string and these pass a Blob
(session-pill's comes straight from SessionManager), so unifying them is an API
change that does not belong in a bug fix.

Relationship to #992

#992 fixed saveFile for this exact race and stated these call sites "all revoke
correctly". They do revoke, which is why they were passed over, but they revoke in
the wrong task. This is the other half of that fix, not a new regression.

Tests

Added coverage for all three components, asserting the user-visible contract: the URL
is not revoked synchronously, and is revoked after the tick.

  • kinematics-panel-overlay.component.test.ts (new)
  • session-pill.component.test.ts (new)
  • masterclass-panel-overlay.component.test.ts (new export block appended)

Each suite also covers that no anchor is left in the document, and the masterclass and
session-pill suites cover the empty/no-blob early return.

Failing before this change: the "does not revoke the object URL synchronously"
case in the kinematics and masterclass suites. Verified by running the kinematics test
against unmodified code:

× does not revoke the object URL synchronously
  expect(jest.fn()).not.toHaveBeenCalled()
  Expected number of calls: 0
  Received number of calls: 1

The session-pill suite is new, so it has no before-state.

Components are instantiated via Object.create(Cls.prototype) with the few fields the
export method touches, matching the existing #923 tests in the masterclass file and
avoiding TestBed for methods that need no DI.

Verification

  • Full phoenix-ng suite: 64 suites / 230 tests passed, 0 failures.
  • eslint clean on all three component directories.
  • No pre-existing test failures were found, so none are being carried.
  • prettier --check warns on these files, but it warns identically on a clean tree —
    the checkout is CRLF and prettier normalises to LF. Not caused by this PR, and left
    alone to keep the diff to the bug.

`URL.createObjectURL` hands the browser a handle to the blob, and the anchor
click that follows starts the download asynchronously. Revoking the URL in the
same synchronous task can therefore tear the blob down before the browser has
taken its own reference, cancelling the download.

`saveFile` already defers its revoke to the next tick for exactly this reason,
with a comment saying so. Three UI panels hand-roll the same download instead of
calling it and revoke synchronously:

- kinematics panel "Export TSV"
- masterclass panel "Export results"
- session pill ".phnxreplay" download

The user clicks export and, depending on blob size and browser, either gets the
file or silently gets nothing. Larger exports fail more often, since a bigger
blob gives the browser a wider window in which to lose the race.

PR HSF#992 fixed `saveFile` and stated these call sites "all revoke correctly".
They do revoke, which is why they were passed over, but they revoke in the wrong
task - so the leak was fixed there while this race stayed live here.

Defer the revoke with `setTimeout` in all three, matching `saveFile`. The
kinematics and masterclass anchors are also hidden and removed after the click,
as `saveFile` does, so an unstyled anchor never lands in the layout.

Added tests asserting the URL is not revoked synchronously and is revoked after
the tick. The kinematics and masterclass "not revoked synchronously" cases fail
before this change; the session-pill suite is new.

Fixes HSF#1030

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚀 Preview deployed: http://phoenix-pr-1031.surge.sh

Built from 6f909a3.

@EdwardMoyse
EdwardMoyse merged commit beafa89 into HSF:main Sep 16, 2026
6 checks passed

This branch was successfully deployed

1 active deployment
pull-request — 6f909a32 Deployed Sep 14, 2026 by github-actions[bot]
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.

Export downloads can be cancelled: blob URL revoked synchronously in kinematics, masterclass and session-pill panels

2 participants