active / System Design Archive

Reader System

Reader System 把 EPUB、PDF、阅读位置、高亮和抽屉体验收敛成一个长期常驻的阅读运行层,目标是接近现代阅读产品,而不是浏览器下载器。

Book Drawer ReaderReader PoolRuntime PersistencePDF.jsepub.jsReader MemoryPublic Edit IntakeBlockNote Spike
Inspiration

借鉴对象

  • Readest
  • Apple Books
  • Kindle
  • 微信读书
  • Omnivore
  • BlockNote
  • Tiptap
  • ProseMirror
  • Hypothesis
Rejected

拒绝的方案

  • iframe 直接打开 OpenList 文件,因为它不能接入目录、阅读记忆、高亮和 Graph。
  • 点击后才加载完整 Reader Runtime,因为 PDF.js / EPUB.js 冷启动会把体感延迟全部压到用户点击之后。
  • 抽屉内 PDF 翻页控件,因为首页 drawer 的阅读体验必须像文章一样连续。
Runtime

运行方式

  • OpenList 只提供缓存后的 raw 字节;访客请求不能临时回源下载或解析。
  • BookDrawerReader 是首页唯一常驻 reader island,维护最近 3 本 Reader Pool。
  • 原生 drawer 脚本必须把最近一次 book-open detail 缓存在 window.__emptyinkpotPendingBookDrawerOpen,避免 React island hydrate 晚于用户点击时丢事件。
  • BookReader shell 保持轻量;PDF runtime 与 EPUB runtime 按 sourceType 拆包预热。
  • hover / focus 意图阶段允许把目标书籍预挂进隐藏 Reader Pool;点击时复用已出生的 reader,而不是才创建 reader。
  • PdfReader 只做 mode dispatch;drawer mode 进入独立 PdfDrawerReader,page mode 进入独立 PdfPageReader,避免条件 hooks 把两套 reader runtime 混在同一个组件生命周期里。
  • PDF 在 drawer mode 不再走 react-pdf Document 组件,而是直接使用 pdfjs-dist + 自定义 PDFDataRangeTransport 接管 Range 请求;首个 768KB Range 分片同时作为 length probe 与 initialData,并把 transport 标记为 progressiveDone,避免 PDF.js 把首段当成未结束 full stream。
  • PDF drawer direct runtime 不把 URL 交给 PDF.js;disableAutoFetch 必须保持 true,让 PDF.js 只请求解析第一页、xref 和 page tree 所需 Range,避免在文档 resolve 前把整本 PDF 拆成一串 206 拉完。
  • PDF drawer 首屏不能等待浏览器端 PDF.js 文档 promise;服务端 /api/openlist/page 从已缓存 PDF 文件渲染指定页为真实页面 JPEG 并落盘,抽屉先展示缓存正文页,PDF.js 后台再接管连续 reader。
  • PDF 页面缓存必须纳入导入管线:/api/openlist/pages/prewarm 在 files/prewarm 后批量准备前几页;访客继续向下滚动时 CachedPdfPageList 按批次请求后续页,每页只在服务端渲染一次,之后复用缓存。
  • PDF 在 drawer mode 使用真实封面作为即时首屏,PDF.js direct runtime 在后台完成首页渲染后再淡入连续滚动正文。
  • 即时封面首屏由 Astro 静态模板直接输出缓存封面 URL,不能依赖 BookCover React island hydrate 后才出现。
  • PDF 在 drawer mode 使用连续滚动多页和 IntersectionObserver 懒渲染,完整 reader 页面保留单页控制。
  • /edit-intake/ 是可编辑 projection surface spike:BlockNote 只作为 MyBlog UI substrate,页面生成 public-edit-intake.v1 JSON,不直接写 DataBase CCG/CDM/AST。
  • MyBlog 侧评论、高亮、rewrite_block 和 insert_asset_block 必须通过 DataBase public edit intake 边界;评论/高亮落 Annotation Graph overlay,真实内容修改进入 review 后的 Graph Edit Operation。
Tradeoff

取舍

  • 常驻 Runtime 会占用更多前端内存,但换来二次打开和近场阅读的稳定体感。
  • PDF 抽屉首屏先显示缓存真实封面不是伪造正文,而是避免 PDF.js 初始化期间出现空白;真正页面渲染完成后由 reader 接管。
  • 完整 reader 页面仍保留 react-pdf 兜底,首页 drawer 先切 direct PDF.js runtime;长期要评估 react-pdf-viewer 或同级成熟内核。
  • MOBI 不在浏览器 reader 内硬解析,后续应在导入管线转 EPUB。
Future Direction

后续方向

  • 把 PDF worker 单例和 EPUB book object 缓存收敛为显式 Runtime Registry。
  • 后台导入阶段增加 Calibre 转换,把 MOBI 标准化为 EPUB reader asset。
  • 把目录、搜索、高亮和批注统一到 Knowledge Runtime。
  • 把 /edit-intake/ spike 接入真实 projection package anchorMap,输出 comment/highlight/rewrite_block/insert_asset_block/moderation fixtures。
  • 在 anchor/intake 边界稳定前,不引入 Yjs/Hocuspocus、Payload 或 Hypothesis storage 作为内容真源。