summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 10:05:30 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 10:05:30 +0200
commitfc148e185c01b2e25361c7625a67d310d2e1d288 (patch)
tree63d5b8f290a424cec235c47f55e42c290b4a3e46 /packages/meshbay-hub/src/meshbay_hub
parent7000082b748bf608484f94d30d63b2c6b83e6814 (diff)
downloadmeshbay-fc148e185c01b2e25361c7625a67d310d2e1d288.tar.gz
fix(spa): repair a page the download worker cannot serve
Downloads on Firefox failed with "the worker did not answer the download within 15s", every time, for one operator, while the same profile driven from here succeeded every time. Their own test sequence found it: a freshly started browser downloaded four files out of four, twice; one Ctrl+F5 and every attempt afterwards failed; restart, fine again; Ctrl+F5 before any attempt and the very first one failed. A document fetched by a hard reload is loaded with the service worker bypassed. It can still be claimed afterwards, so `navigator.serviceWorker.controller` comes back and every check in `_claimController` passes — but the navigations that document starts keep missing the worker, and the hidden iframe a streamed download needs is a navigation. On Firefox and Safari that is the only way to write a file too large to hold in memory, so the download cannot happen at all, for the life of the page. Being controlled is not being servable, so priming now asks the question directly instead of inferring it: a four-byte stream and a hidden iframe, exactly as a real download would, torn down completely so nothing lands in the download folder. When it goes unanswered the page reloads once, ordinarily, which puts it back under the worker. The flag lives in sessionStorage rather than a variable because it has to survive the reload it triggers, and because a page that is still unservable afterwards must stop rather than loop. Also stops telling people to change browser. The message said "use the desktop app, or Chrome or Edge" for a state an ordinary reload undoes, on the one path Firefox has no alternative to; all ten catalogues now say to reload first. The hard reloads were on my instruction: the SPA's HTML is served `no-store`, so a plain reload has always picked up a new build and Ctrl+F5 was never needed. Hub suite 834 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/downloads.js90
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js2
11 files changed, 98 insertions, 12 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js
index 7e4802a..b1eade6 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js
@@ -200,6 +200,8 @@ export const BLOB_LIMIT = 512 * 1024 * 1024;
// ── Streaming to disk without the File System Access API ────────────────────
const SW_PATH = '/sw.js';
+// Kept in step with sw.js's own PREFIX.
+const PREFIX_PATH = '/_mbdl/';
// On Firefox and Safari this worker is not a nicety, it is the only unbounded
// way to write a download to disk: the File System Access API does not exist
@@ -365,7 +367,91 @@ async function _claimController(budgetMs) {
*/
export function primeServiceWorker() {
if (!STREAMS_VIA_SW) return;
- serviceWorker().catch(() => {});
+ serviceWorker().then((worker) => worker && _repairIfBypassed(worker))
+ .catch(() => {});
+}
+
+// How long to wait for the worker to answer the one-byte self-test below.
+// Milliseconds when it works; a page that cannot stream at all is worth four
+// seconds to find out about, once, at boot.
+const SELF_TEST_BUDGET_MS = 4000;
+// Set for the life of this tab, so the repair below can happen at most once and
+// can never become a reload loop.
+const REPAIRED_KEY = 'meshbay.sw-repaired';
+
+/**
+ * Can this document actually have a download served, or only talk to the worker?
+ *
+ * Being controlled is not the same thing, and the gap between them is a real
+ * failure people hit. A document fetched by a **hard** reload — Ctrl+F5,
+ * Ctrl+Shift+R — is loaded with the service worker bypassed. It can still be
+ * claimed afterwards, so `navigator.serviceWorker.controller` comes back and
+ * every check in `_claimController` passes; but the navigations that document
+ * starts keep missing the worker, and the hidden iframe a streamed download
+ * needs *is* a navigation. Every download then fails with "the worker did not
+ * answer", for the life of that page — on Firefox and Safari, the only path
+ * there is for a file too large to hold in memory.
+ *
+ * Reported after an operator was told, by this author, to hard-reload after
+ * each deployment: four downloads out of four worked on a freshly started
+ * browser, and the first attempt after a Ctrl+F5 failed, every time.
+ *
+ * This asks the question directly rather than inferring it: a four-byte stream
+ * and a hidden iframe, exactly as a real download would.
+ */
+async function _canServeDownloads(worker) {
+ const id = `selftest-${Math.random().toString(36).slice(2, 10)}`;
+ let readable, writable;
+ try {
+ ({ readable, writable } = new TransformStream());
+ } catch {
+ return true; // No transferable streams: a different failure, not this one.
+ }
+ const chan = new MessageChannel();
+ const serving = new Promise((resolve) => {
+ chan.port1.onmessage = (e) => {
+ if (e.data && e.data.type === 'mbdl-serving') resolve(true);
+ };
+ });
+ try {
+ worker.postMessage({ type: 'mbdl', id, filename: 'meshbay-selftest.bin',
+ size: 4, readable, port: chan.port2 },
+ [readable, chan.port2]);
+ } catch {
+ return true; // Same: not the bypass this is looking for.
+ }
+ const frame = document.createElement('iframe');
+ frame.hidden = true;
+ frame.src = `${PREFIX_PATH}${id}`;
+ document.body.appendChild(frame);
+ const served = await Promise.race([
+ serving,
+ new Promise((r) => setTimeout(() => r(false), SELF_TEST_BUDGET_MS)),
+ ]);
+ frame.remove();
+ try { chan.port1.close(); } catch { /* already gone */ }
+ // Never completed, so the browser has nothing to save and no file appears.
+ try { await writable.abort('self-test'); } catch { /* already gone */ }
+ return served;
+}
+
+/**
+ * An ordinary reload puts the document back under the worker, so do that once.
+ *
+ * Only at boot, where nothing is in flight and the reload costs a flicker.
+ * Guarded by a session flag rather than a variable: the point is to survive the
+ * reload it triggers, and to stop rather than loop if reloading does not help.
+ */
+async function _repairIfBypassed(worker) {
+ let repaired = false;
+ try { repaired = sessionStorage.getItem(REPAIRED_KEY) === '1'; } catch { /* blocked */ }
+ if (repaired) return;
+ if (await _canServeDownloads(worker)) return;
+ console.warn('[MeshBay] this page cannot be served by the download worker — '
+ + 'reloading once to put it back under the worker\u2019s control '
+ + '(a hard reload leaves a page in this state)');
+ try { sessionStorage.setItem(REPAIRED_KEY, '1'); } catch { /* blocked */ }
+ location.reload();
}
async function serviceWorker(controlMs = SW_CONTROL_BUDGET_MS) {
@@ -458,7 +544,7 @@ async function _attemptStreamedDownload(filename, size, attempt,
const frame = document.createElement('iframe');
frame.hidden = true;
- frame.src = `/_mbdl/${id}`;
+ frame.src = `${PREFIX_PATH}${id}`;
document.body.appendChild(frame);
const answered = await Promise.race([
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 1458cf2..f0ec776 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -219,7 +219,7 @@ export default {
'preview.pdf_fallback': 'Dieser Browser zeigt das PDF nicht direkt an. Laden Sie es '
+ 'stattdessen herunter — entschlüsselt wurde es ohnehin hier.',
'preview.too_large': 'Diese Datei ist {size} groß, mehr als diese Seite im Arbeitsspeicher halten kann ({limit}). Laden Sie sie stattdessen herunter — ein Download wird direkt auf die Festplatte geschrieben.',
- 'download.too_large_for_memory': '„{name}“ ist {size} groß. Dieser Browser kann eine Datei dieser Größe nur speichern, indem er sie direkt auf die Festplatte schreibt, und das ist hier nicht möglich — er müsste die ganze Datei im Arbeitsspeicher halten. Verwenden Sie die Desktop-App oder Chrome bzw. Edge.',
+ 'download.too_large_for_memory': '„{name}“ ist {size} groß. Dieser Browser kann eine Datei dieser Größe nur speichern, indem er sie direkt auf die Festplatte schreibt, und das ist hier nicht möglich — er müsste die ganze Datei im Arbeitsspeicher halten. Laden Sie die Seite neu und versuchen Sie es erneut; falls das nicht hilft, verwenden Sie die Desktop-App.',
'group.upload_indexing': 'wird indiziert …',
'video.err_transport': 'Transport nicht verbunden',
'video.err_mse': 'Codec wird für das Streaming nicht unterstützt: {codec}',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index b0430c0..c065b8a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -217,7 +217,7 @@ export default {
'video.close': 'Close (Esc)',
'preview.pdf_fallback': 'This browser will not display the PDF inline. Download it instead — it was decrypted here either way.',
'preview.too_large': 'This file is {size}, more than this page can hold in memory ({limit}). Download it instead — a download is written straight to disk.',
- 'download.too_large_for_memory': '"{name}" is {size}. This browser can only save a file that large by streaming it to disk, and it has no way to do that here — it would have to hold the whole file in memory. Use the desktop app, or Chrome or Edge.',
+ 'download.too_large_for_memory': '"{name}" is {size}. This browser can only save a file that large by streaming it to disk, and it has no way to do that here — it would have to hold the whole file in memory. Reload the page and try again; if that does not help, use the desktop app.',
'group.upload_indexing': 'indexing…',
'video.err_transport': 'Transport not connected',
'video.err_mse': 'Codec not supported for streaming: {codec}',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index adfb1cc..f655bc0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -217,7 +217,7 @@ export default {
'preview.pdf_fallback': 'Este navegador no mostrará el PDF integrado. Descárguelo '
+ 'en su lugar — en cualquier caso se descifró aquí.',
'preview.too_large': 'Este archivo ocupa {size}, más de lo que esta página puede mantener en memoria ({limit}). Descárguelo en su lugar — una descarga se escribe directamente en disco.',
- 'download.too_large_for_memory': '«{name}» ocupa {size}. Este navegador solo puede guardar un archivo así transmitiéndolo al disco, y aquí no puede hacerlo — tendría que mantener el archivo entero en memoria. Use la aplicación de escritorio, o Chrome o Edge.',
+ 'download.too_large_for_memory': '«{name}» ocupa {size}. Este navegador solo puede guardar un archivo así transmitiéndolo al disco, y aquí no puede hacerlo — tendría que mantener el archivo entero en memoria. Recargue la página e inténtelo de nuevo; si eso no ayuda, use la aplicación de escritorio.',
'group.upload_indexing': 'indexando…',
'video.err_transport': 'Transporte no conectado',
'video.err_mse': 'Códec no compatible con la reproducción en continuo: {codec}',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index b409927..e011873 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -218,7 +218,7 @@ export default {
'preview.pdf_fallback': 'Ce navigateur n’affichera pas le PDF directement. '
+ 'Téléchargez-le plutôt — il a été déchiffré ici dans les deux cas.',
'preview.too_large': 'Ce fichier fait {size}, plus que cette page ne peut garder en mémoire ({limit}). Téléchargez-le plutôt — un téléchargement est écrit directement sur le disque.',
- 'download.too_large_for_memory': '« {name} » fait {size}. Ce navigateur ne peut enregistrer un fichier de cette taille qu\'en l\'écrivant au fil de l\'eau sur le disque, ce qu\'il ne peut pas faire ici — il devrait garder le fichier entier en mémoire. Utilisez l\'application de bureau, ou Chrome ou Edge.',
+ 'download.too_large_for_memory': '« {name} » fait {size}. Ce navigateur ne peut enregistrer un fichier de cette taille qu\'en l\'écrivant au fil de l\'eau sur le disque, ce qu\'il ne peut pas faire ici — il devrait garder le fichier entier en mémoire. Rechargez la page et réessayez ; si cela ne suffit pas, utilisez l\'application de bureau.',
'group.upload_indexing': 'indexation…',
'video.err_transport': 'Transport non connecté',
'video.err_mse': 'Codec non pris en charge pour la diffusion : {codec}',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 7e6246e..80e3c17 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -218,7 +218,7 @@ export default {
'preview.pdf_fallback': 'Questo browser non mostrerà il PDF nella pagina. Lo scarichi '
+ 'invece — in ogni caso è stato decifrato qui.',
'preview.too_large': 'Questo file è di {size}, più di quanto questa pagina possa tenere in memoria ({limit}). Lo scarichi invece — un download viene scritto direttamente su disco.',
- 'download.too_large_for_memory': '«{name}» è di {size}. Questo browser può salvare un file di queste dimensioni solo scrivendolo su disco man mano, e qui non può farlo — dovrebbe tenere l’intero file in memoria. Usi l’applicazione desktop, oppure Chrome o Edge.',
+ 'download.too_large_for_memory': '«{name}» è di {size}. Questo browser può salvare un file di queste dimensioni solo scrivendolo su disco man mano, e qui non può farlo — dovrebbe tenere l’intero file in memoria. Ricarichi la pagina e riprovi; se non basta, usi l’applicazione desktop.',
'group.upload_indexing': 'indicizzazione…',
'video.err_transport': 'Trasporto non connesso',
'video.err_mse': 'Codec non supportato per lo streaming: {codec}',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index a74eec9..5dbb4fa 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -215,7 +215,7 @@ export default {
'preview.pdf_fallback': 'このブラウザーはページ内に PDF を表示しません。'
+ 'ダウンロードしてご覧ください。いずれにせよ復号はここで行われています。',
'preview.too_large': 'このファイルは {size} で、このページがメモリに保持できる上限({limit})を超えています。代わりにダウンロードしてください。ダウンロードはディスクに直接書き込まれます。',
- 'download.too_large_for_memory': '「{name}」は {size} です。このブラウザーでこの大きさのファイルを保存するにはディスクへ逐次書き出すしかありませんが、ここではそれができません — ファイル全体をメモリに保持することになります。デスクトップアプリ、または Chrome か Edge をお使いください。',
+ 'download.too_large_for_memory': '「{name}」は {size} です。このブラウザーでこの大きさのファイルを保存するにはディスクへ逐次書き出すしかありませんが、ここではそれができません — ファイル全体をメモリに保持することになります。ページを再読み込みしてもう一度お試しください。解決しない場合はデスクトップアプリをお使いください。',
'group.upload_indexing': 'インデックスを作成中…',
'video.err_transport': 'トランスポートが接続されていません',
'video.err_mse': 'ストリーミング再生に対応していないコーデックです:{codec}',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index a7d64f7..f9c7cf3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -219,7 +219,7 @@ export default {
'preview.pdf_fallback': 'Deze browser toont de PDF niet in de pagina zelf. Download '
+ 'hem in plaats daarvan — ontsleuteld werd hij hoe dan ook hier.',
'preview.too_large': 'Dit bestand is {size}, meer dan deze pagina in het geheugen kan houden ({limit}). Download het in plaats daarvan — een download wordt rechtstreeks naar schijf geschreven.',
- 'download.too_large_for_memory': '“{name}” is {size}. Deze browser kan een bestand van die omvang alleen opslaan door het meteen naar schijf te schrijven, en dat kan hier niet — het hele bestand zou in het geheugen moeten. Gebruik de desktop-app, of Chrome of Edge.',
+ 'download.too_large_for_memory': '“{name}” is {size}. Deze browser kan een bestand van die omvang alleen opslaan door het meteen naar schijf te schrijven, en dat kan hier niet — het hele bestand zou in het geheugen moeten. Herlaad de pagina en probeer het opnieuw; als dat niet helpt, gebruik dan de desktop-app.',
'group.upload_indexing': 'indexeren…',
'video.err_transport': 'Transport niet verbonden',
'video.err_mse': 'Codec wordt niet ondersteund voor streamen: {codec}',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 5bd34fb..b26f3cb 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -224,7 +224,7 @@ export default {
'preview.pdf_fallback': 'Ta przeglądarka nie wyświetli pliku PDF na stronie. Proszę '
+ 'go pobrać — i tak został odszyfrowany tutaj.',
'preview.too_large': 'Ten plik ma {size}, więcej niż ta strona może utrzymać w pamięci ({limit}). Proszę go zamiast tego pobrać — pobieranie jest zapisywane wprost na dysk.',
- 'download.too_large_for_memory': '„{name}” ma {size}. Ta przeglądarka może zapisać plik tej wielkości tylko strumieniowo na dysk, a tutaj nie ma takiej możliwości — musiałaby utrzymać cały plik w pamięci. Proszę użyć aplikacji desktopowej albo przeglądarki Chrome lub Edge.',
+ 'download.too_large_for_memory': '„{name}” ma {size}. Ta przeglądarka może zapisać plik tej wielkości tylko strumieniowo na dysk, a tutaj nie ma takiej możliwości — musiałaby utrzymać cały plik w pamięci. Proszę odświeżyć stronę i spróbować ponownie; jeśli to nie pomoże, proszę użyć aplikacji desktopowej.',
'group.upload_indexing': 'indeksowanie…',
'video.err_transport': 'Transport nie jest połączony',
'video.err_mse': 'Kodek nieobsługiwany przy odtwarzaniu strumieniowym: {codec}',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index 102ec92..db5a408 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -219,7 +219,7 @@ export default {
'preview.pdf_fallback': 'Este navegador não exibirá o PDF na própria página. Baixe '
+ 'o arquivo — de todo modo ele foi descriptografado aqui.',
'preview.too_large': 'Este arquivo tem {size}, mais do que esta página consegue manter na memória ({limit}). Baixe-o em vez disso — um download é gravado direto no disco.',
- 'download.too_large_for_memory': '"{name}" tem {size}. Este navegador só consegue salvar um arquivo desse tamanho gravando-o direto no disco, e aqui ele não tem como — precisaria manter o arquivo inteiro na memória. Use o aplicativo para computador, ou Chrome ou Edge.',
+ 'download.too_large_for_memory': '"{name}" tem {size}. Este navegador só consegue salvar um arquivo desse tamanho gravando-o direto no disco, e aqui ele não tem como — precisaria manter o arquivo inteiro na memória. Recarregue a página e tente novamente; se não resolver, use o aplicativo para computador.',
'group.upload_indexing': 'indexando…',
'video.err_transport': 'Transporte não conectado',
'video.err_mse': 'Codec sem suporte para transmissão: {codec}',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 6b64785..ba88f4e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -212,7 +212,7 @@ export default {
'video.close': '关闭(Esc)',
'preview.pdf_fallback': '此浏览器不会在页面内显示该 PDF。请改为下载——无论如何它都已在本地解密。',
'preview.too_large': '该文件为 {size},超出本页面可在内存中保存的上限({limit})。请改为下载——下载会直接写入磁盘。',
- 'download.too_large_for_memory': '“{name}”为 {size}。此浏览器只能通过边下边写入磁盘来保存这么大的文件,而这里无法做到——它将不得不把整个文件放在内存中。请使用桌面应用,或 Chrome、Edge。',
+ 'download.too_large_for_memory': '“{name}”为 {size}。此浏览器只能通过边下边写入磁盘来保存这么大的文件,而这里无法做到——它将不得不把整个文件放在内存中。请重新加载页面后重试;如果仍然无效,请使用桌面应用。',
'group.upload_indexing': '建立索引中…',
'video.err_transport': '传输未连接',
'video.err_mse': '该编解码器不支持流式播放:{codec}',