From ff84299a431b00a51215a6be3ed66a4a9dee27ca Mon Sep 17 00:00:00 2001 From: KazeKaze93 Date: Sun, 12 Jul 2026 09:01:04 +0400 Subject: [PATCH 1/2] fix: advance sync cursor only after complete pagination Keep partial post commits, but write lastPostId/lastChecked only when pagination ends naturally. Mark unfinished runs with lastSyncIncomplete and rethrow axios failures as ProviderSearchError(network). Co-authored-by: Cursor --- README.md | 2 +- docs/api-guide.md | 1 + docs/architecture.md | 4 +- docs/database.md | 3 + docs/glossary.md | 4 +- docs/roadmap.md | 7 +- docs/user-guide.md | 2 +- .../0030_add_artists_last_sync_incomplete.sql | 1 + drizzle/meta/_journal.json | 7 + src/main/db/queries/artists.ts | 1 + src/main/db/schema.ts | 4 + src/main/services/sync-service.ts | 95 ++++-- .../integration/services/SyncService.test.ts | 289 +++++++++++++++++- 13 files changed, 376 insertions(+), 44 deletions(-) create mode 100644 drizzle/0030_add_artists_last_sync_incomplete.sql diff --git a/README.md b/README.md index 88e58b0..4c38752 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,7 @@ The application is stable and production-ready (see **`package.json`** → `vers - ✅ **Provider Pattern:** Multi-booru support via `IBooruProvider` interface (Rule34, Gelbooru) - ✅ **Rate Limiting:** Intelligent rate limiting with 1.5s delay between artists, 0.5s between pages - ✅ **Anti-Bot Measures:** Shared throttling/UA strategy applied across current providers. -- ⚠️ **Open P0:** sync `lastPostId` cursor integrity — see [Roadmap](./docs/roadmap.md#open-p0-audit--remaining). Video-cache writes are atomic (tmp+rename) with size-capped eviction. +- ✅ **Sync integrity:** `lastPostId` advances only after complete pagination; unfinished runs set `lastSyncIncomplete`. Video-cache writes are atomic (tmp+rename) with size-capped eviction — see [Roadmap](./docs/roadmap.md). ### UI/UX diff --git a/docs/api-guide.md b/docs/api-guide.md index 9143da0..227f0b4 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -471,6 +471,7 @@ type Artist = { apiEndpoint: string; lastPostId: number; newPostsCount: number; + lastSyncIncomplete: boolean; lastChecked: number | null; createdAt: number; }; diff --git a/docs/architecture.md b/docs/architecture.md index 2eb66e1..5055b4c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1170,9 +1170,7 @@ sequenceDiagram }); ``` - g. **Updates artist** - Updates artist's `lastPostId` and `newPostsCount`. - - ⚠️ **Known integrity gap (open P0):** `lastPostId` may advance mid-pagination or after a partial error commit. An interrupted sync can leave a cursor ahead of fully persisted posts and skip gaps on the next run. Do not document “cursor = fully synced watermark” until the sync-cursor integrity fix lands. + g. **Updates artist** - Mid-batch and error paths update `newPostsCount` (and may set `lastSyncIncomplete`) without moving `lastPostId`. After natural pagination end (`postsData.length < PAGE_SIZE`), a single commit writes `lastPostId`, `lastChecked`, and clears `lastSyncIncomplete`. h. **Progress event** - Emits IPC event: `emit('sync:progress', 'Syncing artist_name...')` diff --git a/docs/database.md b/docs/database.md index 34d37ce..5b8bd05 100644 --- a/docs/database.md +++ b/docs/database.md @@ -80,6 +80,9 @@ export const artists = sqliteTable( apiEndpoint: text("api_endpoint").notNull(), lastPostId: integer("last_post_id").default(0).notNull(), newPostsCount: integer("new_posts_count").default(0).notNull(), + lastSyncIncomplete: integer("last_sync_incomplete", { mode: "boolean" }) + .notNull() + .default(false), lastChecked: integer("last_checked", { mode: "timestamp" }), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() diff --git a/docs/glossary.md b/docs/glossary.md index c8836f3..603a5e1 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -154,9 +154,9 @@ Main-process `VideoProxyServer` serves local `http://127.0.0.1` URLs for `