diff --git a/bugs/05-editor-mainline/done/2026-06-04-pdf-resource-tab-fourth-open-stall.md b/bugs/05-editor-mainline/done/2026-06-04-pdf-resource-tab-fourth-open-stall.md index fdc1bffc..724636d3 100644 --- a/bugs/05-editor-mainline/done/2026-06-04-pdf-resource-tab-fourth-open-stall.md +++ b/bugs/05-editor-mainline/done/2026-06-04-pdf-resource-tab-fourth-open-stall.md @@ -1,19 +1,21 @@ -# PDF resource tab 连续打开第 4 个文件卡住 +# resource tab 连续打开多个预览后卡住 ## 现象 - 在文档页 resource tab 内连续打开多个 PDF 时,第 4 个 PDF 必现卡住。 - 卡住时 tab 标题已经切换,但 iframe 仍停在 `about:blank` 或空 body,PDF canvas 数量为 0。 - 单独打开 `/pdf-preview` 页面、或独立页面中复用单个 iframe 连续切换 PDF,均不能复现。 +- PDF 改成 tab 内 canvas 渲染后,第 4 个 PDF 可打开,但继续打开第 5 个仍会卡住;表格预览第 4 个仍会卡住。 ## 根因 -PDF resource tab 通过 iframe 加载 `/pdf-preview`,连续创建/替换 PDF iframe 后会进入空白加载状态。问题不在具体 PDF 文件,也不在页数懒加载策略。 +根因不是具体 PDF 文件,也不是页数懒加载策略,而是 resource tab 为每个本地资源预览都常驻一个 `/api/local-folder/events` EventSource。连续打开多个 PDF / Office 预览后,隐藏 tab 的 EventSource 仍占用浏览器同源连接槽,新的 Office iframe 或 PDF 文件读取请求会被排队,表现为空白预览。 ## 修复 - resource tab 内的 PDF 改为直接使用 pdf.js 渲染 canvas,不再通过 iframe 打开 `/pdf-preview`。 - 关闭或替换 PDF resource tab 时销毁 pdf.js document,释放渲染资源。 +- resource tab 的本地文件监听改为 active-only:只保留当前激活资源 tab 的 EventSource,切换 tab 或回到页面 tab 时关闭其它资源 tab 的监听。 - 移除 `/pdf-preview` 内此前排查用的懒加载、占位页和多页预渲染逻辑,保留 2x 起步渲染清晰度。 - `dev:hot` 默认把 loopback bind 修正为 `0.0.0.0:`,避免外网访问不到 3000。 @@ -25,6 +27,8 @@ PDF resource tab 通过 iframe 加载 `/pdf-preview`,连续创建/替换 PDF i - `literature/Ouyang_2024_Frontiers_aged_Camellia_oleifera_oil_AD.pdf` - `literature/Leclere_2025_OCL_Tea_oil_concentrate.pdf` - 第 4 个 PDF 结果:`canvasCount=15`,`iframeCount=0`,`status=15 / 15`,浏览器错误 0。 +- 真实浏览器连续打开 5 个 DHA PDF,第 5 个 `Sun2022_DHA_Maillard_Kinetics.pdf` 结果:`canvasCount=8`,`status=8 / 8`,浏览器错误 0。 +- 真实浏览器连续打开 5 个 XLSX 预览,第 4/5 个均 `status=完成`,`tableCount=1`,浏览器错误 0。 - `node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js` - `node scripts/task-dev-hot-plan-test.js` - `cargo test -p mnote-web --manifest-path rust/Cargo.toml pdf_preview_page_does_not_render_visible_toolbar` diff --git a/rust/crates/mnote-web/browser/document-resource-tab-runtime.js b/rust/crates/mnote-web/browser/document-resource-tab-runtime.js index d79989d4..11012f73 100644 --- a/rust/crates/mnote-web/browser/document-resource-tab-runtime.js +++ b/rust/crates/mnote-web/browser/document-resource-tab-runtime.js @@ -708,6 +708,7 @@ export const createResourceTabRuntime = (dependencies = {}) => { if (!activeEntry && window.__mnoteIntendedSlashRoot instanceof HTMLElement) window.__mnoteIntendedSlashRoot = null; if (role === 'primary') syncActiveResourceFileTreeRow(activeResource); syncActiveResourceUrlState(activeResource, role); + syncActivePassiveResourceWatch(role, activeResource); syncOpenEditorsSnapshot(); }; @@ -898,12 +899,7 @@ export const createResourceTabRuntime = (dependencies = {}) => { } entry.mindmapRuntime = null; } - if (entry.resourceWatchEventSource) { - try { - entry.resourceWatchEventSource.close(); - } catch (_) {} - entry.resourceWatchEventSource = null; - } + closePassiveResourceWatch(entry); if (entry.panel instanceof HTMLElement) entry.panel.replaceChildren(); }; @@ -1092,11 +1088,40 @@ export const createResourceTabRuntime = (dependencies = {}) => { } }; + const closePassiveResourceWatch = (entry) => { + if (!entry?.resourceWatchEventSource) return; + try { + entry.resourceWatchEventSource.close(); + } catch (_) {} + entry.resourceWatchEventSource = null; + if (entry.panel instanceof HTMLElement) entry.panel.removeAttribute('data-mnote-resource-watch-ready'); + }; + + const isActiveResourceTabEntry = (entry) => { + if (!(entry?.panel instanceof HTMLElement)) return false; + if (entry.panel.hidden === false) return true; + return entry.tab instanceof HTMLElement && entry.tab.getAttribute('aria-selected') === 'true'; + }; + + const closeInactivePassiveResourceWatches = (activeEntry) => { + const role = normalizePaneRole(activeEntry?.paneRole || 'primary'); + resourceTabRegistry.forEach((entry) => { + if (entry === activeEntry) return; + if (normalizePaneRole(entry?.paneRole) !== role) return; + closePassiveResourceWatch(entry); + }); + }; + const installPassiveResourceWatch = (entry) => { if (!entry || entry.session || typeof window.EventSource !== 'function') return; const rootUri = String(entry.rootUri || '').trim(); const path = String(entry.path || '').trim(); if (!rootUri || !path) return; + if (!isActiveResourceTabEntry(entry)) { + closePassiveResourceWatch(entry); + return; + } + closeInactivePassiveResourceWatches(entry); if (entry.resourceWatchEventSource) { try { entry.resourceWatchEventSource.close(); @@ -1130,6 +1155,17 @@ export const createResourceTabRuntime = (dependencies = {}) => { }); }; + const syncActivePassiveResourceWatch = (paneRole, activeResource) => { + const activeEntry = activeResource ? resourceTabRegistry.get(activeResource) : null; + if (!activeEntry) { + resourceTabRegistry.forEach((entry) => { + if (normalizePaneRole(entry?.paneRole) === normalizePaneRole(paneRole)) closePassiveResourceWatch(entry); + }); + return; + } + installPassiveResourceWatch(activeEntry); + }; + const releaseInlinePdfResource = (entry) => { if (!entry) return; const pdf = entry.inlinePdfDocument;