Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs (Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet (marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert (Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen, a öffnet die Liste (springen, Notiz ändern, löschen). Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt. Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf (tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der Server-Smoke schreibt und mischt zwei Geräte. 75 Tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
52 lines
1.9 KiB
Python
52 lines
1.9 KiB
Python
"""On-disk cache for the (FlatBook, BookLayout) pair a book open computes —
|
|
extraction + row-layout for very large books (tens of thousands of blocks)
|
|
can take several seconds each; reopening the same book at the same terminal
|
|
width should be near-instant instead of paying that cost again every time.
|
|
|
|
Not a correctness-critical cache: any miss (new book, different width, edited
|
|
file) just falls back to recomputing from scratch, so a stale/corrupt cache
|
|
entry is handled by overwriting it, never by crashing the reader.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import pickle
|
|
from pathlib import Path
|
|
|
|
from . import paths
|
|
from .blocks import FlatBook
|
|
from .layout import BookLayout
|
|
|
|
_CACHE_VERSION = 3 # 2: blocks carry char_start/char_len; 3: chapter_srcs
|
|
_CACHE_DIR = paths.cache_dir() / "layout_cache"
|
|
|
|
|
|
def _cache_key(path: Path, width: int) -> str:
|
|
stat = path.stat()
|
|
raw = f"v{_CACHE_VERSION}|{path.resolve()}|{stat.st_size}|{stat.st_mtime_ns}|{width}"
|
|
return hashlib.sha256(raw.encode()).hexdigest()[:32]
|
|
|
|
|
|
def load(path: Path, width: int) -> tuple[FlatBook, BookLayout] | None:
|
|
cache_file = _CACHE_DIR / f"{_cache_key(path, width)}.pickle"
|
|
if not cache_file.exists():
|
|
return None
|
|
try:
|
|
with cache_file.open("rb") as f:
|
|
book, book_layout = pickle.load(f)
|
|
if not isinstance(book, FlatBook) or not isinstance(book_layout, BookLayout):
|
|
return None
|
|
return book, book_layout
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def save(path: Path, width: int, book: FlatBook, book_layout: BookLayout) -> None:
|
|
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
cache_file = _CACHE_DIR / f"{_cache_key(path, width)}.pickle"
|
|
try:
|
|
with cache_file.open("wb") as f:
|
|
pickle.dump((book, book_layout), f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
except Exception:
|
|
pass # best-effort — a failed cache write shouldn't break reading
|