Keep updating the last seen id correctly

This commit is contained in:
Andras Schmelczer 2025-04-08 23:02:41 +01:00
commit 96bf542b91
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
3 changed files with 66 additions and 41 deletions

View file

@ -1,5 +1,6 @@
import type { Logger } from "../tracing/logger"; import type { Logger } from "../tracing/logger";
import { EMPTY_HASH } from "../utils/hash"; import { EMPTY_HASH } from "../utils/hash";
import { CoveredValues } from "../utils/min-covered";
export type VaultUpdateId = number; export type VaultUpdateId = number;
export type DocumentId = string; export type DocumentId = string;
@ -40,7 +41,7 @@ export interface DocumentRecord {
export class Database { export class Database {
private documents: DocumentRecord[]; private documents: DocumentRecord[];
private lastSeenUpdateId: VaultUpdateId | undefined; private lastSeenUpdateIds: CoveredValues;
private hasInitialSyncCompleted: boolean; private hasInitialSyncCompleted: boolean;
public constructor( public constructor(
@ -65,9 +66,10 @@ export class Database {
this.ensureConsistency(); this.ensureConsistency();
this.logger.debug(`Loaded ${this.documents.length} documents`); this.logger.debug(`Loaded ${this.documents.length} documents`);
this.lastSeenUpdateId = initialState.lastSeenUpdateId; const { lastSeenUpdateId } = initialState;
this.logger.debug( this.logger.debug(`Loaded last seen update id: ${lastSeenUpdateId}`);
`Loaded last seen update id: ${this.lastSeenUpdateId}` this.lastSeenUpdateIds = new CoveredValues(
Math.max(0, lastSeenUpdateId ?? 0) // the first updateId will be 1 which is the first integer after -1
); );
this.hasInitialSyncCompleted = this.hasInitialSyncCompleted =
@ -286,18 +288,28 @@ export class Database {
this.save(); this.save();
} }
public getLastSeenUpdateId(): VaultUpdateId | undefined { public getLastSeenUpdateId(): VaultUpdateId {
return this.lastSeenUpdateId; return this.lastSeenUpdateIds.min;
} }
public setLastSeenUpdateId(value: VaultUpdateId | undefined): void { public addLastSeenUpdateId(value: number): void {
this.lastSeenUpdateId = value; const previousMin = this.lastSeenUpdateIds.min;
this.lastSeenUpdateIds.add(value);
if (previousMin !== this.lastSeenUpdateIds.min) {
this.save();
}
}
public setLastSeenUpdateId(value: number): void {
this.lastSeenUpdateIds.min = value;
this.save(); this.save();
} }
public reset(): void { public reset(): void {
this.documents = []; this.documents = [];
this.lastSeenUpdateId = 0; this.lastSeenUpdateIds = new CoveredValues(
0 // the first updateId will be 1 which is the first integer after -1
);
this.hasInitialSyncCompleted = false; this.hasInitialSyncCompleted = false;
this.save(); this.save();
} }
@ -313,7 +325,7 @@ export class Database {
...metadata! // `resolvedDocuments` only returns docs with metadata set ...metadata! // `resolvedDocuments` only returns docs with metadata set
}) })
), ),
lastSeenUpdateId: this.lastSeenUpdateId, lastSeenUpdateId: this.lastSeenUpdateIds.min,
hasInitialSyncCompleted: this.hasInitialSyncCompleted hasInitialSyncCompleted: this.hasInitialSyncCompleted
}); });
} }

View file

@ -17,6 +17,11 @@ import { createPromise } from "../utils/create-promise";
import { SyncResetError } from "../services/sync-reset-error"; import { SyncResetError } from "../services/sync-reset-error";
import { Locks } from "../utils/locks"; import { Locks } from "../utils/locks";
interface WebsocketVaultUpdate {
documents: components["schemas"]["DocumentVersionWithoutContent"][];
isInitialSync: boolean;
}
export class Syncer { export class Syncer {
private readonly remoteDocumentsLock: Locks<DocumentId>; private readonly remoteDocumentsLock: Locks<DocumentId>;
private readonly remainingOperationsListeners: (( private readonly remainingOperationsListeners: ((
@ -273,6 +278,7 @@ export class Syncer {
public stop(): void { public stop(): void {
clearInterval(this.refreshApplyRemoteChangesWebSocketInterval); clearInterval(this.refreshApplyRemoteChangesWebSocketInterval);
try { try {
this.applyRemoteChangesWebSocket?.close(); this.applyRemoteChangesWebSocket?.close();
} catch (e) { } catch (e) {
@ -302,14 +308,30 @@ export class Syncer {
wsUri wsUri
); );
this.applyRemoteChangesWebSocket.onmessage = (event): void => this.applyRemoteChangesWebSocket.onmessage = async (
void this.syncRemotelyUpdatedFile(event.data).catch( event
(e: unknown) => { ): Promise<void> => {
this.logger.error( // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
`Failed to sync remotely updated file: ${e}` const message = JSON.parse(event.data) as WebsocketVaultUpdate;
try {
await Promise.all(
message.documents.map(async (document) =>
this.syncRemotelyUpdatedFile(document)
)
);
if (message.isInitialSync && message.documents.length > 0) {
this.database.setLastSeenUpdateId(
message.documents
.map((document) => document.vaultUpdateId)
.reduce((a, b) => Math.max(a, b))
); );
} }
); } catch (e) {
this.logger.error(`Failed to sync remotely updated file: ${e}`);
}
};
// The JS WebSocket API doesn't support setting headers, so we have to send the token as a message // The JS WebSocket API doesn't support setting headers, so we have to send the token as a message
this.applyRemoteChangesWebSocket.onopen = (): void => { this.applyRemoteChangesWebSocket.onopen = (): void => {
@ -317,7 +339,8 @@ export class Syncer {
this.applyRemoteChangesWebSocket?.send( this.applyRemoteChangesWebSocket?.send(
JSON.stringify({ JSON.stringify({
deviceId: this.deviceId, deviceId: this.deviceId,
token: settings.token token: settings.token,
lastSeenVaultUpdateId: this.database.getLastSeenUpdateId()
}) })
); );
this.webSocketStatusChangeListeners.forEach((listener) => { this.webSocketStatusChangeListeners.forEach((listener) => {
@ -327,7 +350,7 @@ export class Syncer {
this.applyRemoteChangesWebSocket.onclose = (event): void => { this.applyRemoteChangesWebSocket.onclose = (event): void => {
this.logger.warn( this.logger.warn(
`WebSocket closed with code ${event.code}: ${event.reason}` `WebSocket closed with code ${event.code} (${event.reason == "" ? "unknown reason" : event.reason})`
); );
this.webSocketStatusChangeListeners.forEach((listener) => { this.webSocketStatusChangeListeners.forEach((listener) => {
listener(); listener();
@ -347,12 +370,9 @@ export class Syncer {
}, 5000); }, 5000);
} }
private async syncRemotelyUpdatedFile(message: string): Promise<void> { private async syncRemotelyUpdatedFile(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion remoteVersion: components["schemas"]["DocumentVersionWithoutContent"]
const remoteVersion = JSON.parse( ): Promise<void> {
message
) as components["schemas"]["DocumentVersionWithoutContent"];
let document = this.database.getDocumentByDocumentId( let document = this.database.getDocumentByDocumentId(
remoteVersion.documentId remoteVersion.documentId
); );
@ -400,6 +420,8 @@ export class Syncer {
this.database.removeDocumentPromise(promise); this.database.removeDocumentPromise(promise);
} }
} }
this.database.addLastSeenUpdateId(remoteVersion.vaultUpdateId);
} finally { } finally {
if (hasLockToRelease) { if (hasLockToRelease) {
this.remoteDocumentsLock.unlock(remoteVersion.documentId); this.remoteDocumentsLock.unlock(remoteVersion.documentId);

View file

@ -59,7 +59,7 @@ export class UnrestrictedSyncer {
document document
); );
this.tryIncrementVaultUpdateId(response.vaultUpdateId); this.database.addLastSeenUpdateId(response.vaultUpdateId);
} }
); );
} }
@ -90,6 +90,8 @@ export class UnrestrictedSyncer {
}, },
document document
); );
this.database.addLastSeenUpdateId(response.vaultUpdateId);
} }
); );
} }
@ -156,6 +158,7 @@ export class UnrestrictedSyncer {
this.logger.info( this.logger.info(
`Document ${document.relativePath} has been deleted before we could finish updating it` `Document ${document.relativePath} has been deleted before we could finish updating it`
); );
this.database.addLastSeenUpdateId(response.vaultUpdateId);
return; return;
} }
@ -174,6 +177,7 @@ export class UnrestrictedSyncer {
this.logger.debug( this.logger.debug(
`Document ${document.relativePath} is already more up to date than the fetched version` `Document ${document.relativePath} is already more up to date than the fetched version`
); );
this.database.addLastSeenUpdateId(response.vaultUpdateId); // in case the previous `vaultUpdateId` update hasn't made it through
return; return;
} }
@ -206,7 +210,7 @@ export class UnrestrictedSyncer {
await this.operations.delete(document.relativePath); await this.operations.delete(document.relativePath);
this.tryIncrementVaultUpdateId(response.vaultUpdateId); this.database.addLastSeenUpdateId(response.vaultUpdateId);
return; return;
} }
@ -221,14 +225,6 @@ export class UnrestrictedSyncer {
); // this can throw FileNotFoundError ); // this can throw FileNotFoundError
} }
this.database.updateDocumentMetadata(
{
parentVersionId: response.vaultUpdateId,
hash: contentHash
},
document
);
if ( if (
!("type" in response) || !("type" in response) ||
response.type === "MergingUpdate" response.type === "MergingUpdate"
@ -268,7 +264,7 @@ export class UnrestrictedSyncer {
); );
} }
this.tryIncrementVaultUpdateId(response.vaultUpdateId); this.database.addLastSeenUpdateId(response.vaultUpdateId);
} }
); );
} }
@ -291,6 +287,7 @@ export class UnrestrictedSyncer {
this.logger.debug( this.logger.debug(
`Document ${remoteVersion.relativePath} is already at least as up to date as the fetched version` `Document ${remoteVersion.relativePath} is already at least as up to date as the fetched version`
); );
return; return;
} }
@ -425,10 +422,4 @@ export class UnrestrictedSyncer {
} }
} }
} }
private tryIncrementVaultUpdateId(responseVaultUpdateId: number): void {
if (this.database.getLastSeenUpdateId() === responseVaultUpdateId - 1) {
this.database.setLastSeenUpdateId(responseVaultUpdateId);
}
}
} }