Search / static /reader /core /reader-document-loader.mjs
vomebook's picture
retire native DOCX reader
f3244ea
Raw History Blame Contribute Delete
6.53 kB
export function createReaderDocumentLoader(readerContext) {
function loadScript(url) {
readerContext.assertReaderActive();
return new Promise((resolve, reject) => {
const script = document.createElement("script");
let settled = false, timeout = 0, untrack = () => {};
const finish = (error) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
untrack();
script.onload = script.onerror = null;
if (error) {
script.remove();
reject(error);
} else resolve();
};
const networkError = () => Object.assign(
new Error("阅读组件下载失败,请检查网络后重试加载,或下载原文件。"),
{ code: "READER_ENGINE_NETWORK" }
);
untrack = readerContext.trackReaderResource(() => finish(readerContext.readerAbortError()));
// Download sibling engines concurrently, execute in dependency order.
script.async = false;
script.src = url;
script.onload = () => finish();
script.onerror = () => finish(networkError());
timeout = setTimeout(() => finish(networkError()), 20000);
document.head.appendChild(script);
});
}
function fetchWithReaderTimeout(url, timeoutMs = readerContext.READER_PROXY_TIMEOUT_MS, requestInit = {}) {
return readerContext.readerRequestManager.request(url, timeoutMs, requestInit);
}
function retryableReaderProxyError(error) {
return [408, 429, 500, 502, 503, 504].includes(error?.status) ||
["TypeError", "TimeoutError"].includes(error?.name) ||
/failed to fetch|network|timeout|HTTP\s*(?:408|429|5\d\d)|server response\s*\((?:408|429|5\d\d)\)/i.test(error?.message || "");
}
async function retryReaderProxy(open) {
for (let attempt = 0; ; attempt++) {
readerContext.assertReaderActive();
try {
return await open();
} catch (error) {
if (attempt >= 2 || !readerContext.retryableReaderProxyError(error)) throw error;
await readerContext.waitForReader(350 * (attempt + 1));
}
}
}
function fetchReaderUrl(rawUrl, requestInit = {}) {
if (String(rawUrl).includes("/api/reader-bucket-resource?"))
return readerContext.fetchWithReaderTimeout(rawUrl, readerContext.READER_PROXY_TIMEOUT_MS, requestInit);
const proxyUrl = readerContext.readerContentUrl(rawUrl);
return readerContext.retryReaderProxy(async () => {
const response = await readerContext.fetchWithReaderTimeout(proxyUrl, readerContext.READER_PROXY_TIMEOUT_MS, requestInit);
if (response.ok) return response;
await response.body?.cancel();
throw Object.assign(new Error(`HTTP ${response.status}`), { status: response.status });
}).catch((error) => {
readerContext.assertReaderActive();
requestInit.signal?.throwIfAborted();
if (readerContext.retryableReaderProxyError(error)) throw error;
return readerContext.fetchWithReaderTimeout(rawUrl, readerContext.READER_PROXY_TIMEOUT_MS, requestInit);
});
}
function fetchReaderResponse() {
const early = window.__VOICE_PDF_PRELOAD__;
if (early?.manifest && early.manifestUrl === new URL(readerContext.sourceUrl, location.href).href) {
const pending = early.manifest;
early.manifest = null;
return pending.then(
(response) => (response?.ok ? response : readerContext.fetchReaderUrl(readerContext.sourceUrl)),
() => readerContext.fetchReaderUrl(readerContext.sourceUrl)
);
}
return readerContext.fetchReaderUrl(readerContext.sourceUrl);
}
function prepareReaderDocument(engineTasks) {
const engines = Promise.all(engineTasks);
const response = Promise.race([readerContext.fetchReaderResponse(), engines.then(() => new Promise(() => {}))]);
// Rendering owns both promises; early rejection must not become unhandled.
response.catch(() => {});
engines.catch(() => {});
return {response, engines};
}
function readPreparedBytes(response, engines, limit = VoiceOfMLReaderSecurity.LIMITS.documentBytes) {
return Promise.all([Promise.resolve(response).then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return VoiceOfMLReaderSecurity.readBytes(response,limit);
}), engines]).then(([bytes]) => bytes);
}
function loadTextDocument() {
return readerContext.fetchReaderResponse().then((response) => ({ response }));
}
function loadMarkdownDocument() {
return readerContext.prepareReaderDocument([readerContext.loadScript(readerContext.MARKED_URL), readerContext.loadScript(readerContext.PURIFY_URL)]);
}
function loadHtmlDocument() {
const {response, engines} = readerContext.prepareReaderDocument([readerContext.loadScript(readerContext.PURIFY_URL)]);
return {response, engine:engines};
}
function loadMediaDocument() {
return Promise.resolve(null);
}
async function loadImageDocument(target = readerContext.sourceUrl) {
const deadline = performance.now() + readerContext.READER_PROXY_TIMEOUT_MS;
const image = new Image();
image.className = "reader-image";
image.alt = readerContext.documentState.title;
image.decoding = "async";
try {
return await readerContext.imageLoader.load(image, readerContext.readerContentUrl(target), readerContext.readerAbortController.signal, deadline);
} catch (error) {
readerContext.assertReaderActive();
if (error?.code === "READER_RESOURCE_LIMIT") throw error;
return readerContext.imageLoader.load(image, target, readerContext.readerAbortController.signal, deadline);
}
}
function loadChapterManifestDocument() {
const target = readerContext.chapterManifestUrl || readerContext.sourceUrl, early = window.__VOICE_READER_CHAPTER_PRELOAD__;
if (early?.manifest && early.manifestUrl === new URL(target, location.href).href) {
const pending = early.manifest;
early.manifest = null;
return pending.then(response => response.ok ? response : readerContext.fetchReaderUrl(target), () => readerContext.fetchReaderUrl(target));
}
return readerContext.fetchReaderUrl(target);
}
return {
loadScript,
fetchWithReaderTimeout,
retryableReaderProxyError,
retryReaderProxy,
fetchReaderUrl,
fetchReaderResponse,
prepareReaderDocument,
readPreparedBytes,
loadTextDocument,
loadMarkdownDocument,
loadHtmlDocument,
loadMediaDocument,
loadImageDocument,
loadChapterManifestDocument
};
}