fix(sync): prevent SYNC_IMPORT for fresh clients syncing to empty server

The server migration check was incorrectly creating a SYNC_IMPORT when
a fresh client (with local data but no sync history) synced to an empty
server. This caused operations from other clients to be filtered out as
"invalidated by SYNC_IMPORT" because they were CONCURRENT with it.

Now _checkAndHandleServerMigration() checks for previously synced ops
before triggering migration, correctly distinguishing between:
- Fresh client (only local ops) → uploads ops normally
- Server migration (has sync history) → creates SYNC_IMPORT

Also adds npm scripts for debugging supersync E2E tests.
This commit is contained in:
Johannes Millan 2025-12-17 15:56:17 +01:00
parent 5cb9219cdd
commit 02d80249af
3 changed files with 36 additions and 3 deletions

View file

@ -60,6 +60,8 @@
"e2e:report": "PLAYWRIGHT_HTML_REPORT=1 npx playwright test --config e2e/playwright.config.ts",
"e2e:webdav": "docker compose up -d webdav && ./scripts/wait-for-webdav.sh && npm run e2e -- --grep webdav; docker compose down",
"e2e:supersync": "docker compose up -d --build supersync && echo 'Waiting for SuperSync server...' && until curl -s http://localhost:1900/health > /dev/null 2>&1; do sleep 1; done && echo 'Server ready!' && npm run e2e -- --grep @supersync; docker compose down supersync",
"e2e:supersync:file": "docker compose up -d --build supersync && echo 'Waiting for SuperSync server...' && until curl -s http://localhost:1900/health > /dev/null 2>&1; do sleep 1; done && echo 'Server ready!' && E2E_VERBOSE=true npx playwright test --config e2e/playwright.config.ts --reporter=list",
"e2e:supersync:down": "docker compose down supersync",
"electron": "NODE_ENV=PROD electron .",
"electron:build": "tsc -p electron/tsconfig.electron.json",
"electron:watch": "tsc -p electron/tsconfig.electron.json --watch",

View file

@ -424,6 +424,22 @@ export class OperationLogStoreService {
return cursor ? (cursor.key as number) : 0;
}
/**
* Checks if there are any operations that have been synced to the server.
* Used to distinguish between:
* - Fresh client (only local ops, never synced) NOT a server migration
* - Client that previously synced (has synced ops) Server migration scenario
*/
async hasSyncedOps(): Promise<boolean> {
await this._ensureInit();
// Use the bySyncedAt index to efficiently check for any synced ops
const cursor = await this.db
.transaction('ops')
.store.index('bySyncedAt')
.openCursor();
return cursor !== null;
}
async saveStateCache(snapshot: {
state: unknown;
lastAppliedOpSeq: number;

View file

@ -382,11 +382,14 @@ export class OperationLogSyncService {
* Check if we're connecting to a new/empty server and need to upload full state.
*
* This handles the server migration scenario:
* - Client has history (not fresh)
* - Client has PREVIOUSLY SYNCED operations (not just local ops)
* - lastServerSeq is 0 for this server (first time connecting)
* - Server is empty (latestSeq = 0)
*
* When detected, creates a SYNC_IMPORT with full state before regular ops are uploaded.
*
* IMPORTANT: A fresh client with only local (unsynced) ops is NOT a migration scenario.
* Fresh clients should just upload their ops normally without creating a SYNC_IMPORT.
*/
private async _checkAndHandleServerMigration(
syncProvider: SyncProviderServiceInterface<SyncProviderId>,
@ -411,11 +414,23 @@ export class OperationLogSyncService {
return;
}
// Server is empty AND we have history (not fresh) AND lastServerSeq is 0
// CRITICAL: Check if this client has PREVIOUSLY synced operations.
// A client that has never synced (only local ops) is NOT a migration case.
// It's just a fresh client that should upload its ops normally.
const hasSyncedOps = await this.opLogStore.hasSyncedOps();
if (!hasSyncedOps) {
OpLog.normal(
'OperationLogSyncService: Empty server detected, but no previously synced ops. ' +
'This is a fresh client, not a server migration. Proceeding with normal upload.',
);
return;
}
// Server is empty AND we have PREVIOUSLY SYNCED ops AND lastServerSeq is 0
// This is a server migration - create SYNC_IMPORT with full state
OpLog.warn(
'OperationLogSyncService: Server migration detected during upload check. ' +
'Empty server detected, creating full state SYNC_IMPORT.',
'Empty server with previously synced ops. Creating full state SYNC_IMPORT.',
);
await this._handleServerMigration();
}