add worker thread tracking

add stGen which gets bumped for every worker thread spawned by
loadBook

every thread captures stGen when it spawns and before actually loading
the book in the main thread, and in the worker thread it checks if
myGen == stGen

this throws away stale requests, meaning if you try and load another
book while one is still loading, it will ignore the one still loading
and start loading the latest request
This commit is contained in:
Marko Andjelic 2026-07-01 21:18:46 +01:00
commit 81c2caaf67
Signed by: marko
GPG key ID: 9C5E99C8C682FB59
3 changed files with 39 additions and 26 deletions

View file

@ -18,5 +18,6 @@ initialState title path =
stIndex = ChapterIndex 0,
stZoom = 1.0,
stTitle = title,
stBookPath = path
stBookPath = path,
stGen = 0
}

View file

@ -34,7 +34,6 @@ import Control.Monad.Except (ExceptT)
import Control.Monad.Reader (ReaderT)
import qualified Data.Map.Strict as M
import qualified Data.Text as T
-- | Position of a chapter within the linear reading order (the spine).
newtype ChapterIndex = ChapterIndex Int
deriving (Show, Eq, Ord, Num)
@ -99,7 +98,8 @@ data AppState = AppState
stIndex :: ChapterIndex,
stZoom :: Double,
stTitle :: T.Text,
stBookPath :: FilePath
stBookPath :: FilePath,
stGen :: Int
}
-- | A user request, produced by the keyboard / sidebar and handled centrally.

View file

@ -7,14 +7,14 @@
module UI (runApp) where
import Control.Concurrent.Async (async)
import Control.Concurrent.STM (TVar, atomically, modifyTVar', newTVarIO, readTVarIO, writeTVar)
import Control.Concurrent.STM (TVar, atomically, modifyTVar', newTVarIO, readTVarIO, writeTVar, readTVar)
import Control.Monad (unless, void, when)
import Data.GI.Base (AttrOp (..), on)
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Data.Word (Word32)
import Control.Exception (SomeException, try)
import EpubParser (EpubEnv, openEpub)
import EpubParser (EpubEnv, openEpub) -- brings the `BookInfo EpubEnv` instance into scope
import qualified GI.GLib as GLib
import qualified GI.GLib.Constants as GLibConst
import qualified GI.Gdk as Gdk
@ -168,27 +168,39 @@ wireEvents ctx = do
-- | Load the book's structure off the GTK thread, then populate the sidebar
-- and open the first (or last-read) chapter back on the main loop.
loadBook :: (BookInfo a) => a -> Ctx -> IO ()
loadBook env ctx = void $
async $ do
result <- loadStructure env
case result of
Left err -> putStrLn ("Failed to load book: " ++ err)
Right (BookStructure refs toc) -> do
atomically $ do
modifyTVar' (ctxState ctx) $ \s ->
s { stRefs = refs,
stToc = toc,
stCache = M.empty,
stIndex = ChapterIndex 0 }
writeTVar (ctxRender ctx) (renderChapter env)
postGtk $ do
Gtk.listBoxRemoveAll (appSidebar (ctxWidgets ctx))
populateSidebar (appSidebar (ctxWidgets ctx)) (displayRows refs toc)
saved <- loadLastRead
let start = case saved of
Just (path, idx) | path == bookFilePath env -> idx
_ -> ChapterIndex 0
handleAction ctx (GoToChapter start "")
loadBook env ctx = do
myGen <- atomically $ do
s <- readTVar (ctxState ctx)
let g = stGen s + 1
writeTVar (ctxState ctx) (s {stGen = g})
pure g
void $
async $ do
result <- loadStructure env
case result of
Left err -> putStrLn ("Failed to load book: " ++ err)
Right (BookStructure refs toc) -> do
committed <- atomically $ do
s <- readTVar (ctxState ctx)
let mine = stGen s == myGen
when mine $ do
writeTVar (ctxState ctx)
(s { stRefs = refs, stToc = toc,
stCache = M.empty, stIndex = ChapterIndex 0,
stTitle = bookTitle env, stBookPath = bookFilePath env})
writeTVar (ctxRender ctx) (renderChapter env)
pure mine
when committed $ postGtk $ do
s <- readTVarIO (ctxState ctx)
when (stGen s == myGen) $ do
Gtk.listBoxRemoveAll (appSidebar (ctxWidgets ctx))
populateSidebar (appSidebar (ctxWidgets ctx)) (displayRows refs toc)
saved <- loadLastRead
let start = case saved of
Just (path, idx) | path == bookFilePath env -> idx
_ -> ChapterIndex 0
handleAction ctx (GoToChapter start "")
epPicker :: Maybe Gtk.ApplicationWindow -> Ctx -> IO ()
epPicker window ctx = do