From 2aff3ac06ea56538e352f13534f3094ade160ee7 Mon Sep 17 00:00:00 2001 From: 491034170 <142008960+491034170@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:05:30 +0800 Subject: [PATCH 1/5] fix(windows): normalize Google sync state --- .../src/main/integrations/syncState.ts | 6 ++--- .../main/integrations/syncStateLogic.test.ts | 26 +++++++++++++++++++ .../src/main/integrations/syncStateLogic.ts | 26 +++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/desktop/windows/src/main/integrations/syncState.ts b/desktop/windows/src/main/integrations/syncState.ts index 714d7438c9..2977e5e184 100644 --- a/desktop/windows/src/main/integrations/syncState.ts +++ b/desktop/windows/src/main/integrations/syncState.ts @@ -3,7 +3,7 @@ import { app } from 'electron' import { existsSync, readFileSync, writeFileSync, rmSync } from 'fs' import { join } from 'path' -import { emptySourceState, recordProcessed, type SourceState } from './syncStateLogic' +import { emptySourceState, normalizeSourceState, recordProcessed, type SourceState } from './syncStateLogic' import type { GoogleSource } from '../../shared/types' type SyncFile = { gmail: SourceState; calendar: SourceState } @@ -17,8 +17,8 @@ function read(): SyncFile { if (existsSync(file())) { const raw = JSON.parse(readFileSync(file(), 'utf8')) as Partial return { - gmail: raw.gmail ?? emptySourceState(), - calendar: raw.calendar ?? emptySourceState() + gmail: normalizeSourceState(raw.gmail), + calendar: normalizeSourceState(raw.calendar) } } } catch { diff --git a/desktop/windows/src/main/integrations/syncStateLogic.test.ts b/desktop/windows/src/main/integrations/syncStateLogic.test.ts index 464c4ec7c3..65f4b23cd0 100644 --- a/desktop/windows/src/main/integrations/syncStateLogic.test.ts +++ b/desktop/windows/src/main/integrations/syncStateLogic.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest' import { emptySourceState, filterNew, + normalizeSourceState, recordProcessed, MAX_PROCESSED } from './syncStateLogic' @@ -17,6 +18,31 @@ describe('filterNew', () => { }) }) +describe('normalizeSourceState', () => { + it('returns an empty state for missing or malformed persisted data', () => { + expect(normalizeSourceState(null)).toEqual(emptySourceState()) + expect(normalizeSourceState('bad')).toEqual(emptySourceState()) + expect(normalizeSourceState({ lastSyncAt: 'soon', processedIds: 'a,b' })).toEqual(emptySourceState()) + }) + + it('keeps valid fields and drops invalid processed ids', () => { + expect( + normalizeSourceState({ + lastSyncAt: 1234, + processedIds: ['a', '', 42, 'b', 'a', 'c'] + }) + ).toEqual({ lastSyncAt: 1234, processedIds: ['b', 'a', 'c'] }) + }) + + it('bounds normalized ids to the newest MAX_PROCESSED entries', () => { + const ids = Array.from({ length: MAX_PROCESSED + 2 }, (_, i) => `id${i}`) + const next = normalizeSourceState({ lastSyncAt: 1, processedIds: ids }) + expect(next.processedIds.length).toBe(MAX_PROCESSED) + expect(next.processedIds[0]).toBe('id2') + expect(next.processedIds.at(-1)).toBe(`id${MAX_PROCESSED + 1}`) + }) +}) + describe('recordProcessed', () => { it('merges new ids, dedups, and advances lastSyncAt', () => { const next = recordProcessed({ lastSyncAt: 0, processedIds: ['a'] }, ['a', 'b'], 1234) diff --git a/desktop/windows/src/main/integrations/syncStateLogic.ts b/desktop/windows/src/main/integrations/syncStateLogic.ts index 993f1c4f47..a01092f3fc 100644 --- a/desktop/windows/src/main/integrations/syncStateLogic.ts +++ b/desktop/windows/src/main/integrations/syncStateLogic.ts @@ -12,6 +12,32 @@ export function emptySourceState(): SourceState { return { lastSyncAt: 0, processedIds: [] } } +export function normalizeSourceState(value: unknown): SourceState { + if (!value || typeof value !== 'object') return emptySourceState() + const raw = value as { lastSyncAt?: unknown; processedIds?: unknown } + const lastSyncAt = + typeof raw.lastSyncAt === 'number' && Number.isFinite(raw.lastSyncAt) && raw.lastSyncAt > 0 + ? raw.lastSyncAt + : 0 + + if (!Array.isArray(raw.processedIds)) { + return { lastSyncAt, processedIds: [] } + } + + const seen = new Set() + const processedIds: string[] = [] + for (let i = raw.processedIds.length - 1; i >= 0; i--) { + const id = raw.processedIds[i] + if (typeof id !== 'string' || !id || seen.has(id)) continue + processedIds.unshift(id) + seen.add(id) + } + + const bounded = + processedIds.length > MAX_PROCESSED ? processedIds.slice(processedIds.length - MAX_PROCESSED) : processedIds + return { lastSyncAt, processedIds: bounded } +} + /** Items whose id is NOT already in the processed set. */ export function filterNew(items: T[], processedIds: string[]): T[] { const seen = new Set(processedIds) From 9d538ecd2a664ab948ea074579483dfe5e1fbefc Mon Sep 17 00:00:00 2001 From: 491034170 <142008960+491034170@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:24:57 +0800 Subject: [PATCH 2/5] fix(windows): bound sync state normalization scan --- .../windows/src/main/integrations/syncStateLogic.test.ts | 8 ++++++++ desktop/windows/src/main/integrations/syncStateLogic.ts | 1 + 2 files changed, 9 insertions(+) diff --git a/desktop/windows/src/main/integrations/syncStateLogic.test.ts b/desktop/windows/src/main/integrations/syncStateLogic.test.ts index 65f4b23cd0..8a1c352239 100644 --- a/desktop/windows/src/main/integrations/syncStateLogic.test.ts +++ b/desktop/windows/src/main/integrations/syncStateLogic.test.ts @@ -41,6 +41,14 @@ describe('normalizeSourceState', () => { expect(next.processedIds[0]).toBe('id2') expect(next.processedIds.at(-1)).toBe(`id${MAX_PROCESSED + 1}`) }) + + it('bounds normalized ids after deduping newest entries', () => { + const ids = [...Array.from({ length: MAX_PROCESSED }, (_, i) => `id${i}`), 'tail', 'tail'] + const next = normalizeSourceState({ lastSyncAt: 1, processedIds: ids }) + expect(next.processedIds.length).toBe(MAX_PROCESSED) + expect(next.processedIds[0]).toBe('id1') + expect(next.processedIds.at(-1)).toBe('tail') + }) }) describe('recordProcessed', () => { diff --git a/desktop/windows/src/main/integrations/syncStateLogic.ts b/desktop/windows/src/main/integrations/syncStateLogic.ts index a01092f3fc..171e6f243e 100644 --- a/desktop/windows/src/main/integrations/syncStateLogic.ts +++ b/desktop/windows/src/main/integrations/syncStateLogic.ts @@ -31,6 +31,7 @@ export function normalizeSourceState(value: unknown): SourceState { if (typeof id !== 'string' || !id || seen.has(id)) continue processedIds.unshift(id) seen.add(id) + if (seen.size >= MAX_PROCESSED) break } const bounded = From 1b7338c717a1eb4ea55251ee2a1f9ac176d7cdd4 Mon Sep 17 00:00:00 2001 From: 491034170 <142008960+491034170@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:46:29 +0800 Subject: [PATCH 3/5] Format sync state imports --- desktop/windows/src/main/integrations/syncState.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/desktop/windows/src/main/integrations/syncState.ts b/desktop/windows/src/main/integrations/syncState.ts index 2977e5e184..3df3f1e52a 100644 --- a/desktop/windows/src/main/integrations/syncState.ts +++ b/desktop/windows/src/main/integrations/syncState.ts @@ -3,7 +3,12 @@ import { app } from 'electron' import { existsSync, readFileSync, writeFileSync, rmSync } from 'fs' import { join } from 'path' -import { emptySourceState, normalizeSourceState, recordProcessed, type SourceState } from './syncStateLogic' +import { + emptySourceState, + normalizeSourceState, + recordProcessed, + type SourceState +} from './syncStateLogic' import type { GoogleSource } from '../../shared/types' type SyncFile = { gmail: SourceState; calendar: SourceState } From 5d17b36c3aa743c0fb1540f80d0e3924cceed4f5 Mon Sep 17 00:00:00 2001 From: 491034170 <142008960+491034170@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:46:30 +0800 Subject: [PATCH 4/5] Format sync state normalization --- desktop/windows/src/main/integrations/syncStateLogic.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/desktop/windows/src/main/integrations/syncStateLogic.ts b/desktop/windows/src/main/integrations/syncStateLogic.ts index 171e6f243e..f0b1b73b88 100644 --- a/desktop/windows/src/main/integrations/syncStateLogic.ts +++ b/desktop/windows/src/main/integrations/syncStateLogic.ts @@ -35,7 +35,9 @@ export function normalizeSourceState(value: unknown): SourceState { } const bounded = - processedIds.length > MAX_PROCESSED ? processedIds.slice(processedIds.length - MAX_PROCESSED) : processedIds + processedIds.length > MAX_PROCESSED + ? processedIds.slice(processedIds.length - MAX_PROCESSED) + : processedIds return { lastSyncAt, processedIds: bounded } } From ad12f482c15a06d15f6c3ee525651decb2b9094d Mon Sep 17 00:00:00 2001 From: 491034170 <142008960+491034170@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:46:30 +0800 Subject: [PATCH 5/5] Format sync state tests --- desktop/windows/src/main/integrations/syncStateLogic.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/desktop/windows/src/main/integrations/syncStateLogic.test.ts b/desktop/windows/src/main/integrations/syncStateLogic.test.ts index 8a1c352239..2c19a48cc2 100644 --- a/desktop/windows/src/main/integrations/syncStateLogic.test.ts +++ b/desktop/windows/src/main/integrations/syncStateLogic.test.ts @@ -22,7 +22,9 @@ describe('normalizeSourceState', () => { it('returns an empty state for missing or malformed persisted data', () => { expect(normalizeSourceState(null)).toEqual(emptySourceState()) expect(normalizeSourceState('bad')).toEqual(emptySourceState()) - expect(normalizeSourceState({ lastSyncAt: 'soon', processedIds: 'a,b' })).toEqual(emptySourceState()) + expect(normalizeSourceState({ lastSyncAt: 'soon', processedIds: 'a,b' })).toEqual( + emptySourceState() + ) }) it('keeps valid fields and drops invalid processed ids', () => {