Paint saved playlists before the launch sync and swap a background-downloaded update instantly

This commit is contained in:
Jonathan Sykes
2026-09-22 13:27:12 +08:00
parent 691e204d61
commit 987697e759
7 changed files with 300 additions and 18 deletions

View File

@@ -16,7 +16,16 @@
const { test } = require('node:test');
const assert = require('node:assert');
const { applyUpdate, refreshShellInPlace, BUST_PARAM } = require('./sw-update');
const { applyUpdate, refreshShellInPlace, askCacheStatus, BUST_PARAM } = require('./sw-update');
// Minimal MessageChannel stand-in: port2 is handed to the "worker", which
// delivers a reply back through port1.onmessage.
function FakeChannel() {
const p1 = { onmessage: null };
const p2 = { __deliver: (data) => { if (p1.onmessage) p1.onmessage({ data }); } };
this.port1 = p1;
this.port2 = p2;
}
const ORIGIN = 'https://worship.example';
@@ -211,3 +220,79 @@ test('applyUpdate with no registration at all still reloads', async () => {
await applyUpdate({ reg: null, container: fakeContainer(), reload: () => { reloads++; } });
assert.strictEqual(reloads, 1);
});
// ---- background download + instant swap ------------------------------------
test('applyUpdate skips the download when the waiting worker already precached the build', async () => {
const order = [];
const container = fakeContainer();
await applyUpdate({
reg: { waiting: { postMessage: (m) => order.push('msg:' + m.type) } },
container,
reload: () => order.push('reload'),
refreshShell: async () => { order.push('refresh'); },
askStatus: async () => ({ ready: true, version: 'new' }),
onPhase: (p) => order.push('phase:' + p),
setTimeout: () => {},
});
assert.deepStrictEqual(order, ['phase:ready', 'msg:SKIP_WAITING'],
'the whole point: no re-download when the shell is already on disk');
container.fireControllerChange();
assert.ok(order.includes('reload'));
});
test('applyUpdate still downloads when the waiting worker reports an incomplete cache', async () => {
const order = [];
await applyUpdate({
reg: { waiting: { postMessage: (m) => order.push('msg:' + m.type) } },
container: fakeContainer(),
reload: () => order.push('reload'),
refreshShell: async () => { order.push('refresh'); },
askStatus: async () => ({ ready: false, missing: 3 }),
onPhase: (p) => order.push('phase:' + p),
setTimeout: () => {},
});
assert.deepStrictEqual(order, ['phase:downloading', 'refresh', 'msg:SKIP_WAITING']);
});
test('a worker that never answers falls back to downloading, it does not hang', async () => {
const order = [];
await applyUpdate({
reg: { waiting: { postMessage: () => {} } },
container: fakeContainer(),
reload: () => order.push('reload'),
refreshShell: async () => { order.push('refresh'); },
askStatus: async () => null, // no reply / timed out
setTimeout: () => {},
});
assert.deepStrictEqual(order, ['refresh']);
});
test('a failing status probe is not fatal — the download path still runs', async () => {
const order = [];
await applyUpdate({
reg: { waiting: { postMessage: () => {} } },
container: fakeContainer(),
reload: () => order.push('reload'),
refreshShell: async () => { order.push('refresh'); },
askStatus: async () => { throw new Error('port closed'); },
setTimeout: () => {},
});
assert.deepStrictEqual(order, ['refresh']);
});
test('askCacheStatus resolves null when the worker never replies', async () => {
let fire = null;
const p = askCacheStatus(
{ postMessage() {} },
{ timeoutMs: 10, setTimeout: (fn) => { fire = fn; }, Channel: FakeChannel },
);
fire();
assert.strictEqual(await p, null);
});
test('askCacheStatus returns the worker reply', async () => {
const worker = { postMessage(_msg, ports) { ports[0].__deliver({ ready: true, version: 'abc' }); } };
const got = await askCacheStatus(worker, { setTimeout: () => {}, Channel: FakeChannel });
assert.deepStrictEqual(got, { ready: true, version: 'abc' });
});