Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions desktop/windows/src/main/integrations/syncState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
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 }
Expand All @@ -17,8 +22,8 @@ function read(): SyncFile {
if (existsSync(file())) {
const raw = JSON.parse(readFileSync(file(), 'utf8')) as Partial<SyncFile>
return {
gmail: raw.gmail ?? emptySourceState(),
calendar: raw.calendar ?? emptySourceState()
gmail: normalizeSourceState(raw.gmail),
calendar: normalizeSourceState(raw.calendar)
}
}
} catch {
Expand Down
36 changes: 36 additions & 0 deletions desktop/windows/src/main/integrations/syncStateLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'
import {
emptySourceState,
filterNew,
normalizeSourceState,
recordProcessed,
MAX_PROCESSED
} from './syncStateLogic'
Expand All @@ -17,6 +18,41 @@ 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}`)
})

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', () => {
it('merges new ids, dedups, and advances lastSyncAt', () => {
const next = recordProcessed({ lastSyncAt: 0, processedIds: ['a'] }, ['a', 'b'], 1234)
Expand Down
29 changes: 29 additions & 0 deletions desktop/windows/src/main/integrations/syncStateLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,35 @@ 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<string>()
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)
if (seen.size >= MAX_PROCESSED) break
}
Comment thread
tianmind-studio marked this conversation as resolved.

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<T extends { id: string }>(items: T[], processedIds: string[]): T[] {
const seen = new Set(processedIds)
Expand Down
Loading