Avoid duplication from initial sync

This commit is contained in:
Andras Schmelczer 2025-03-22 14:06:17 +00:00
commit 6906bc4f5e
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
2 changed files with 97 additions and 18 deletions

View file

@ -1,4 +1,5 @@
import type { Logger } from "../tracing/logger"; import type { Logger } from "../tracing/logger";
import { EMPTY_HASH } from "../utils/hash";
export type VaultUpdateId = number; export type VaultUpdateId = number;
export type DocumentId = string; export type DocumentId = string;
@ -19,6 +20,7 @@ export interface StoredDocumentMetadata {
export interface StoredDatabase { export interface StoredDatabase {
documents: StoredDocumentMetadata[]; documents: StoredDocumentMetadata[];
lastSeenUpdateId: VaultUpdateId | undefined; lastSeenUpdateId: VaultUpdateId | undefined;
hasInitialSyncCompleted: boolean;
} }
/** /**
@ -39,6 +41,7 @@ export interface DocumentRecord {
export class Database { export class Database {
private documents: DocumentRecord[]; private documents: DocumentRecord[];
private lastSeenUpdateId: VaultUpdateId | undefined; private lastSeenUpdateId: VaultUpdateId | undefined;
private hasInitialSyncCompleted: boolean;
public constructor( public constructor(
private readonly logger: Logger, private readonly logger: Logger,
@ -66,6 +69,12 @@ export class Database {
this.logger.debug( this.logger.debug(
`Loaded last seen update id: ${this.lastSeenUpdateId}` `Loaded last seen update id: ${this.lastSeenUpdateId}`
); );
this.hasInitialSyncCompleted =
initialState.hasInitialSyncCompleted ?? false;
this.logger.debug(
`Loaded hasInitialSyncCompleted: ${this.hasInitialSyncCompleted}`
);
} }
public get length(): number { public get length(): number {
@ -105,21 +114,6 @@ export class Database {
}); });
} }
public getLastSeenUpdateId(): VaultUpdateId | undefined {
return this.lastSeenUpdateId;
}
public setLastSeenUpdateId(value: VaultUpdateId | undefined): void {
this.lastSeenUpdateId = value;
this.save();
}
public reset(): void {
this.documents = [];
this.lastSeenUpdateId = 0;
this.save();
}
public updateDocumentMetadata( public updateDocumentMetadata(
metadata: { metadata: {
parentVersionId: VaultUpdateId; parentVersionId: VaultUpdateId;
@ -215,6 +209,29 @@ export class Database {
return entry; return entry;
} }
public createNewEmptyDocument(
documentId: DocumentId,
parentVersionId: VaultUpdateId,
relativePath: RelativePath
): DocumentRecord {
const entry = {
relativePath,
documentId,
metadata: {
parentVersionId,
hash: EMPTY_HASH
},
isDeleted: false,
updates: [],
parallelVersion: 0
};
this.documents.push(entry);
this.save();
return entry;
}
public getDocumentByDocumentId( public getDocumentByDocumentId(
find: DocumentId find: DocumentId
): DocumentRecord | undefined { ): DocumentRecord | undefined {
@ -260,6 +277,31 @@ export class Database {
candidate.isDeleted = true; candidate.isDeleted = true;
} }
public getHasInitialSyncCompleted(): boolean {
return this.hasInitialSyncCompleted;
}
public setHasInitialSyncCompleted(value: boolean): void {
this.hasInitialSyncCompleted = value;
this.save();
}
public getLastSeenUpdateId(): VaultUpdateId | undefined {
return this.lastSeenUpdateId;
}
public setLastSeenUpdateId(value: VaultUpdateId | undefined): void {
this.lastSeenUpdateId = value;
this.save();
}
public reset(): void {
this.documents = [];
this.lastSeenUpdateId = 0;
this.hasInitialSyncCompleted = false;
this.save();
}
private save(): void { private save(): void {
this.ensureConsistency(); this.ensureConsistency();
void this.saveData({ void this.saveData({
@ -268,10 +310,11 @@ export class Database {
documentId, documentId,
relativePath, relativePath,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
...metadata! // resolvedDocuments only returns docs with metadata set ...metadata! // `resolvedDocuments` only returns docs with metadata set
}) })
), ),
lastSeenUpdateId: this.lastSeenUpdateId lastSeenUpdateId: this.lastSeenUpdateId,
hasInitialSyncCompleted: this.hasInitialSyncCompleted
}); });
} }

View file

@ -36,7 +36,7 @@ export class Syncer {
concurrency: settings.getSettings().syncConcurrency concurrency: settings.getSettings().syncConcurrency
}); });
settings.addOnSettingsChangeHandlers((newSettings, oldSettings) => { settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
if (newSettings.syncConcurrency === oldSettings.syncConcurrency) { if (newSettings.syncConcurrency === oldSettings.syncConcurrency) {
return; return;
} }
@ -310,6 +310,8 @@ export class Syncer {
} }
private async internalScheduleSyncForOfflineChanges(): Promise<void> { private async internalScheduleSyncForOfflineChanges(): Promise<void> {
await this.createFakeDocumentsFromRemoteState();
const allLocalFiles = await this.operations.listAllFiles(); const allLocalFiles = await this.operations.listAllFiles();
let locallyPossiblyDeletedFiles = [ let locallyPossiblyDeletedFiles = [
@ -387,4 +389,38 @@ export class Syncer {
await Promise.all([updates, deletes]); await Promise.all([updates, deletes]);
} }
/**
* Create fake documents in the database for all files that are present locally
* and also exist remotely. This will stop the subequent syncs from duplicating
* the documents by creating the same documents from multiple clients.
*/
private async createFakeDocumentsFromRemoteState(): Promise<void> {
if (this.database.getHasInitialSyncCompleted()) {
return;
}
const [allLocalFiles, remote] = await Promise.all([
this.operations.listAllFiles(),
this.syncQueue.add(async () => this.syncService.getAll())
]);
if (remote !== undefined) {
remote.latestDocuments
.filter(
(remoteDocument) =>
allLocalFiles.includes(remoteDocument.relativePath) &&
!remoteDocument.isDeleted
)
.forEach((remoteDocument) => {
this.database.createNewEmptyDocument(
remoteDocument.documentId,
remoteDocument.vaultUpdateId,
remoteDocument.relativePath
);
});
}
this.database.setHasInitialSyncCompleted(true);
}
} }