reilly-downloader/background.js
Joey Yakimowich-Payne 7a44902433 feat: surface detailed download progress
Changelog:
- forward download progress from background/offscreen workers to content frames
- add progress UI and button state management in Safari Books page script
- emit structured progress callbacks and faster image fetching in SafariBooksDownloader
- sync Firefox bundle with progress plumbing and concurrency improvements
- ignore packaged extension archives

Description:
Expose progress events through the downloader so the content script can present real-time status, including chapter and asset fetching milestones. Added concurrency controls, page detection, and packaging telemetry, while keeping Firefox build behavior in sync and ignoring generated .xpi bundles.
2025-10-16 10:30:18 -06:00

287 lines
7.5 KiB
JavaScript

const pendingResponses = new Map();
const executionContext = {
mode: null,
windowId: null,
tabId: null
};
let contextReadyPromise = null;
let contextReadyResolver = null;
let preferWindowContext = true;
function forwardProgressToClient(pending, message) {
if (!pending || pending.tabId == null) {
return;
}
const payload = {
type: "download-progress",
requestId: message.requestId,
payload: message.payload
};
if (pending.frameId != null) {
chrome.tabs.sendMessage(pending.tabId, payload, { frameId: pending.frameId }).catch(() => {});
} else {
chrome.tabs.sendMessage(pending.tabId, payload).catch(() => {});
}
}
function createReadyPromise() {
contextReadyPromise = new Promise((resolve) => {
contextReadyResolver = resolve;
});
}
function resolveReadyPromise() {
if (contextReadyResolver) {
contextReadyResolver();
contextReadyResolver = null;
}
if (!contextReadyPromise) {
contextReadyPromise = Promise.resolve();
}
}
function resetContextState() {
executionContext.windowId = null;
executionContext.tabId = null;
executionContext.mode = null;
contextReadyPromise = null;
contextReadyResolver = null;
}
async function closeOffscreenDocumentIfSupported() {
if (chrome.offscreen?.closeDocument) {
try {
await chrome.offscreen.closeDocument();
} catch (_) {
// ignore - document might already be gone
}
}
}
async function ensureWindowContext() {
if (executionContext.mode === "window" && executionContext.tabId != null) {
try {
await chrome.tabs.get(executionContext.tabId);
if (!contextReadyPromise) {
contextReadyPromise = Promise.resolve();
}
await contextReadyPromise;
return;
} catch (_) {
resetContextState();
}
}
const popupUrl = chrome.runtime.getURL("offscreen.html#window");
const createdWindow = await chrome.windows.create({
url: popupUrl,
type: "popup",
focused: false,
width: 420,
height: 520
});
executionContext.mode = "window";
executionContext.windowId = createdWindow.id;
executionContext.tabId = createdWindow.tabs && createdWindow.tabs.length ? createdWindow.tabs[0].id : null;
createReadyPromise();
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, clientInfo = {}) {
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");
const tabId = typeof clientInfo.tabId === "number" ? clientInfo.tabId : null;
const frameId = typeof clientInfo.frameId === "number" ? clientInfo.frameId : null;
const pendingEntry = {
sendResponse,
bookId,
options,
attempt,
contextMode,
tabId,
frameId,
clientInfo
};
pendingResponses.set(requestId, pendingEntry);
if (tabId != null) {
forwardProgressToClient(pendingEntry, {
requestId,
payload: {
stage: "starting",
bookId
}
});
}
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) => {
if (message?.type === "downloadBook") {
const bookId = message.bookId;
if (!bookId) {
sendResponse({ ok: false, error: "Missing book ID." });
return false;
}
const options = {
theme: message.theme ?? "none",
kindle: Boolean(message.kindle)
};
const clientInfo = {
tabId: sender?.tab?.id ?? null,
frameId: typeof sender?.frameId === "number" ? sender.frameId : null
};
startDownloadTask(bookId, options, sendResponse, 0, clientInfo);
return true;
}
if (message?.type === "offscreen-ready") {
resolveReadyPromise();
return false;
}
if (message?.type === "offscreen-download-complete") {
const { requestId, ok, error } = message;
const pending = pendingResponses.get(requestId);
if (!pending) {
return false;
}
if (!ok && shouldRetryInWindow(error, pending)) {
pendingResponses.delete(requestId);
preferWindowContext = true;
closeOffscreenDocumentIfSupported()
.catch(() => {})
.finally(() => {
resetContextState();
startDownloadTask(
pending.bookId,
pending.options,
pending.sendResponse,
pending.attempt + 1,
pending.clientInfo || {}
);
});
return false;
}
pendingResponses.delete(requestId);
pending.sendResponse({ ok, error });
cleanupWindowIfIdle();
return false;
}
if (message?.type === "offscreen-download-progress") {
const { requestId } = message;
const pending = requestId ? pendingResponses.get(requestId) : null;
if (pending) {
forwardProgressToClient(pending, message);
}
return false;
}
return undefined;
});