feat: lay groundwork for multi-flow registries

This commit is contained in:
QLHazyCoder
2026-05-13 04:46:56 +08:00
parent 04f89620be
commit a85ed0ce89
28 changed files with 2775 additions and 92 deletions
+123 -35
View File
@@ -11,6 +11,7 @@
isRetryableContentScriptTransportError,
LOG_PREFIX,
matchesSourceUrlFamily,
sourceRegistry = null,
setState,
sleepWithStop,
STOP_ERROR_MESSAGE,
@@ -19,6 +20,65 @@
const pendingCommands = new Map();
function resolveCanonicalSource(source) {
if (sourceRegistry?.resolveCanonicalSource) {
return sourceRegistry.resolveCanonicalSource(source);
}
return String(source || '').trim();
}
function getSourceKeys(source) {
if (sourceRegistry?.getSourceKeys) {
const registryKeys = sourceRegistry.getSourceKeys(source);
if (Array.isArray(registryKeys) && registryKeys.length) {
return registryKeys;
}
}
const normalized = String(source || '').trim();
return normalized ? [normalized] : [];
}
function getSourceCommandKey(source) {
const keys = getSourceKeys(source);
return keys[0] || String(source || '').trim();
}
function sourcesMatch(leftSource, rightSource) {
const left = resolveCanonicalSource(leftSource);
const right = resolveCanonicalSource(rightSource);
return Boolean(left && right && left === right);
}
function getSourceMapValue(record, source) {
const map = record && typeof record === 'object' ? record : {};
for (const key of getSourceKeys(source)) {
if (Object.prototype.hasOwnProperty.call(map, key)) {
return map[key];
}
}
return undefined;
}
function setSourceMapValue(record, source, value) {
const nextRecord = { ...(record || {}) };
const keys = getSourceKeys(source);
const canonicalKey = keys[0] || String(source || '').trim();
for (const key of keys.slice(1)) {
delete nextRecord[key];
}
if (canonicalKey) {
nextRecord[canonicalKey] = value;
}
return nextRecord;
}
function getCleanupOwnerSource(cleanupScope) {
if (sourceRegistry?.getCleanupOwnerSource) {
return sourceRegistry.getCleanupOwnerSource(cleanupScope);
}
return cleanupScope === 'oauth-localhost-callback' ? 'signup-page' : '';
}
function normalizeAutomationWindowId(value) {
if (value === null || value === undefined || value === '') {
return null;
@@ -170,7 +230,7 @@
}
async function registerTab(source, tabId) {
const registry = await getTabRegistry();
let registry = await getTabRegistry();
let windowId = null;
if (chrome?.tabs?.get && Number.isInteger(tabId)) {
const tab = await chrome.tabs.get(tabId).catch(() => null);
@@ -180,42 +240,42 @@
}
windowId = normalizeAutomationWindowId(tab?.windowId);
}
registry[source] = {
registry = setSourceMapValue(registry, source, {
tabId,
ready: true,
...(windowId !== null ? { windowId } : {}),
};
});
await setState({ tabRegistry: registry });
console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
}
async function isTabAlive(source) {
const registry = await getTabRegistry();
const entry = registry[source];
let registry = await getTabRegistry();
const entry = getSourceMapValue(registry, source);
if (!entry) return false;
try {
const tab = await chrome.tabs.get(entry.tabId);
if (!(await isTabInAutomationWindow(tab))) {
registry[source] = null;
registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
return false;
}
return true;
} catch {
registry[source] = null;
registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
return false;
}
}
async function getTabId(source) {
const registry = await getTabRegistry();
const tabId = registry[source]?.tabId || null;
let registry = await getTabRegistry();
const tabId = getSourceMapValue(registry, source)?.tabId || null;
if (!Number.isInteger(tabId)) {
return null;
}
if (!(await isTabInAutomationWindow(tabId))) {
registry[source] = null;
registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
return null;
}
@@ -225,8 +285,7 @@
async function rememberSourceLastUrl(source, url) {
if (!source || !url) return;
const state = await getState();
const sourceLastUrls = { ...(state.sourceLastUrls || {}) };
sourceLastUrls[source] = url;
const sourceLastUrls = setSourceMapValue(state.sourceLastUrls, source, url);
await setState({ sourceLastUrls });
}
@@ -234,7 +293,7 @@
const { excludeTabIds = [] } = options;
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
const state = await getState();
const lastUrl = state.sourceLastUrls?.[source];
const lastUrl = getSourceMapValue(state.sourceLastUrls, source);
const referenceUrls = [currentUrl, lastUrl].filter(Boolean);
if (!referenceUrls.length) return;
@@ -249,9 +308,10 @@
await chrome.tabs.remove(matchedIds).catch(() => { });
const registry = await getTabRegistry();
if (registry[source]?.tabId && matchedIds.includes(registry[source].tabId)) {
registry[source] = null;
let registry = await getTabRegistry();
const sourceEntry = getSourceMapValue(registry, source);
if (sourceEntry?.tabId && matchedIds.includes(sourceEntry.tabId)) {
registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
}
@@ -274,7 +334,7 @@
async function closeLocalhostCallbackTabs(callbackUrl, options = {}) {
if (!isLocalhostOAuthCallbackUrl(callbackUrl)) return 0;
const { excludeTabIds = [] } = options;
const { excludeTabIds = [], ownerSource = getCleanupOwnerSource('oauth-localhost-callback') } = options;
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
const tabs = await queryTabsInAutomationWindow({});
const matchedIds = tabs
@@ -286,9 +346,10 @@
await chrome.tabs.remove(matchedIds).catch(() => { });
const registry = await getTabRegistry();
if (registry['signup-page']?.tabId && matchedIds.includes(registry['signup-page'].tabId)) {
registry['signup-page'] = null;
let registry = await getTabRegistry();
const ownerEntry = getSourceMapValue(registry, ownerSource);
if (ownerEntry?.tabId && matchedIds.includes(ownerEntry.tabId)) {
registry = setSourceMapValue(registry, ownerSource, null);
await setState({ tabRegistry: registry });
}
@@ -468,7 +529,7 @@
while (Date.now() - start < timeoutMs) {
attempt += 1;
const pong = await pingContentScriptOnTab(tabId);
if (pong?.ok && (!pong.source || pong.source === source)) {
if (pong?.ok && (!pong.source || sourcesMatch(pong.source, source))) {
console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
await registerTab(source, tabId);
return;
@@ -478,9 +539,13 @@
throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
}
const registry = await getTabRegistry();
if (registry[source]) {
registry[source].ready = false;
let registry = await getTabRegistry();
const sourceEntry = getSourceMapValue(registry, source);
if (sourceEntry) {
registry = setSourceMapValue(registry, source, {
...sourceEntry,
ready: false,
});
await setState({ tabRegistry: registry });
}
@@ -505,7 +570,7 @@
}
const pongAfterInject = await pingContentScriptOnTab(tabId);
if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) {
if (pongAfterInject?.ok && (!pongAfterInject.source || sourcesMatch(pongAfterInject.source, source))) {
console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready after inject ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
await registerTab(source, tabId);
return;
@@ -609,14 +674,16 @@
function queueCommand(source, message, timeout = 15000) {
return new Promise((resolve, reject) => {
const commandKey = getSourceCommandKey(source);
const timer = setTimeout(() => {
pendingCommands.delete(source);
pendingCommands.delete(commandKey);
reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
}, timeout);
pendingCommands.set(source, {
pendingCommands.set(commandKey, {
message,
resolve,
reject,
source,
timer,
responseTimeoutMs: timeout,
});
@@ -625,11 +692,16 @@
}
function flushCommand(source, tabId) {
const pending = pendingCommands.get(source);
const pending = pendingCommands.get(getSourceCommandKey(source));
if (pending) {
clearTimeout(pending.timer);
pendingCommands.delete(source);
sendTabMessageWithTimeout(tabId, source, pending.message, pending.responseTimeoutMs).then(pending.resolve).catch(pending.reject);
pendingCommands.delete(getSourceCommandKey(source));
sendTabMessageWithTimeout(
tabId,
pending.source || source,
pending.message,
pending.responseTimeoutMs
).then(pending.resolve).catch(pending.reject);
console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
}
}
@@ -677,18 +749,29 @@
const sameUrl = currentTab.url === url;
const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
const registry = await getTabRegistry();
let registry = await getTabRegistry();
const sourceEntry = getSourceMapValue(registry, source);
if (sameUrl) {
await chrome.tabs.update(tabId, { active: true });
if (shouldReloadOnReuse) {
if (registry[source]) registry[source].ready = false;
if (sourceEntry) {
registry = setSourceMapValue(registry, source, {
...sourceEntry,
ready: false,
});
}
await setState({ tabRegistry: registry });
await chrome.tabs.reload(tabId);
await waitForTabUpdateComplete(tabId);
}
if (options.inject) {
if (registry[source]) registry[source].ready = false;
if (sourceEntry) {
registry = setSourceMapValue(registry, source, {
...sourceEntry,
ready: false,
});
}
await setState({ tabRegistry: registry });
if (options.injectSource) {
await chrome.scripting.executeScript({
@@ -710,7 +793,12 @@
return tabId;
}
if (registry[source]) registry[source].ready = false;
if (sourceEntry) {
registry = setSourceMapValue(registry, source, {
...sourceEntry,
ready: false,
});
}
await setState({ tabRegistry: registry });
await chrome.tabs.update(tabId, { url, active: true });
@@ -765,7 +853,7 @@
throwIfStopped();
const { responseTimeoutMs = getContentScriptResponseTimeoutMs(message) } = options;
const registry = await getTabRegistry();
const entry = registry[source];
const entry = getSourceMapValue(registry, source);
if (!entry || !entry.ready) {
throwIfStopped();