aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 02:09:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 02:09:52 +0200
commit7e480254a014a3e72815e8b971d5560d42872c5a (patch)
tree82fc06461330cf247a9c0c9370d0ccbf5e465e80 /packages/meshbay-hub/src/meshbay_hub
parentd6c4808d9a3ef6bc740cda7892508f15ee9ea030 (diff)
downloadmeshbay-7e480254a014a3e72815e8b971d5560d42872c5a.tar.gz
fix(spa): the target queue must not be able to freeze a batch
Four downloads on Firefox all sat at "preparing", with the node journal showing `d=0/8(q0) u=0/8(q0)` — not one transfer opened, so nothing had got past the client's target opening. Serialising those openings was new in d6c4808, and on Firefox it regressed what had always worked: four openings that ran at the same time began waiting on the slowest. `_targetQueue` is module-level and never reset, so an opening that never settles leaves the page unable to start any download again until it is reloaded. Two bounds, both narrowings of the queue rather than of any capability: Only an opening that could actually show a dialog joins it. Firefox and Safari have no `showSaveFilePicker` at all, so nothing there can race anything and the queue bought nothing while costing everything; they now bypass it entirely, which restores the previous behaviour by construction rather than by tuning. And no opening waits behind another for longer than TARGET_QUEUE_BUDGET_MS (90s) — generous enough never to cut in front of a real dialog, finite because the alternative is a download panel that only a reload can fix. Releasing early is safe: whatever is ahead is still the only unbatched opening, so the released one takes the streamed path and opens no second dialog. Measured on Firefox 154 against the deployed hub before writing any of this: `register` and `ready` return instantly, the page is controlled, and four serialised openings are served in 5-18 ms. The streamed path was never the delay; the queue was. Both new cases were checked against the unfixed source: without the bypass the peak concurrency is 1 instead of 4, and without the budget the stuck-opening case hangs. Hub suite 826 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/file-utils.js37
1 files changed, 36 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
index 065079b..ef074dd 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
@@ -218,16 +218,42 @@ async function _openDownloadTarget(filename, size = 0, pickerOpts = {},
//
// So the queue is explicit now, and it is the *targets* that queue, not the
// rows: every download still appears the moment it is asked for.
+//
+// Two things keep the queue from becoming the problem it was meant to solve.
+// It only ever holds openings that could actually put a dialog on screen, and
+// no opening waits behind another for longer than a budget.
let _targetQueue = Promise.resolve();
let _targetsInFlight = 0;
+// How long an opening waits for the one ahead of it before going anyway.
+//
+// A queue with no bound is a way for one stuck opening to freeze every later
+// download for the life of the page, since `_targetQueue` is never reset. That
+// is what turned a slow first download into four rows stuck at "preparing" on
+// Firefox. Generous, because a dialog legitimately waits for a person and
+// cutting in front of one would be worse than waiting; finite, because the
+// alternative is a download panel that never recovers.
+//
+// Going anyway is safe: whatever was ahead is still the only unbatched opening,
+// so the one released here takes the streamed path and opens no second dialog.
+const TARGET_QUEUE_BUDGET_MS = 90000;
+
function _openTargetInTurn(filename, size, pickerOpts, swSize) {
+ // Only an opening that could show a dialog has any reason to wait. Firefox
+ // and Safari have no `showSaveFilePicker` at all, so nothing there can race
+ // anything, and queueing them bought nothing while costing everything: four
+ // downloads that used to open their targets at the same time became four
+ // that waited on the slowest.
+ const canPick = typeof window !== 'undefined'
+ && typeof window.showSaveFilePicker === 'function';
+ if (!canPick) return _openDownloadTarget(filename, size, pickerOpts, swSize);
+
// Anything that has to wait its turn is, by definition, not the first of the
// batch — so it will not be the one holding the user's gesture.
const batched = _targetsInFlight > 0;
_targetsInFlight += 1;
- const mine = _targetQueue
+ const mine = _waitBriefly(_targetQueue, TARGET_QUEUE_BUDGET_MS)
.then(() => _openDownloadTarget(filename, size, pickerOpts, swSize,
{ batched }))
.finally(() => { _targetsInFlight -= 1; });
@@ -237,6 +263,15 @@ function _openTargetInTurn(filename, size, pickerOpts, swSize) {
return mine;
}
+/** Settles with `promise`, or after `ms`, whichever comes first. */
+function _waitBriefly(promise, ms) {
+ return new Promise((resolve) => {
+ const timer = setTimeout(resolve, ms);
+ promise.then(() => { clearTimeout(timer); resolve(); },
+ () => { clearTimeout(timer); resolve(); });
+ });
+}
+
/** The download of last resort, for browsers with no way to stream to disk. */
function _saveBlob(blob, filename) {
const url = URL.createObjectURL(blob);