fix(kobo): stop endless sync retries and cut round trips

Report success when Kobo rejects a delete for a tag it no longer has, so the device stops retrying
one it can never complete. Skip the full-library reconcile while a device is still draining pages,
and page fifty books at a time so a large re-announce costs a few round trips instead of hundreds.

Ref #1005
This commit is contained in:
neon
2026-08-14 02:26:46 +00:00
parent fddb41fdb9
commit 2b8e1d6a79
6 changed files with 221 additions and 44 deletions
+10 -6
View File
@@ -6,6 +6,7 @@ import { and, eq, inArray } from 'drizzle-orm';
import { Client } from 'pg';
import * as schema from '../src/db/schema';
import { SYNC_PAGE_SIZE } from '../src/modules/kobo/services/kobo-sync.service';
import { waitForCondition } from './e2e/app-harness';
import { createEpubFixture } from './e2e/reader-state-isolation/reader-state-isolation-fixture-builder';
import {
@@ -25,6 +26,9 @@ type NewEntitlement = { BookEntitlement: { Id: string }; ReadingState: Record<st
const MIGRATIONS_DIRECTORY = join(dirname(fileURLToPath(import.meta.url)), '../src/db/migrations');
// One book past a full page, so the suite keeps exercising a page boundary whatever SYNC_PAGE_SIZE is.
const LIBRARY_BOOK_COUNT = SYNC_PAGE_SIZE + 1;
function escapeIdentifier(identifier: string): string {
return `"${identifier.replace(/"/g, '""')}"`;
}
@@ -240,7 +244,7 @@ describe('Kobo multi-device library sync (e2e)', { timeout: 180_000 }, () => {
library = await createLibraryWithFolder(ctx, { name: `kobo-multi-device-${randomUUID()}` });
const paths = await Promise.all(
Array.from({ length: 6 }, (_, index) =>
Array.from({ length: LIBRARY_BOOK_COUNT }, (_, index) =>
createEpubFixture(library.folderPath, `kobo-multi-device-${index + 1}.epub`, {
title: `Kobo Multi Device ${index + 1}`,
uid: `urn:uuid:${randomUUID()}`,
@@ -291,7 +295,7 @@ describe('Kobo multi-device library sync (e2e)', { timeout: 180_000 }, () => {
const first = await sync(echoDevice);
const firstEntitlements = newEntitlements(first.entries);
expect(first.hasMore).toBe(true);
expect(firstEntitlements).toHaveLength(5);
expect(firstEntitlements).toHaveLength(SYNC_PAGE_SIZE);
for (const entitlement of firstEntitlements) {
await putReadingState(echoDevice, entitlement.BookEntitlement.Id, entitlement.ReadingState);
@@ -323,8 +327,8 @@ describe('Kobo multi-device library sync (e2e)', { timeout: 180_000 }, () => {
const bFirst = await sync(deviceB);
expect(aFirst.hasMore).toBe(true);
expect(bFirst.hasMore).toBe(true);
expect(entitlementIds(aFirst.entries)).toHaveLength(5);
expect(entitlementIds(bFirst.entries)).toHaveLength(5);
expect(entitlementIds(aFirst.entries)).toHaveLength(SYNC_PAGE_SIZE);
expect(entitlementIds(bFirst.entries)).toHaveLength(SYNC_PAGE_SIZE);
const aSecond = await sync(deviceA, aFirst.syncToken);
expect(aSecond.hasMore).toBe(false);
@@ -373,7 +377,7 @@ describe('Kobo multi-device library sync (e2e)', { timeout: 180_000 }, () => {
snapshots.map((snapshot) => snapshot.id),
),
);
expect(rows).toHaveLength(12);
expect(rows).toHaveLength(LIBRARY_BOOK_COUNT * 2);
expect(rows.every((row) => row.synced)).toBe(true);
});
@@ -482,7 +486,7 @@ describe('Kobo multi-device library sync (e2e)', { timeout: 180_000 }, () => {
.map((snapshot) => snapshot.id),
),
);
expect(aAndBRows).toHaveLength(12);
expect(aAndBRows).toHaveLength(LIBRARY_BOOK_COUNT * 2);
expect(aAndBRows.every((row) => row.synced)).toBe(true);
const [targetIdentity] = await ctx.db