From ca5c860594bdda30a06d45f5027ef48072241624 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Fri, 3 Apr 2026 22:17:20 +0800 Subject: [PATCH] fix(kosync): don't normalize xpointer for more accurate progress sync, closes #3672 and closes #3616 (#3733) --- .../services/hardcover/HardcoverClient.test.ts | 1 + .../readest-app/src/app/reader/hooks/useKOSync.ts | 15 +++++++++------ .../src/app/reader/hooks/useProgressSync.ts | 11 +++++------ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/apps/readest-app/src/__tests__/services/hardcover/HardcoverClient.test.ts b/apps/readest-app/src/__tests__/services/hardcover/HardcoverClient.test.ts index 250db25d..a900fb8d 100644 --- a/apps/readest-app/src/__tests__/services/hardcover/HardcoverClient.test.ts +++ b/apps/readest-app/src/__tests__/services/hardcover/HardcoverClient.test.ts @@ -178,6 +178,7 @@ describe('HardcoverClient', () => { test('should handle rate limiting with retries', async () => { // request() does NOT call authenticate() so only 2 mock values are needed + vi.spyOn(console, 'warn').mockImplementation(() => {}); // First request fails with 429 then succeeds fetchMock.mockResolvedValueOnce({ diff --git a/apps/readest-app/src/app/reader/hooks/useKOSync.ts b/apps/readest-app/src/app/reader/hooks/useKOSync.ts index 116279d3..7a4e9f34 100644 --- a/apps/readest-app/src/app/reader/hooks/useKOSync.ts +++ b/apps/readest-app/src/app/reader/hooks/useKOSync.ts @@ -9,7 +9,7 @@ import { Book, BookProgress, FIXED_LAYOUT_FORMATS } from '@/types/book'; import { BookDoc } from '@/libs/document'; import { debounce } from '@/utils/debounce'; import { eventDispatcher } from '@/utils/event'; -import { getCFIFromXPointer, normalizeProgressXPointer, XCFI } from '@/utils/xcfi'; +import { getCFIFromXPointer, XCFI } from '@/utils/xcfi'; type SyncState = 'idle' | 'checking' | 'conflict' | 'synced' | 'error'; @@ -74,7 +74,7 @@ export const useKOSync = (bookKey: string) => { const { doc, index: spineIndex } = content; const converter = new XCFI(doc, spineIndex || 0); const xpointerResult = converter.cfiToXPointer(cfi); - koProgress = normalizeProgressXPointer(xpointerResult.xpointer); + koProgress = xpointerResult.xpointer; } } catch (error) { console.error('Failed to convert CFI to XPointer', error); @@ -90,6 +90,9 @@ export const useKOSync = (bookKey: string) => { const applyRemoteProgress = async (book: Book, bookDoc: BookDoc, remote: KoSyncProgress) => { const view = getView(bookKey); + const bookData = getBookData(bookKey); + if (!view || !bookData) return; + if (FIXED_LAYOUT_FORMATS.has(book.format)) { const pageToGo = parseInt(remote.progress!, 10); if (isNaN(pageToGo)) return; @@ -97,10 +100,10 @@ export const useKOSync = (bookKey: string) => { } else { if (!remote.progress?.startsWith('/body')) return; try { - const apContents = view?.renderer.getContents() ?? []; - const apPrimaryIdx = view?.renderer.primaryIndex; - const content = apContents.find((x) => x.index === apPrimaryIdx) ?? apContents[0]; - const koProgress = normalizeProgressXPointer(remote.progress); + const content = view?.renderer + .getContents() + .find((x) => x.index === view?.renderer.primaryIndex); + const koProgress = remote.progress; const cfi = await getCFIFromXPointer(koProgress, content?.doc, content?.index, bookDoc); view?.goTo(cfi); } catch (error) { diff --git a/apps/readest-app/src/app/reader/hooks/useProgressSync.ts b/apps/readest-app/src/app/reader/hooks/useProgressSync.ts index 668cdcd9..bb41912b 100644 --- a/apps/readest-app/src/app/reader/hooks/useProgressSync.ts +++ b/apps/readest-app/src/app/reader/hooks/useProgressSync.ts @@ -11,7 +11,7 @@ import { CFI } from '@/libs/document'; import { debounce } from '@/utils/debounce'; import { eventDispatcher } from '@/utils/event'; import { DEFAULT_BOOK_SEARCH_CONFIG, SYNC_PROGRESS_INTERVAL_SEC } from '@/services/constants'; -import { getCFIFromXPointer, getXPointerFromCFI, normalizeProgressXPointer } from '@/utils/xcfi'; +import { getCFIFromXPointer, getXPointerFromCFI } from '@/utils/xcfi'; export const useProgressSync = (bookKey: string) => { const _ = useTranslation(); @@ -61,7 +61,7 @@ export const useProgressSync = (bookKey: string) => { if (content && !FIXED_LAYOUT_FORMATS.has(book.format)) { const { doc, index } = content; const xpointerResult = await getXPointerFromCFI(config.location!, doc, index || 0); - config.xpointer = normalizeProgressXPointer(xpointerResult.xpointer); + config.xpointer = xpointerResult.xpointer; } } catch (error) { console.warn('Failed to convert CFI to XPointer', error); @@ -124,16 +124,15 @@ export const useProgressSync = (bookKey: string) => { if (syncedConfig) { const configCFI = config?.location; let remoteCFILocation = syncedConfig.location; - const xPointer = syncedConfig.xpointer; + const xpointer = syncedConfig.xpointer; const bookData = getBookData(bookKey); const view = getView(bookKey); - if (xPointer && view && bookData && bookData.bookDoc) { + if (xpointer && view && bookData && bookData.bookDoc) { const pContents = view.renderer.getContents(); const pIdx = view.renderer.primaryIndex; const content = pContents.find((x) => x.index === pIdx) ?? pContents[0]; - const koProgress = normalizeProgressXPointer(xPointer); const candidateCFI = await getCFIFromXPointer( - koProgress, + xpointer, content?.doc, content?.index, bookData.bookDoc,