fix(sync): bail out of deferred sync tasks when the document is gone

Every push and pull runs deferred — updateProgress and getProgress hand
their work to UIManager:scheduleIn(0.5), and API replies are consumed in
UIManager:nextTick closures — so by the time the task runs, the ReaderUI
may have released its document (close/teardown race) or a slow async
load may not have attached it yet (the CRE full-render window: "cached
rendering is invalid" leaves the reader document-less for seconds on
big EPUBs). The task then dereferenced self.ui.document unguarded and
the unhandled error inside a UIManager task killed the whole app:

    main.lua:846: attempt to index field 'document' (a nil value)

    ...in function 'collectBookData'
    ...in function '_doUpdateProgress'
    ...in function 'action' (uimanager _checkTasks)

Observed as a hard crash on the auto-push (pages_before_update = 2)
right after opening an EPUB that needed a full re-render.

getBookhoardUUID and getContextText already nil-guarded their document
access; the task entry points never did. Guard them:

- _doUpdateProgress: bail before collectBookData — the single choke
  point for every push source (periodic, close, suspend, menu, the
  legacy bootstrap push), so one guard covers the whole class
- _doGetProgress: bail at entry, and re-validate first thing inside the
  nextTick closure (its body and applyServerAnnotations touch
  document.info long after the request was issued)
- getFileSHA256: return nil instead of indexing doc_settings/document
  (all callers already handle nil)

No sync-payload or endpoint changes; server behavior untouched.
This commit is contained in:
John O'Keefe
2026-09-19 10:35:48 -04:00
parent 75b498b413
commit d584f17092
+15
View File
@@ -737,6 +737,9 @@ function Bookhoard:getLastProgress()
end end
function Bookhoard:getFileSHA256() function Bookhoard:getFileSHA256()
-- Reached from deferred flows too (e.g. _linkBookThenPull after a
-- network-connected event); callers already handle nil.
if not (self.ui and self.ui.doc_settings and self.ui.document) then return nil end
local cached = self.ui.doc_settings:readSetting("bookhoard_sha256") local cached = self.ui.doc_settings:readSetting("bookhoard_sha256")
if cached then return cached end if cached then return cached end
@@ -1208,6 +1211,12 @@ function Bookhoard:updateProgress(ensure_networking, interactive)
end end
function Bookhoard:_doUpdateProgress(interactive) function Bookhoard:_doUpdateProgress(interactive)
-- Every push runs deferred (scheduleIn(0.5) in updateProgress), so it
-- can fire after the ReaderUI released its document (close/teardown
-- race, e.g. "attempt to index field 'document'" killing the app) or
-- before a slow async load attached it (the CRE full-render window).
-- Bail silently; the next real page turn re-pushes.
if not (self.ui and self.ui.document) then return end
local book_data = self:collectBookData() local book_data = self:collectBookData()
if not book_data then return end if not book_data then return end
@@ -1441,6 +1450,9 @@ function Bookhoard:_bootstrapUUIDThenPull(interactive)
end end
function Bookhoard:_doGetProgress(interactive) function Bookhoard:_doGetProgress(interactive)
-- Pulls are deferred too (see _doUpdateProgress): the document can be
-- gone — or not yet attached — by the time the timer fires.
if not (self.ui and self.ui.document) then return end
local book_uuid = self:getBookhoardUUID() local book_uuid = self:getBookhoardUUID()
if not book_uuid then return end if not book_uuid then return end
@@ -1448,6 +1460,9 @@ function Bookhoard:_doGetProgress(interactive)
local ok, result = api:getMetadata(book_uuid) local ok, result = api:getMetadata(book_uuid)
UIManager:nextTick(function() UIManager:nextTick(function()
-- The HTTP reply landed after the defer; re-validate before the
-- document-dependent body below (applyServerAnnotations, sync).
if not (self.ui and self.ui.document) then return end
if not ok or not result then if not ok or not result then
if interactive then if interactive then
UIManager:show(InfoMessage:new{ UIManager:show(InfoMessage:new{