feat(sync): coalesce offline media intent

This commit is contained in:
KoalaDev
2026-08-20 20:30:54 +02:00
parent c064953cca
commit 90050bd9ec
9 changed files with 1228 additions and 57 deletions
+184
View File
@@ -1,4 +1,12 @@
import { test, expect } from './helpers/extension-fixture.mjs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION } from '../../shared/constants.js';
const testDir = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(path.join(testDir, '..', '..', 'server', 'package.json'));
const NodeWebSocket = require('ws');
/**
* Drives the packed extension itself: real background service worker, real
@@ -63,6 +71,61 @@ async function applyCanonicalMediaState(context, extensionId, tabId, mediaState)
}, { tabId, mediaState }));
}
async function connectLegacyRelayClient(port) {
const socket = new NodeWebSocket(
`ws://127.0.0.1:${port}/socket.io/?EIO=4&transport=websocket&version=3.1.3&token=${OFFICIAL_SERVER_TOKEN}`
);
socket.messages = [];
socket.on('message', value => socket.messages.push(value.toString()));
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('legacy relay connection timed out')), 5000);
socket.once('open', () => {
clearTimeout(timeout);
resolve();
});
});
socket.send('40');
await expect.poll(() => socket.messages.filter(message => message.startsWith('0') || message.startsWith('40')).length).toBeGreaterThanOrEqual(2);
socket.messages.length = 0;
return socket;
}
function sendLegacyRelayEvent(socket, event, data = {}) {
socket.send(`42${JSON.stringify([event, data])}`);
}
async function waitForLegacyRelayEvent(socket, event, timeoutMs = 10_000) {
await expect.poll(() => socket.messages.some(message => {
if (!message.startsWith('42')) return false;
try { return JSON.parse(message.substring(2))[0] === event; } catch { return false; }
}), { timeout: timeoutMs }).toBe(true);
const index = socket.messages.findIndex(message => {
if (!message.startsWith('42')) return false;
try { return JSON.parse(message.substring(2))[0] === event; } catch { return false; }
});
return JSON.parse(socket.messages.splice(index, 1)[0].substring(2))[1];
}
async function joinLegacyRelayRoom(socket, roomId, peerId) {
sendLegacyRelayEvent(socket, 'join_room', { roomId, peerId, protocolVersion: PROTOCOL_VERSION });
return waitForLegacyRelayEvent(socket, 'room_data');
}
async function terminateExtensionServiceWorker(context, extensionId, page) {
const session = await context.newCDPSession(page);
try {
const { targetInfos } = await session.send('Target.getTargets');
const target = targetInfos.find(candidate =>
candidate.type === 'service_worker'
&& candidate.url.startsWith(`chrome-extension://${extensionId}/`)
);
if (!target) throw new Error('extension service worker target not found');
await session.send('Target.closeTarget', { targetId: target.targetId });
} finally {
await session.detach();
}
}
async function setAudioSettings(context, extensionId, settings) {
return withExtensionPage(context, extensionId, page => page.evaluate(
value => chrome.storage.local.set({ audioSettings: value }),
@@ -1047,6 +1110,127 @@ test('keeps controlling a Drive-style player across an ordinary play and pause',
await expect.poll(() => playerFrame.locator('video').evaluate(video => video.paused)).toBe(false);
});
test('coalesces persisted offline media intent before canonical reconnect recovery', async ({ context, extensionId, baseURL }) => {
test.setTimeout(60_000);
const relay = await import('../../server/index.js');
let legacy = null;
let lateJoiner = null;
try {
await relay.startServer(0, '127.0.0.1');
const port = relay.httpServer.address().port;
const roomId = `e2e-coalesced-${Date.now()}`;
legacy = await connectLegacyRelayClient(port);
await joinLegacyRelayRoom(legacy, roomId, 'legacy-e2e');
const url = `${baseURL}/pages/simple-player.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const { tabId } = await selectTargetTab(context, extensionId, url);
await withExtensionPage(context, extensionId, extensionPage => extensionPage.evaluate(async settings => {
await chrome.storage.local.set(settings);
return chrome.runtime.sendMessage({ type: 'CONNECT' });
}, {
serverUrl: `ws://127.0.0.1:${port}`,
useCustomServer: true,
roomId,
password: '',
username: 'current-e2e'
}));
await expect.poll(() => getExtensionState(context, extensionId, { type: 'GET_STATUS' }))
.toMatchObject({ status: 'connected', roomId, queuedLogicalEvents: 0 });
// Establish accepted server truth that would be stale for this client
// after its later offline actions.
sendLegacyRelayEvent(legacy, 'play', { currentTime: 1, seq: 1, actionTimestamp: 1 });
sendLegacyRelayEvent(legacy, 'seek', { currentTime: 1, targetTime: 1, seq: 2, actionTimestamp: 2 });
await expect.poll(() => relay.rooms.get(roomId)?.mediaState)
.toMatchObject({ revision: 2, playbackState: 'playing', currentTime: 1, updatedBy: 'legacy-e2e' });
await expect.poll(() => page.locator('#player').evaluate(video => video.currentTime)).toBeGreaterThan(0.8);
legacy.messages.length = 0;
const connectedStatus = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
const extensionSocketId = Array.from(relay.rooms.get(roomId).peerData.entries())
.find(([, data]) => data.peerId === connectedStatus.peerId)?.[0];
expect(extensionSocketId).toBeTruthy();
// Point future reconnect attempts at an unused local port, then sever
// only the extension socket. The relay and legacy peer stay live.
await withExtensionPage(context, extensionId, extensionPage => extensionPage.evaluate(
serverUrl => chrome.storage.local.set({ serverUrl }),
'ws://127.0.0.1:1'
));
relay.io.sockets.sockets.get(extensionSocketId).disconnect(true);
await expect.poll(() => getExtensionState(context, extensionId, { type: 'GET_STATUS' }).then(status => status.status))
.not.toBe('connected');
expect(await sendServerCommand(context, extensionId, tabId, 'play', { currentTime: 2 })).toMatchObject({ status: 'ok' });
expect(await sendServerCommand(context, extensionId, tabId, 'seek', { targetTime: 4 })).toMatchObject({ status: 'ok' });
expect(await sendServerCommand(context, extensionId, tabId, 'seek', { targetTime: 6 })).toMatchObject({ status: 'ok' });
expect(await sendServerCommand(context, extensionId, tabId, 'pause', { currentTime: 6 })).toMatchObject({ status: 'ok' });
await expect.poll(() => getExtensionState(context, extensionId, { type: 'GET_STATUS' }))
.toMatchObject({ queuedLogicalEvents: 1, queuedMediaIntents: 1, queuedWireEvents: 2 });
await expect.poll(() => page.locator('#player').evaluate(video => ({ paused: video.paused, currentTime: video.currentTime })))
.toMatchObject({ paused: true });
// Terminate the actual MV3 worker. The next runtime message starts a new
// worker, which must migrate/restore the logical queue and local sequence.
await terminateExtensionServiceWorker(context, extensionId, page);
await expect.poll(() => getExtensionState(context, extensionId, { type: 'GET_STATUS' }).then(status => status.queuedMediaIntents))
.toBe(1);
const restoredQueue = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
expect(restoredQueue.queuedLogicalEvents).toBeGreaterThanOrEqual(1);
expect(restoredQueue.queuedWireEvents).toBeGreaterThanOrEqual(2);
await withExtensionPage(context, extensionId, extensionPage => extensionPage.evaluate(async serverUrl => {
await chrome.storage.local.set({ serverUrl });
chrome.alarms.create('keepAlive', { when: Date.now() + 50 });
}, `ws://127.0.0.1:${port}`));
await page.locator('#player').evaluate(video => {
window.__koalaReconnectSeeks = [];
video.addEventListener('seeked', () => window.__koalaReconnectSeeks.push(video.currentTime));
});
const replaySeek = await waitForLegacyRelayEvent(legacy, 'seek', 20_000);
const replayPause = await waitForLegacyRelayEvent(legacy, 'pause', 20_000);
expect(replaySeek).toMatchObject({ currentTime: 6, targetTime: 6 });
expect(replayPause).toMatchObject({ currentTime: 6 });
expect(replaySeek.seq).toBeLessThan(replayPause.seq);
await expect.poll(() => getExtensionState(context, extensionId, { type: 'GET_STATUS' }))
.toMatchObject({ status: 'connected', roomId, queuedLogicalEvents: 0, queuedMediaIntents: 0 });
await expect.poll(() => relay.rooms.get(roomId)?.mediaState)
.toMatchObject({ playbackState: 'paused', currentTime: 6 });
expect(relay.rooms.get(roomId).mediaState.revision).toBeGreaterThan(2);
// The stale r2 snapshot must never have sought the local player back to 1
// before its authorized pending intent replayed.
const reconnectSeeks = await page.evaluate(() => window.__koalaReconnectSeeks || []);
expect(reconnectSeeks.some(value => value < 3)).toBe(false);
await expect.poll(() => page.locator('#player').evaluate(video => ({ paused: video.paused, currentTime: video.currentTime })))
.toMatchObject({ paused: true });
lateJoiner = await connectLegacyRelayClient(port);
const lateRoom = await joinLegacyRelayRoom(lateJoiner, roomId, 'late-e2e');
expect(lateRoom.mediaState).toMatchObject({
revision: relay.rooms.get(roomId).mediaState.revision,
playbackState: 'paused',
currentTime: 6
});
await page.waitForTimeout(700);
const leakedMediaEvents = legacy.messages.filter(message => {
if (!message.startsWith('42')) return false;
try { return ['play', 'pause', 'seek'].includes(JSON.parse(message.substring(2))[0]); } catch { return false; }
});
expect(leakedMediaEvents).toEqual([]);
} finally {
await context.setOffline(false).catch(() => {});
try { legacy?.close(); } catch { /* already closed */ }
try { lateJoiner?.close(); } catch { /* already closed */ }
await relay.stopServerForTests();
}
});
function FRAMED_VIDEO_PAUSED() {
return document.querySelector('iframe').contentDocument.querySelector('video').paused;
}