72233e1c6a
* docs(sync): design spec for syncing reading status (#4634) Field-level LWW for reading_status (dedicated reading_status_updated_at), a new 'abandoned' status in the Readest UI, and a koplugin bridge to KOReader's native summary.status (whole-library apply + capture). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(sync): implementation plan for syncing reading status (#4634) Bite-sized TDD tasks across 3 parts: A) cloud field-level LWW (reading_status_updated_at on server upsert + client pull-merge), B) 'abandoned'/On-hold status in the Readest UI, C) koplugin bridge to KOReader summary.status (mapping + reconcile + whole-library apply/capture). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): add reading_status_updated_at for field-level status LWW (#4634) * feat(sync): stamp readingStatusUpdatedAt on status change in updateBookProgress Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): stamp status timestamp on explicit library status edits * feat(sync): resolve reading status by its own timestamp in client pull-merge * feat(sync): resolve reading status by its own timestamp in server upsert (#4634) * fix(sync): tighten reading-status merge typing + strengthen test (A5 review) Replace as-unknown-as double-casts at read sites with typed locals (clientBook/serverBook); retain a single as-unknown-as only at the server-wins construction site where the static type is too narrow. Strengthen test 3 to assert both fields with toEqual. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(library): render the 'On hold' (abandoned) status badge * feat(library): add 'Mark as On hold' actions + i18n for abandoned status Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * i18n: translate 'On hold' and 'Mark as On hold' for the abandoned status (#4634) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(koplugin): add reading-status mapping + reconcile between Readest and KOReader * feat(koplugin): persist + sync reading_status_updated_at in LibraryStore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(koplugin): bridge reading status to KOReader summary.status on library sync (#4634) * test(sync): cover koplugin v1->v2 migration + tighten status-sync tests (final review) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(sync): redesign KOReader first-sync (decisive-only + bootstrap) (#4634) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(koplugin): safe first-sync of reading status (decisive-only + bootstrap) (#4634) KOReader auto-sets summary.status='reading' on first open, and legacy Readest statuses have reading_status_updated_at=0, so pure timestamp LWW let opening a finished book downgrade it. Restrict sync to deliberate statuses (finished/ complete, abandoned/on-hold, unread->clear); never capture KO 'reading'/'New'. On the unsynced baseline (Readest ts=0) conflicts resolve Readest-authoritative, then stamp now_ms to exit bootstrap into steady-state LWW. reconcile now returns write_ko/write_store flags; statussync captures now_ms once and equalizes both sides (convergent, idempotent, resumable). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(koplugin): cover remaining first-sync graph cells + document sort effect (review) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
46 lines
2.0 KiB
Lua
46 lines
2.0 KiB
Lua
-- statussync.lua — bridge LibraryStore.reading_status <-> KOReader's per-book
|
|
-- summary.status. The decision is delegated to the pure readingstatus.reconcile;
|
|
-- this module only walks local-present rows and performs the chosen IO. The IO
|
|
-- is injected via `deps` so it unit-tests without DocSettings; production wires
|
|
-- a DocSettings-backed deps in librarywidget.
|
|
local readingstatus = require("library.readingstatus")
|
|
|
|
local M = {}
|
|
|
|
-- deps: { now_ms(), open_summary(file_path) -> {status, modified}|nil,
|
|
-- write_status(file_path, ko_status_or_nil) }
|
|
function M.reconcileLocalStatuses(store, deps)
|
|
if not store or not deps then return 0 end
|
|
-- Capture "now" once so every book baselines at the same instant.
|
|
local now_ms = deps.now_ms()
|
|
local changed = 0
|
|
local rows = store:listBooks({})
|
|
for _, row in ipairs(rows) do
|
|
if row.local_present == 1 and row.file_path then
|
|
local summary = deps.open_summary(row.file_path) or {}
|
|
local ko_ts = readingstatus.parse_modified_ms(summary.modified) or now_ms
|
|
local r = readingstatus.reconcile(
|
|
{ reading_status = row.reading_status,
|
|
reading_status_updated_at = row.reading_status_updated_at },
|
|
{ status = summary.status, ts = ko_ts },
|
|
now_ms)
|
|
-- Sidecar first (durable), then the store (durable + bumps
|
|
-- updated_at for the push). A crash between re-reconciles this book
|
|
-- identically next pass — convergent and idempotent.
|
|
if r.write_ko then
|
|
deps.write_status(row.file_path, r.ko_status) -- ko_status may be nil (clear)
|
|
end
|
|
if r.write_store then
|
|
store:touchBook(row.hash, {
|
|
reading_status = r.readest_status,
|
|
reading_status_updated_at = r.ts,
|
|
})
|
|
end
|
|
if r.write_ko or r.write_store then changed = changed + 1 end
|
|
end
|
|
end
|
|
return changed
|
|
end
|
|
|
|
return M
|