diff --git a/frontend/app.js b/frontend/app.js index 869ed00..24bdb0a 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -8690,11 +8690,11 @@ async function applyUpdate(target, { quiet = false } = {}) { // When the waiting worker already downloaded this build in the // background, the toast says so and nothing is re-downloaded. askStatus: (worker) => SU.askCacheStatus(worker, { timeoutMs: 4000, setTimeout: (fn, ms) => setTimeout(fn, ms) }), - onPhase: (phase) => { + onPhase: (phase, status) => { if (quiet) return; if (_updateToast) _updateToast.remove(); _updateToast = phase === 'ready' - ? toast('Applying the update…', { duration: 20000 }) + ? toast(status?.offlineReady === false ? `Applying the update… Offline cache is still preparing (${status.cached}/${status.total}).` : 'Applying the update…', { duration: 20000 }) : toast('Downloading the update…', { duration: 305000 }); }, refreshShell: ('caches' in window) ? () => SU.refreshShellInPlace({ diff --git a/frontend/lazy.js b/frontend/lazy.js index 7bb4258..d37ee8e 100644 --- a/frontend/lazy.js +++ b/frontend/lazy.js @@ -100,9 +100,10 @@ else root.navigator.serviceWorker?.getRegistration().then(send).catch(()=>{}); }, warm() { - if(root.navigator.connection?.saveData || !root.navigator.serviceWorker) return; - const idle = root.requestIdleCallback || (fn => root.setTimeout(fn,1000)); - idle(()=>root.navigator.serviceWorker.ready.then(reg=>reg.active?.postMessage({type:'WARM_ASSETS',saveData:!!root.navigator.connection?.saveData})).catch(()=>{})); + if (!root.navigator.serviceWorker || root.navigator.onLine === false) return; + root.navigator.serviceWorker.ready.then(reg => { + for (const worker of new Set([reg.active, reg.waiting])) worker?.postMessage({type:'COMPLETE_ASSETS'}); + }).catch(()=>{}); }, onLoad(name, fn) { if(api.loaded(name)) { Promise.resolve().then(fn).catch(error=>{if(root.CustomEvent)root.dispatchEvent?.(new root.CustomEvent('ytp-lazy-error',{detail:{group:name,error}}));}); return; } @@ -128,6 +129,8 @@ if(event.data?.type==='SW_UPDATE_AVAILABLE') api.warm(); }); root.navigator.serviceWorker?.addEventListener('controllerchange',()=>{api.reportLayout();api.warm();}); + root.addEventListener?.('online',()=>api.warm()); + api.warm(); let settings={}; try { settings=JSON.parse(root.localStorage.getItem('_ytpdata') || '{}').settings || {}; api.layout=settings.layout || 'classic'; } catch {} Object.assign(doc.documentElement.dataset,{layout:api.layout,theme:settings.theme || 'dark',font:settings.fontScale || 'normal',density:settings.density || 'comfortable',perf:settings.perfMode?'on':'off',motion:settings.reduceMotion?'reduced':'full'}); diff --git a/frontend/lazy.test.js b/frontend/lazy.test.js index b084413..454a37b 100644 --- a/frontend/lazy.test.js +++ b/frontend/lazy.test.js @@ -84,3 +84,14 @@ test('classic chunk registry proxies use window.YT and keep the executed singlet const singleton=root.YT.TestApi;await root.Lazy.load('feature:test'); assert.equal(root.YT.TestApi,singleton);assert.equal(inserted.length,2); }); + + +test('online launches request worker completion even with Save-Data and skip offline messages',async()=>{ + const f=fixture(),sent=[]; + const active={postMessage:m=>sent.push(m)},waiting={postMessage:m=>sent.push(m)}; + f.root.navigator.connection={saveData:true};f.root.navigator.onLine=true; + f.root.navigator.serviceWorker={ready:Promise.resolve({active,waiting})}; + f.root.Lazy.warm();await Promise.resolve(); + assert.deepEqual(sent.map(m=>m.type),['COMPLETE_ASSETS','COMPLETE_ASSETS']); + f.root.navigator.onLine=false;f.root.Lazy.warm();await Promise.resolve();assert.equal(sent.length,2); +}); diff --git a/frontend/sw-update.js b/frontend/sw-update.js index 974baa7..ec41569 100644 --- a/frontend/sw-update.js +++ b/frontend/sw-update.js @@ -250,14 +250,15 @@ // Opt-in: without an askStatus the download path runs exactly as before. // (It must never be implicit — a probe that waits on a reply would hang // forever if the caller injected a setTimeout that never fires.) - let precached = false; + let precached = false, offlineStatus; if (waiting && askStatus) { try { const status = await askStatus(waiting); precached = !!(status && status.ready); + offlineStatus = status; } catch { precached = false; } } - if (onPhase) onPhase(precached ? 'ready' : 'downloading'); + if (onPhase) onPhase(precached ? 'ready' : 'downloading', offlineStatus); if (!precached && refreshShell) await refreshShell(); if (!guard()) throw new Error('Playing - update after this song / when paused'); @@ -280,7 +281,25 @@ waiting.postMessage({ type: 'SKIP_WAITING' }); } - const SwUpdate = { applyUpdate, refreshShellInPlace, askCacheStatus, BUST_PARAM }; + function showOfflineStatus(status) { + const el = root.document?.getElementById('offlineAssetStatus'); + if (el && typeof status?.total === 'number') { + el.textContent = `${status.offlineReady ? 'Offline ready' : 'Preparing offline'} ${status.cached}/${status.total}`; + } else if (el && typeof status?.ready === 'boolean') { + el.textContent = status.ready ? 'Offline ready (legacy cache)' : 'Offline cache incomplete (legacy mode)'; + } + } + async function offlineStatus() { + const registration = await root.navigator?.serviceWorker?.getRegistration(); + const status = await askCacheStatus(registration?.active); + showOfflineStatus(status); + return status; + } + root.navigator?.serviceWorker?.addEventListener?.('message', event => { + if (event.data?.type === 'OFFLINE_STATUS') showOfflineStatus(event.data); + }); + + const SwUpdate = { offlineStatus, showOfflineStatus, applyUpdate, refreshShellInPlace, askCacheStatus, BUST_PARAM }; if (typeof module !== 'undefined' && module.exports) { module.exports = SwUpdate; diff --git a/frontend/sw-update.test.js b/frontend/sw-update.test.js index 6ea1ce1..aaed52b 100644 --- a/frontend/sw-update.test.js +++ b/frontend/sw-update.test.js @@ -368,3 +368,22 @@ test('an unanswered worker status cannot fall through to an empty legacy refresh vm.runInNewContext(fs.readFileSync(__dirname+'/sw-update.js','utf8'),sandbox); await assert.rejects(sandbox.SwUpdate.refreshShellInPlace({cachesApi:{open:async()=>({}),keys:async()=>[]},fetchFn:async()=>({ok:true,json:async()=>({buildTag:'new'})}),isShellCache:()=>true}),/Worker update status unavailable/); }); + + +test('update readiness retains completion progress without blocking the ready fast path',async()=>{ + const phases=[],container=fakeContainer();let downloaded=false; + const status={ready:true,offlineReady:false,cached:90,total:151,missingFiles:['/optional.js']}; + await applyUpdate({reg:{waiting:{postMessage(){}}},container,askStatus:async()=>status, + onPhase:(phase,progress)=>phases.push({phase,progress}),refreshShell:async()=>{downloaded=true;},setTimeout:()=>{}}); + assert.equal(downloaded,false);assert.equal(phases[0].phase,'ready');assert.deepStrictEqual(phases[0].progress,status); +}); +test('offline cache status reports preparing and complete counts in Settings',()=>{ + const vm=require('node:vm'),fs=require('node:fs'),element={}; + const sandbox={document:{getElementById:()=>element}}; + vm.runInNewContext(fs.readFileSync(__dirname+'/sw-update.js','utf8'),sandbox); + sandbox.SwUpdate.showOfflineStatus({offlineReady:false,cached:90,total:151}); + assert.equal(element.textContent,'Preparing offline 90/151'); + sandbox.SwUpdate.showOfflineStatus({offlineReady:true,cached:151,total:151}); + assert.equal(element.textContent,'Offline ready 151/151'); + sandbox.SwUpdate.showOfflineStatus({ready:true});assert.equal(element.textContent,'Offline ready (legacy cache)'); +}); diff --git a/frontend/views-core.js b/frontend/views-core.js index 314b18b..aba9471 100644 --- a/frontend/views-core.js +++ b/frontend/views-core.js @@ -843,6 +843,7 @@ async function renderSettings() {