This commit is contained in:
Joey Yakimowich-Payne 2025-10-14 12:48:19 -06:00
commit 5d03f8fb38
2 changed files with 153 additions and 94 deletions

View file

@ -6,6 +6,7 @@ const executionContext = {
}; };
let contextReadyPromise = null; let contextReadyPromise = null;
let contextReadyResolver = null; let contextReadyResolver = null;
let preferWindowContext = true;
function createReadyPromise() { function createReadyPromise() {
contextReadyPromise = new Promise((resolve) => { contextReadyPromise = new Promise((resolve) => {
@ -23,62 +24,35 @@ function resolveReadyPromise() {
} }
} }
async function ensureDocumentContext() { function resetContextState() {
const hasOffscreenApi = Boolean(chrome.offscreen?.createDocument); executionContext.windowId = null;
if (hasOffscreenApi) { executionContext.tabId = null;
try { executionContext.mode = null;
if (chrome.offscreen.hasDocument) { contextReadyPromise = null;
const exists = await chrome.offscreen.hasDocument(); contextReadyResolver = null;
if (exists) { }
executionContext.mode = "offscreen";
if (!contextReadyPromise) {
contextReadyPromise = Promise.resolve();
}
await contextReadyPromise;
return;
}
}
} catch (error) {
// ignore and fall back
}
async function closeOffscreenDocumentIfSupported() {
if (chrome.offscreen?.closeDocument) {
try { try {
createReadyPromise(); await chrome.offscreen.closeDocument();
await chrome.offscreen.createDocument({ } catch (_) {
url: chrome.runtime.getURL("offscreen.html"), // ignore - document might already be gone
reasons: ["DOM_PARSER"],
justification: "Process O'Reilly pages to build EPUB downloads"
});
executionContext.mode = "offscreen";
await contextReadyPromise;
return;
} catch (error) {
const alreadyExists = String(error?.message || "").includes("already exists");
if (!alreadyExists) {
console.warn("SafariBooks Downloader: Offscreen document failed, falling back to hidden window.", error);
} else {
executionContext.mode = "offscreen";
if (!contextReadyPromise) {
contextReadyPromise = Promise.resolve();
}
await contextReadyPromise;
return;
}
} }
} }
}
if (executionContext.mode === "window") { async function ensureWindowContext() {
if (executionContext.tabId != null) { if (executionContext.mode === "window" && executionContext.tabId != null) {
try { try {
await chrome.tabs.get(executionContext.tabId); await chrome.tabs.get(executionContext.tabId);
if (!contextReadyPromise) { if (!contextReadyPromise) {
contextReadyPromise = Promise.resolve(); contextReadyPromise = Promise.resolve();
}
await contextReadyPromise;
return;
} catch (_) {
executionContext.tabId = null;
} }
await contextReadyPromise;
return;
} catch (_) {
resetContextState();
} }
} }
@ -98,33 +72,124 @@ async function ensureDocumentContext() {
await contextReadyPromise; await contextReadyPromise;
} }
async function ensureDocumentContext() {
if (!preferWindowContext) {
const hasOffscreenApi = Boolean(chrome.offscreen?.createDocument);
if (hasOffscreenApi) {
try {
if (chrome.offscreen.hasDocument) {
const exists = await chrome.offscreen.hasDocument();
if (exists) {
executionContext.mode = "offscreen";
if (!contextReadyPromise) {
contextReadyPromise = Promise.resolve();
}
await contextReadyPromise;
return;
}
}
} catch (_) {
// ignore - we'll attempt to create a fresh document below
}
try {
createReadyPromise();
await chrome.offscreen.createDocument({
url: chrome.runtime.getURL("offscreen.html"),
reasons: ["DOM_PARSER"],
justification: "Process O'Reilly pages to build EPUB downloads"
});
executionContext.mode = "offscreen";
await contextReadyPromise;
return;
} catch (error) {
const alreadyExists = String(error?.message || "").includes("already exists");
if (alreadyExists) {
executionContext.mode = "offscreen";
if (!contextReadyPromise) {
contextReadyPromise = Promise.resolve();
}
await contextReadyPromise;
return;
}
console.warn("SafariBooks Downloader: Offscreen document unavailable, using hidden window instead.", error);
preferWindowContext = true;
await closeOffscreenDocumentIfSupported();
resetContextState();
}
} else {
preferWindowContext = true;
}
}
await ensureWindowContext();
}
function cleanupWindowIfIdle() {
if (executionContext.mode === "window" && pendingResponses.size === 0 && executionContext.windowId != null) {
chrome.windows
.remove(executionContext.windowId)
.catch(() => {})
.finally(() => {
resetContextState();
});
}
}
function shouldRetryInWindow(error, pending) {
if (!error || pending.attempt >= 1) {
return false;
}
if (pending.contextMode !== "offscreen") {
return false;
}
const message = typeof error === "string" ? error : String(error);
return message.toLowerCase().includes("domparser");
}
async function startDownloadTask(bookId, options, sendResponse, attempt = 0) {
try {
await ensureDocumentContext();
} catch (error) {
sendResponse({ ok: false, error: error?.message || String(error) });
return;
}
const requestId = crypto.randomUUID();
const contextMode = executionContext.mode ?? (preferWindowContext ? "window" : "offscreen");
pendingResponses.set(requestId, { sendResponse, bookId, options, attempt, contextMode });
try {
await chrome.runtime.sendMessage({
type: "offscreen-download",
requestId,
bookId,
options
});
} catch (error) {
const pending = pendingResponses.get(requestId);
if (pending) {
pendingResponses.delete(requestId);
pending.sendResponse({ ok: false, error: error?.message || String(error) });
cleanupWindowIfIdle();
}
}
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message?.type === "downloadBook") { if (message?.type === "downloadBook") {
ensureDocumentContext() const bookId = message.bookId;
.then(() => { if (!bookId) {
const requestId = crypto.randomUUID(); sendResponse({ ok: false, error: "Missing book ID." });
pendingResponses.set(requestId, sendResponse); return false;
chrome.runtime }
.sendMessage({
type: "offscreen-download", const options = {
requestId, theme: message.theme ?? "none",
bookId: message.bookId, kindle: Boolean(message.kindle)
options: { };
theme: message.theme ?? "none",
kindle: Boolean(message.kindle) startDownloadTask(bookId, options, sendResponse);
}
})
.catch((error) => {
const responder = pendingResponses.get(requestId);
if (responder) {
responder({ ok: false, error: error.message });
pendingResponses.delete(requestId);
}
});
})
.catch((error) => {
sendResponse({ ok: false, error: error.message });
});
return true; return true;
} }
@ -135,25 +200,26 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message?.type === "offscreen-download-complete") { if (message?.type === "offscreen-download-complete") {
const { requestId, ok, error } = message; const { requestId, ok, error } = message;
const responder = pendingResponses.get(requestId); const pending = pendingResponses.get(requestId);
if (responder) { if (!pending) {
responder({ ok, error }); return false;
pendingResponses.delete(requestId);
} }
if (executionContext.mode === "window" && pendingResponses.size === 0 && executionContext.windowId != null) { if (!ok && shouldRetryInWindow(error, pending)) {
chrome.windows pendingResponses.delete(requestId);
.remove(executionContext.windowId) preferWindowContext = true;
closeOffscreenDocumentIfSupported()
.catch(() => {}) .catch(() => {})
.finally(() => { .finally(() => {
executionContext.windowId = null; resetContextState();
executionContext.tabId = null; startDownloadTask(pending.bookId, pending.options, pending.sendResponse, pending.attempt + 1);
executionContext.mode = null;
contextReadyPromise = null;
contextReadyResolver = null;
}); });
return false;
} }
pendingResponses.delete(requestId);
pending.sendResponse({ ok, error });
cleanupWindowIfIdle();
return false; return false;
} }

View file

@ -13,13 +13,6 @@
"service_worker": "background.js", "service_worker": "background.js",
"type": "module" "type": "module"
}, },
"offscreen": {
"page": "offscreen.html",
"reasons": [
"DOM_PARSER"
],
"justification": "Parse book HTML to build EPUB files in a DOM-enabled context"
},
"action": { "action": {
"default_title": "SafariBooks Downloader", "default_title": "SafariBooks Downloader",
"default_popup": "index.html", "default_popup": "index.html",