feat: lay groundwork for multi-flow registries
This commit is contained in:
+123
-35
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user