diff --git a/src/lib/analytics.test.ts b/src/lib/analytics.test.ts index f5a32e3..0faa74b 100644 --- a/src/lib/analytics.test.ts +++ b/src/lib/analytics.test.ts @@ -296,6 +296,63 @@ describe("OfflineAnalytics", () => { error ); }); + + it("should cancel pending debounced sync when manual sync is triggered", async () => { + // Simulate a debounced sync being scheduled + const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout"); + + // Track an event to trigger debounced sync + await analytics!.trackPageView("/test", "Test"); + + // Verify debounced sync was scheduled + expect(analytics!["syncTimeout"]).toBeDefined(); + const scheduledTimeoutId = analytics!["syncTimeout"]; + + // Manually trigger sync (like when coming online) + await analytics!.syncEvents(); + + // Verify pending timeout was cleared + expect(clearTimeoutSpy).toHaveBeenCalledWith(scheduledTimeoutId); + expect(analytics!["syncTimeout"]).toBeUndefined(); + + clearTimeoutSpy.mockRestore(); + }); + + it("should not create duplicate syncs from debounce and manual trigger", async () => { + const mockEvents = [ + { + id: 1, + type: "page_view", + category: "test", + action: "test", + synced: false, + timestamp: Date.now(), + sessionId: "test", + }, + ]; + + vi.mocked(db.analytics!.where).mockReturnValue({ + equals: vi.fn().mockReturnValue({ + toArray: vi.fn().mockResolvedValue(mockEvents), + and: vi.fn(), + }), + } as never); + + // Track an event (schedules debounced sync) + await analytics!.trackPageView("/test", "Test"); + + // Manually trigger sync immediately (before debounce fires) + await analytics!.syncEvents(); + + // Verify sync was called (via bulkUpdate) + expect(db.analytics!.bulkUpdate).toHaveBeenCalledTimes(1); + + // Wait for debounce timeout to potentially fire + await new Promise((resolve) => setTimeout(resolve, 1100)); + + // Verify sync was NOT called again (debounce was cancelled) + expect(db.analytics!.bulkUpdate).toHaveBeenCalledTimes(1); + }); }); describe("getStats", () => { diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index 7647d84..9b80f81 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -289,6 +289,13 @@ class OfflineAnalytics { async syncEvents(): Promise { if (!this.isOnline || this.isDestroyed) return; + // Clear any pending debounced sync to prevent duplicate sync attempts + // This ensures manual sync (e.g., from handleOnline) cancels debounced sync + if (this.syncTimeout) { + clearTimeout(this.syncTimeout); + this.syncTimeout = undefined; + } + // Prevent concurrent syncs - atomic check and set if (this.isSyncing) { console.log("Sync already in progress, skipping...");