feat(reader): make KOReader plugin sync non-blocking and resumable

Lifecycle handlers previously blocked on network work, so closing or suspending a book stalled the
device on slow or offline links. Persist snapshots first and drain them through a lifecycle outbox
with connected-state gates and phase-aware retries, so the UI stays responsive and pending work
survives restarts.

Move persistence behind a state manager with atomic flushes, and chunk sweeps, bulk downloads, and
annotation exchange through a background queue with checkpoints and transfer policies, so large
libraries make progress incrementally instead of restarting from scratch after an interruption.

Server side, add bulk manifest and page stats endpoints with batched queries and catalog query
filtering so the plugin can reconcile many books per round trip rather than one request per book.
This commit is contained in:
neon
2026-07-28 02:06:50 +00:00
parent 3aff8f521b
commit edc2a2ccfc
93 changed files with 12600 additions and 1458 deletions
+46
View File
@@ -287,6 +287,52 @@ export interface KoreaderCatalogDiscoverResponse {
discover: KoreaderCatalogBookListItem[];
}
// Everything a bulk download needs per downloadable file, so no per-book detail
// request remains. `fileHash` is the KOReader partial MD5 of the exact bytes the
// download route streams, so it doubles as the restart-validation checksum and
// as the digest that keys local match state.
export interface KoreaderCatalogManifestFile {
id: number;
format: string;
sizeBytes: number | null;
contentVersion: string;
fileHash: string | null;
downloadUrl: string;
devicePath: string;
}
export interface KoreaderCatalogManifestBook {
id: number;
title: string;
authors: string[];
seriesName: string | null;
seriesIndex: number | null;
formats: string[];
files: KoreaderCatalogManifestFile[];
}
export interface KoreaderCatalogManifestPage {
items: KoreaderCatalogManifestBook[];
hasNext: boolean;
// Opaque, bound to the user, the normalized filter and the manifest snapshot.
nextCursor: string | null;
manifestVersion: string;
// Set when a supplied cursor was minted under a different cursor contract.
// A library change during a run does not set it: the keyset orders by immutable
// book id, so enumeration continues and only manifestVersion carries the change.
// On a restart, already-published files are skipped by size and hash validation,
// so no transfer work is repeated.
restartRequired: boolean;
}
export type KoreaderPluginCapability = "catalogBulkManifest";
export interface KoreaderPluginVersionInfo {
pluginVersion: string;
serverVersion: string;
capabilities: KoreaderPluginCapability[];
}
export interface KoreaderCatalogReadStatusResult {
readStatus: KoreaderCatalogSettableReadStatus;
}