From d584f17092f40457a9ec6bb1b89b2205ed2bccb4 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 19 Sep 2026 10:35:48 -0400 Subject: [PATCH] fix(sync): bail out of deferred sync tasks when the document is gone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- main.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/main.lua b/main.lua index cdc401c..a04d9fb 100644 --- a/main.lua +++ b/main.lua @@ -737,6 +737,9 @@ function Bookhoard:getLastProgress() end 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") if cached then return cached end @@ -1208,6 +1211,12 @@ function Bookhoard:updateProgress(ensure_networking, interactive) end 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() if not book_data then return end @@ -1441,6 +1450,9 @@ function Bookhoard:_bootstrapUUIDThenPull(interactive) end 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() if not book_uuid then return end @@ -1448,6 +1460,9 @@ function Bookhoard:_doGetProgress(interactive) local ok, result = api:getMetadata(book_uuid) 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 interactive then UIManager:show(InfoMessage:new{