fix: Protect offline storage state
Some checks are pending
build / binary (push) Waiting to run
build / container (push) Waiting to run
test / test (push) Waiting to run
test / lint-openapi (push) Waiting to run

This commit is contained in:
Chmouel Boudjnah 2026-09-08 00:03:31 +02:00
parent bcbf0b382e
commit 940c942196
4 changed files with 42 additions and 5 deletions

View file

@ -13,6 +13,8 @@ import {
const buttons = [...document.querySelectorAll("[data-offline-book]")];
if (buttons.length) {
const offlineCapable = globalThis.isSecureContext &&
!!globalThis.navigator?.serviceWorker && !!globalThis.navigator?.locks;
const prefix = deploymentPrefix();
const csrf = document.querySelector('input[name="csrf"]')?.value || "";
const auth = readerAuth({
@ -47,6 +49,11 @@ if (buttons.length) {
};
for (const button of buttons) {
if (!offlineCapable) {
button.disabled = true;
say(button, "Offline reading requires a secure browser connection.", true);
continue;
}
activeAccount(storagePartition())
.then(account => account && updateReadyState(button, account))
.catch(error => console.warn("offline download state could not be loaded", error));
@ -81,6 +88,7 @@ if (buttons.length) {
await removeBookSnapshots({
partition,
account: identity.account,
epoch: context.epoch,
bookID: button.dataset.offlineBook,
});
button.dataset.offlineReady = "";

View file

@ -478,12 +478,13 @@ export async function updateOfflineAnnotation({
}
export async function removeOfflineAnnotation({
partition = storagePartition(), account, epoch, bookID, id,
partition = storagePartition(), account, epoch, bookID, id, removeAnnotation = true,
} = {}) {
if (!partition || !account || !bookID || !id) return;
return withDB(async db => {
await guardedWork(db, [ANNOTATIONS, OUTBOX], { partition, account, epoch }, tx => {
tx.objectStore(ANNOTATIONS).delete(keyForAnnotation({ partition, account, bookID, id }));
if (removeAnnotation)
tx.objectStore(ANNOTATIONS).delete(keyForAnnotation({ partition, account, bookID, id }));
const outbox = tx.objectStore(OUTBOX);
const cursorRequest = outbox.index("account").openCursor(IDBKeyRange.bound(
[partition, account, "", 0],
@ -1015,11 +1016,15 @@ export function localPublicationRequest(snapshot, resources, path) {
return Promise.resolve({ ok: false, status: 404, url: clean });
}
export async function removeBookSnapshots({ partition = storagePartition(), account, bookID } = {}) {
export async function removeBookSnapshots({
partition = storagePartition(), account, epoch, bookID,
} = {}) {
const selectedAccount = account || await activeAccount(partition);
if (!selectedAccount) return;
return withDB(async db => {
await transactionWork(db, [SNAPSHOTS, RESOURCES], "readwrite", tx => {
await guardedWork(db, [SNAPSHOTS, RESOURCES], {
partition, account: selectedAccount, epoch,
}, tx => {
const snapshots = tx.objectStore(SNAPSHOTS);
const resources = tx.objectStore(RESOURCES);
let records;
@ -1041,6 +1046,26 @@ export async function removeBookSnapshots({ partition = storagePartition(), acco
});
}
async function removeStagingSnapshots(context, bookID) {
return withDB(async db => {
await guardedWork(db, [SNAPSHOTS, RESOURCES], context, tx => {
const snapshots = tx.objectStore(SNAPSHOTS);
const resources = tx.objectStore(RESOURCES);
const request = snapshots.index("book").getAll([
context.partition, context.account, bookID,
]);
request.onsuccess = () => {
for (const snapshot of request.result) {
if (snapshot.state !== "staging") continue;
for (const href of snapshot.resourceHrefs || [])
resources.delete([snapshot.key, href]);
snapshots.delete(snapshot.key);
}
};
});
});
}
export async function downloadPublication({
request,
@ -1089,6 +1114,7 @@ export async function downloadPublication({
author: (manifest.metadata?.author || {}).name || "",
createdAt: Date.now(),
};
await removeStagingSnapshots(context, bookID);
await putStagingSnapshot(snapshot);
const queue = [...plan.resources.values()];
const seen = new Set(queue.map(resource => resource.href));

View file

@ -99,7 +99,9 @@ async function render() {
remove.addEventListener("click", async () => {
remove.disabled = true;
try {
await removeBookSnapshots({ partition, account, bookID: snapshot.bookID });
await removeBookSnapshots({
partition, account, epoch: context.epoch, bookID: snapshot.bookID,
});
notifyOfflineChange();
await render();
} catch (error) {

View file

@ -588,6 +588,7 @@ async function handleAnnotationConflict(local, server, record = null) {
account: offlineAccount,
bookID: cfg.bookID,
id: server.id,
removeAnnotation: false,
});
} else {
await updateOfflineAnnotation({