fix(sync): flush pending Readest cloud push when the reader closes (#4535)
Closing a book within the SYNC_PROGRESS_INTERVAL_SEC (3s) auto-sync debounce window saved progress locally but dropped the pending Readest cloud push on teardown. The `sync-book-progress` close handler only reset the pull gate and re-pulled — it never pushed — so other devices stayed on the previous cloud-synced position until the book was reopened (issue #4532). Flush the debounced push at the start of `handleSyncBookProgress`, before the pull gate is reset, so the latest local position reaches the cloud before the view tears down. `syncConfig` reads `configPulled` synchronously, so flushing while the gate is still open takes the push branch. Mirrors the existing KOSync close-time `pushProgress.flush()`. The manual Sync button shares the event and now becomes a true two-way sync (push local, then pull remote). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -276,4 +276,37 @@ describe('useProgressSync', () => {
|
||||
await advance(1500);
|
||||
expect(pullCallCount()).toBe(callsBeforeRefresh + 2);
|
||||
});
|
||||
|
||||
test('sync-book-progress flushes the pending cloud push on book close', async () => {
|
||||
// Reproduces issue #4532: the reader is closed inside the 3s auto-sync
|
||||
// debounce window, so the pending Readest cloud push would otherwise be
|
||||
// dropped on unmount and never reach the cloud.
|
||||
// Mount: the empty pull settles and opens the gate (configPulled = true).
|
||||
const { rerender } = renderHook(() => useProgressSync('h1-view1'));
|
||||
await advance(0);
|
||||
expect(pushCallCount()).toBe(0);
|
||||
|
||||
// User paginates to a new position — this arms the 3s auto-sync debounce.
|
||||
h.state.progress = { location: 'cfi-loc-next' };
|
||||
await act(async () => {
|
||||
rerender();
|
||||
await flushMicrotasks();
|
||||
});
|
||||
// The debounce has not fired yet, so nothing has been pushed.
|
||||
expect(pushCallCount()).toBe(0);
|
||||
|
||||
// Closing the reader dispatches sync-book-progress within the debounce
|
||||
// window — before the 3s timer would have fired.
|
||||
await act(async () => {
|
||||
const listeners = h.eventListeners.get('sync-book-progress');
|
||||
listeners?.forEach((fn) =>
|
||||
fn(new CustomEvent('sync-book-progress', { detail: { bookKey: 'h1-view1' } })),
|
||||
);
|
||||
await flushMicrotasks();
|
||||
});
|
||||
|
||||
// The pending push is flushed immediately — Device A's last local position
|
||||
// reaches the cloud before the reader tears down.
|
||||
expect(pushCallCount()).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,9 +130,24 @@ export const useProgressSync = (bookKey: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleAutoSync = useCallback(
|
||||
debounce(() => {
|
||||
syncConfig();
|
||||
}, SYNC_PROGRESS_INTERVAL_SEC * 1000),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSyncBookProgress = async (event: CustomEvent) => {
|
||||
const { bookKey: syncBookKey } = event.detail;
|
||||
if (syncBookKey === bookKey) {
|
||||
// Flush any pending debounced push first so the latest local progress
|
||||
// reaches the cloud before we (re)pull. This covers the book-close case
|
||||
// (issue #4532): the reader can tear down inside the SYNC_PROGRESS_INTERVAL_SEC
|
||||
// auto-sync window, which would otherwise drop the pending push and leave
|
||||
// other devices on the previous cloud-synced position. Must run while the
|
||||
// gate below is still open so syncConfig takes the push branch.
|
||||
handleAutoSync.flush();
|
||||
// Manual pull-to-refresh: tear down any prior retry chain so the new
|
||||
// attempt starts fresh, rather than being short-circuited by the
|
||||
// "retry already pending" guard in pullWithRetry.
|
||||
@@ -143,7 +158,8 @@ export const useProgressSync = (bookKey: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Push: ad-hoc push when the book is closed
|
||||
// Push: flush the pending push + pull when the book is closed or the user
|
||||
// taps the manual Sync button.
|
||||
useEffect(() => {
|
||||
eventDispatcher.on('sync-book-progress', handleSyncBookProgress);
|
||||
return () => {
|
||||
@@ -152,14 +168,6 @@ export const useProgressSync = (bookKey: string) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bookKey]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleAutoSync = useCallback(
|
||||
debounce(() => {
|
||||
syncConfig();
|
||||
}, SYNC_PROGRESS_INTERVAL_SEC * 1000),
|
||||
[],
|
||||
);
|
||||
|
||||
// Push: auto-push progress when progress changes with a debounce
|
||||
useEffect(() => {
|
||||
if (!progress?.location || !user) return;
|
||||
|
||||
Reference in New Issue
Block a user