From 57434dde6e09270462d5b2c253f7c017e36060c6 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Tue, 18 Aug 2026 19:14:18 -0400 Subject: [PATCH] Sync fixes for underpowered devices: thin client, server-heavy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - collectAnnotations no longer calls getPageFromXPointer per annotation (a CRE-engine lookup each — the most expensive thing the sync loop did on weak hardware). Paging documents compute percentage arithmetically; CRE documents omit it entirely and the server derives it from the locator against the actual book. - chapter/page are sent raw; the server's tolerant types accept strings, empty strings, and CRE xpointers in the page field. Previously a single annotation with chapter:'' failed the ENTIRE progress push with a 400, silently killing progress sync too. - applyServerAnnotations validates pos0 before applying: rolling documents require '/body/...' xpointers, paging documents numeric pages. Unplaceable locators are skipped instead of becoming junk local bookmarks that would re-push as duplicates. --- main.lua | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/main.lua b/main.lua index fb9d717..2faac39 100644 --- a/main.lua +++ b/main.lua @@ -827,19 +827,21 @@ function Bookhoard:collectAnnotations() if not all_bookmarks then return bookmarks, highlights, notes end - local file_sha256 = self:getFileSHA256() local total_pages = self.ui.document:getPageCount() + local has_pages = self.ui.document.info.has_pages for _, bm in ipairs(all_bookmarks) do - local page_num = bm.page - if type(page_num) == "string" and self.ui.document.info.has_pages == false then - page_num = self.ui.document:getPageFromXPointer(page_num) - end - page_num = tonumber(page_num) or 0 + -- Thin-client policy: no per-annotation CRE lookups here (a + -- getPageFromXPointer call each is the most expensive thing this + -- loop can do on weak hardware). Percentages are arithmetic for + -- paging documents and simply omitted for CRE documents — the + -- server derives them from the locator against the actual book. + local page_num = tonumber(bm.page) or 0 + local percentage = has_pages and total_pages > 0 and (page_num / total_pages) or nil - local percentage = total_pages > 0 and (page_num / total_pages) or 0 local chapter = bm.chapter or "" + if type(chapter) == "table" then chapter = "" end local entry = { chapter = chapter, @@ -849,9 +851,11 @@ function Bookhoard:collectAnnotations() pos1 = bm.pos1 or "", page = tostring(bm.page or ""), text = bm.text or "", - percentage = Math.roundPercent(percentage), book_sha256 = file_sha256, } + if percentage then + entry.percentage = Math.roundPercent(percentage) + end local has_text = bm.text and bm.text ~= "" local has_notes = bm.notes and bm.notes ~= "" @@ -1159,9 +1163,23 @@ function Bookhoard:applyServerAnnotations(annotations) return pos0 end + local function isValidPos0(pos0) + -- Rolling (CRE) documents address bookmarks/highlights by xpointer; + -- paging documents (PDF/comics/DjVu) by page number. Anything else + -- (a raw "cfi:" locator, an epubcfi(...) string, a JSON anchor, …) + -- cannot be placed in this document and must not become a local + -- bookmark: it could never be matched again and would be re-pushed + -- to the server as a junk duplicate on the next sync. + if pos0 == "" then return false end + if has_pages then + return tonumber(pos0) ~= nil + end + return pos0:sub(1, 7) == "/body/" + end + local function addOrUpdate(server_entry, has_text) local pos0 = server_entry.pos0 or "" - if pos0 == "" then return end + if not isValidPos0(pos0) then return end local idx = findLocalByPos0(pos0) if idx then local bm = local_bms[idx]