mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-19 15:46:15 +00:00
fix(extension): stop the frame registry from being wiped mid-setup
Inspected the live KODIK layout: the player is a cross-origin frame at depth 2 inside a visible same-origin wrapper, with no sandbox attribute and no video element at all until playback starts. The registry wipe added in the previous commit was wrong. tabs.onUpdated reports status 'loading' for same-document History API navigations as well, which is exactly what these sites do when you switch mirror or episode part. The wipe therefore landed while the player frame was being built, leaving the recovery probe with nothing to fall back on; if the all-frames sweep was rejected at that moment the target stayed on frame 0 with no way back. The registry now self-corrects instead: a probe that reached more than the top frame is authoritative and current, so it supersedes the stored ids. A probe that only reached frame 0 proves nothing and merges. Stale ids cost one bounded, isolated probe until the next good resolve replaces them. This matches the reported symptom, where audio boost and compressor worked on KODIK — proving the content script was running in the player frame — while the dev panel still reported frame 0 with no video, because the target had fallen back after promotion had already succeeded once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+25
-14
@@ -113,6 +113,29 @@ function rememberFrameId(tabId, frameId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the registry when a probe actually saw the tab's frame layout.
|
||||
*
|
||||
* Clearing on navigation looked right and was wrong: tabs.onUpdated reports
|
||||
* status 'loading' for same-document History API navigations too, which is
|
||||
* exactly what these sites do when you switch mirror or episode part. The wipe
|
||||
* therefore landed the moment the player frame was being built, leaving the
|
||||
* recovery probe with nothing. A successful multi-frame probe is authoritative
|
||||
* and current, so it supersedes whatever was there instead.
|
||||
*/
|
||||
function refreshFrameIds(tabId, frameIds) {
|
||||
const normalizedTabId = normalizeTabId(tabId);
|
||||
if (normalizedTabId === null) return;
|
||||
const ids = (Array.isArray(frameIds) ? frameIds : [])
|
||||
.filter(frameId => Number.isInteger(frameId) && frameId >= 0);
|
||||
if (ids.length > 1) {
|
||||
knownFrameIdsByTab.set(normalizedTabId, new Set(ids.slice(0, MAX_KNOWN_FRAMES_PER_TAB)));
|
||||
return;
|
||||
}
|
||||
// A probe that only reached the top frame proves nothing about the rest.
|
||||
for (const frameId of ids) rememberFrameId(normalizedTabId, frameId);
|
||||
}
|
||||
|
||||
function listKnownFrameIds(tabId) {
|
||||
const frames = knownFrameIdsByTab.get(normalizeTabId(tabId));
|
||||
return frames ? Array.from(frames) : [];
|
||||
@@ -2486,9 +2509,7 @@ async function injectContentScript(tabId, {
|
||||
// injected. Waiting for a content script to message us first would leave
|
||||
// the registry empty exactly when it is needed most — the first
|
||||
// activation, before any script exists to report itself.
|
||||
for (const frameId of contentTarget.discoveredFrameIds || []) {
|
||||
rememberFrameId(tabId, frameId);
|
||||
}
|
||||
refreshFrameIds(tabId, contentTarget.discoveredFrameIds);
|
||||
if (!isTargetActivationSuperseded(tabId, activationGeneration)
|
||||
&& activeTargetActivation?.tabId === tabId) {
|
||||
activeTargetActivation.frameId = contentTarget.frameId;
|
||||
@@ -3074,9 +3095,7 @@ async function selectedMediaTargetMoved(tabId) {
|
||||
attempts: 1,
|
||||
knownFrameIds: listKnownFrameIds(tabId)
|
||||
});
|
||||
for (const frameId of resolved.discoveredFrameIds || []) {
|
||||
rememberFrameId(tabId, frameId);
|
||||
}
|
||||
refreshFrameIds(tabId, resolved.discoveredFrameIds);
|
||||
} catch {
|
||||
// An access-required error must reach the full activation path so the
|
||||
// popup can surface it.
|
||||
@@ -3220,14 +3239,6 @@ async function retryPendingTarget({ expectedRequestId = null, requireGrantedAcce
|
||||
}
|
||||
}
|
||||
|
||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
||||
// A committed navigation replaces every frame in the tab. Keeping the old
|
||||
// ids would mean probing dead frames on every resolve from then on.
|
||||
if (changeInfo.status === 'loading' && typeof changeInfo.url === 'string') {
|
||||
forgetFrameIds(tabId);
|
||||
}
|
||||
});
|
||||
|
||||
if (chrome.permissions?.onAdded?.addListener) {
|
||||
chrome.permissions.onAdded.addListener((addedPermissions) => {
|
||||
ensureState().then(async () => {
|
||||
|
||||
@@ -65,8 +65,12 @@ describe('target tab lifecycle', () => {
|
||||
expect(backgroundSource).toContain('function rememberFrameId(tabId, frameId)');
|
||||
expect(backgroundSource).toContain('rememberFrameId(senderTabId, sender?.frameId)');
|
||||
expect(backgroundSource).toContain('contentTarget.discoveredFrameIds');
|
||||
// A committed navigation invalidates every id from the previous page.
|
||||
expect(backgroundSource).toMatch(/changeInfo\.status === 'loading'[\s\S]{0,120}forgetFrameIds\(tabId\)/);
|
||||
// The registry must never be cleared on navigation: tabs.onUpdated
|
||||
// reports 'loading' for same-document History API navigations too, which
|
||||
// is exactly when these players are built. It self-corrects instead.
|
||||
expect(backgroundSource).toContain('function refreshFrameIds(tabId, frameIds)');
|
||||
expect(backgroundSource).not.toMatch(/changeInfo\.status === 'loading'[\s\S]{0,160}forgetFrameIds/);
|
||||
expect(backgroundSource).toContain('refreshFrameIds(tabId, contentTarget.discoveredFrameIds)');
|
||||
expect(backgroundSource).not.toMatch(/chrome\.webNavigation/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user