fix(sync): adopt keyed echoes of device-native annotations by pos0

Every served annotation carries a dedup key (the server stamps computed
keys at creation), so findLocal's pos0 fallback was unreachable for
keyed serves: when the key had no local holder it returned 'real new
entry' immediately. A device-native highlight had no local key until the
first pull stamped one — so the first serve-back of our own creation
applied as a SECOND local annotation. Duplicated on the device, while
the server stayed single-row (both copies resolve to the same computed
key on later pushes).

When the key has no local holder, now fall back to pos0 matching
restricted to keyless local entries of the same kind (v2: drawer set vs
page bookmark; v1: notes field holding selection text). That is our own
echo: stamp the key and update in place instead of duplicating. Keyed
local entries are never pos0-matched, so distinct annotations sharing a
position still cannot cross-match.
This commit is contained in:
2026-09-09 12:38:42 -04:00
parent 9425edb6e9
commit 75b498b413
+34 -3
View File
@@ -1545,7 +1545,7 @@ function Bookhoard:applyServerAnnotations(annotations)
end end
local has_pages = self.ui.document.info.has_pages local has_pages = self.ui.document.info.has_pages
local function findLocal(server_entry) local function findLocal(server_entry, has_text)
-- Identity match by dedup key: survives pos0 drift (improved -- Identity match by dedup key: survives pos0 drift (improved
-- server conversion) and, crucially, never cross-matches a -- server conversion) and, crucially, never cross-matches a
-- DIFFERENT annotation that merely shares the position. -- DIFFERENT annotation that merely shares the position.
@@ -1556,7 +1556,38 @@ function Bookhoard:applyServerAnnotations(annotations)
return i return i
end end
end end
return nil -- keyed but not present: a real new entry -- No key holder, but a keyed serve may still be THIS device's
-- own creation echoed back: the server stamped a computed key
-- on our push before we ever pulled one. Adopt a keyless local
-- annotation of the same kind at the identical pos0
-- (addOrUpdate stamps + updates it) instead of applying a
-- duplicate beside it. Keyed local entries are never
-- pos0-matched, so distinct annotations sharing a position
-- cannot cross-match.
local pos0 = server_entry.pos0 or ""
if pos0 ~= "" then
for i, bm in ipairs(entries) do
if not bm.bookhoard_dedup_key then
local is_highlight
if model == "v2" then
is_highlight = bm.drawer ~= nil
else
is_highlight = bm.notes ~= nil and bm.notes ~= ""
end
if is_highlight ~= has_text then
-- Wrong kind: a page bookmark at the same spot
-- is a different annotation, never an echo.
else
local p = bm.pos0
if type(p) == "table" then p = tostring(p.page or "") end
if p == pos0 then
return i
end
end
end
end
end
return nil -- genuinely absent: a real new entry
end end
-- Legacy serve (no key): fall back to pos0 matching. -- Legacy serve (no key): fall back to pos0 matching.
local pos0 = server_entry.pos0 or "" local pos0 = server_entry.pos0 or ""
@@ -1631,7 +1662,7 @@ function Bookhoard:applyServerAnnotations(annotations)
-- locators return above and are never tracked — absence of an -- locators return above and are never tracked — absence of an
-- annotation this device never held is not a deletion. -- annotation this device never held is not a deletion.
local kind = has_text and "highlight" or "bookmark" local kind = has_text and "highlight" or "bookmark"
local idx = findLocal(server_entry) local idx = findLocal(server_entry, has_text)
if idx then if idx then
local bm = entries[idx] local bm = entries[idx]
-- Legacy pos0 match on a device-native entry: stamp the key so -- Legacy pos0 match on a device-native entry: stamp the key so