Icon-only playlist header and filter-row actions, one-row transport, resumable update download
- Playlist title wraps within 90% of the width; rename and share are icons beside it, and tapping the title opens the rename box showing the full name - Offline, pin, select and delete are icon buttons to the right of the filter input - Transport is one row again: prev, back 10, play, forward 10, next - Update download stages each file as it arrives so a retry only fetches what is missing, keeps 4 requests in flight and allows 5 minutes
This commit is contained in:
@@ -33,6 +33,8 @@
|
||||
'use strict';
|
||||
|
||||
const BUST_PARAM = '__ytpfresh';
|
||||
const STAGING_PREFIX = 'ytp-staging-';
|
||||
const DEFAULT_CONCURRENCY = 4;
|
||||
|
||||
function withTimeout(promise, ms, setTimeoutFn, label) {
|
||||
if (!ms || !setTimeoutFn) return promise;
|
||||
@@ -44,9 +46,10 @@
|
||||
|
||||
/**
|
||||
* Re-downloads every file held by the versioned shell caches and replaces
|
||||
* the cached copies. All-or-nothing: if any download fails, nothing is
|
||||
* written and it throws, so a flaky connection can never leave a
|
||||
* half-old/half-new shell.
|
||||
* the cached copies. All-or-nothing: if any download fails, the live shell
|
||||
* is untouched and it throws, so a flaky connection can never leave a
|
||||
* half-old/half-new shell. Files that did arrive stay staged, so the next
|
||||
* try for the same build resumes instead of starting over.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {CacheStorage} opts.cachesApi
|
||||
@@ -56,12 +59,14 @@
|
||||
* @param {number} [opts.timeoutMs] overall download budget
|
||||
* @param {number} [opts.attempts] tries per file (default 3)
|
||||
* @param {number} [opts.attemptTimeoutMs] bound on a single try
|
||||
* @param {number} [opts.concurrency] downloads in flight at once (default 4)
|
||||
* @param {string} [opts.stagingKey] target build; staged files survive a failed try for it
|
||||
* @param {Function} [opts.setTimeout]
|
||||
* @returns {Promise<{refreshed: number, caches: number}>}
|
||||
* @returns {Promise<{refreshed: number, caches: number, resumed: number}>}
|
||||
*/
|
||||
async function refreshShellInPlace({ cachesApi, fetchFn, isShellCache, bust, timeoutMs, attempts, attemptTimeoutMs, setTimeout: setTimeoutFn }) {
|
||||
async function refreshShellInPlace({ cachesApi, fetchFn, isShellCache, bust, timeoutMs, attempts, attemptTimeoutMs, concurrency, stagingKey, setTimeout: setTimeoutFn }) {
|
||||
const names = (await cachesApi.keys()).filter(isShellCache);
|
||||
if (!names.length) return { refreshed: 0, caches: 0 }; // uncontrolled page: a reload already hits the network
|
||||
if (!names.length) return { refreshed: 0, caches: 0, resumed: 0 }; // uncontrolled page: a reload already hits the network
|
||||
|
||||
const urls = new Set();
|
||||
for (const name of names) {
|
||||
@@ -73,6 +78,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Resumable: every file lands in a staging cache the moment it arrives,
|
||||
// so a retry after a timeout only fetches what is still missing. The
|
||||
// staging cache is keyed by the target build — a newer deploy never
|
||||
// reuses files staged for an older one. Nothing touches the live shell
|
||||
// caches until the whole set is staged (still all-or-nothing).
|
||||
const stagingName = STAGING_PREFIX + (stagingKey || 'current');
|
||||
for (const k of await cachesApi.keys()) {
|
||||
if (k.startsWith(STAGING_PREFIX) && k !== stagingName) await cachesApi.delete(k);
|
||||
}
|
||||
const staging = await cachesApi.open(stagingName);
|
||||
const missing = [];
|
||||
for (const url of urls) if (!(await staging.match(url))) missing.push(url);
|
||||
const resumed = urls.size - missing.length;
|
||||
|
||||
const token = bust || String(Date.now());
|
||||
// Each file gets a few attempts (the homelab link drops requests), each
|
||||
// bounded so one hung connection can't eat the whole budget.
|
||||
@@ -86,7 +105,7 @@
|
||||
const res = await withTimeout(
|
||||
fetchFn(u.href, { cache: 'reload', credentials: 'same-origin', signal: ctl ? ctl.signal : undefined }),
|
||||
attemptTimeoutMs, setTimeoutFn, `${new URL(url).pathname} timed out`);
|
||||
if (res && res.ok) return [url, res];
|
||||
if (res && res.ok) { await staging.put(url, res); return; }
|
||||
last = new Error(`${new URL(url).pathname} → ${res ? res.status : 'no response'}`);
|
||||
} catch (err) {
|
||||
if (ctl) { try { ctl.abort(); } catch { /* already done */ } }
|
||||
@@ -95,18 +114,35 @@
|
||||
}
|
||||
throw last;
|
||||
};
|
||||
const fetched = await withTimeout(Promise.all([...urls].map(fetchOne)),
|
||||
timeoutMs, setTimeoutFn, 'update download timed out');
|
||||
// A lossy link copes far better with a few requests in flight than with
|
||||
// the whole shell at once.
|
||||
const pool = async () => {
|
||||
const queue = missing.slice();
|
||||
let failed = null;
|
||||
const worker = async () => {
|
||||
while (queue.length && !failed) {
|
||||
const url = queue.shift();
|
||||
try { await fetchOne(url); } catch (err) { failed = failed || err; }
|
||||
}
|
||||
};
|
||||
await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency || DEFAULT_CONCURRENCY, queue.length || 1)) }, worker));
|
||||
if (failed) throw failed;
|
||||
};
|
||||
await withTimeout(pool(), timeoutMs, setTimeoutFn, 'update download timed out');
|
||||
|
||||
for (const name of names) {
|
||||
const cache = await cachesApi.open(name);
|
||||
for (const [url, res] of fetched) await cache.put(url, res.clone());
|
||||
for (const url of urls) {
|
||||
const res = await staging.match(url);
|
||||
if (res) await cache.put(url, res);
|
||||
}
|
||||
// An old worker's cache-first handler may have stored the busted URLs.
|
||||
for (const req of await cache.keys()) {
|
||||
if (new URL(req.url).searchParams.has(BUST_PARAM)) await cache.delete(req);
|
||||
}
|
||||
}
|
||||
return { refreshed: fetched.length, caches: names.length };
|
||||
await cachesApi.delete(stagingName);
|
||||
return { refreshed: urls.size, caches: names.length, resumed };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user