From e6766fff42e60e11641c487ea259e720f49569f6 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sat, 15 Mar 2025 12:11:25 +0000 Subject: [PATCH] working!!!! (hopefully) --- .../src/file-operations/file-operations.ts | 67 +++++-------------- .../sync-operations/unrestricted-syncer.ts | 67 ++++++++++++------- frontend/test-client/src/agent/mock-client.ts | 15 +++-- 3 files changed, 70 insertions(+), 79 deletions(-) diff --git a/frontend/sync-client/src/file-operations/file-operations.ts b/frontend/sync-client/src/file-operations/file-operations.ts index 8071a0f5..f74576d5 100644 --- a/frontend/sync-client/src/file-operations/file-operations.ts +++ b/frontend/sync-client/src/file-operations/file-operations.ts @@ -1,10 +1,6 @@ import type { Logger } from "src/tracing/logger"; import type { FileSystemOperations } from "./filesystem-operations"; -import type { - Database, - DocumentId, - RelativePath -} from "src/persistence/database"; +import type { Database, RelativePath } from "src/persistence/database"; import { isBinary, isFileTypeMergable, mergeText } from "sync_lib"; import { FileNotFoundError, @@ -53,12 +49,12 @@ export class FileOperations { return this.fs.exists(path); } - // Create and write the file if it doesn't exist.Otherwise, it has the same behavior as write. + // Create and write the file if it doesn't exist. Otherwise, it has the same behavior as write. // All parent directories are created if they don't exist. public async create( path: RelativePath, newContent: Uint8Array, - documentId?: DocumentId + whatevs?: any ): Promise { this.logger.debug(`Creating file: ${path}`); if (await this.fs.exists(path)) { @@ -67,25 +63,14 @@ export class FileOperations { `Didn't expect ${path} to exist, deconflicting by moving it to '${deconflictedPath}'` ); - const document = - this.database.getLatestDocumentByRelativePath(path); - this.logger.debug( - `Existing metadata for ${path}: ${JSON.stringify(document?.metadata)}` - ); - - if (document !== undefined && document.documentId === documentId) { - // This can happen if the document got moved both locally and remotely - // to the same file path. In this case, we shouldn't deconflict, however, - // we also can't overwrite otherwise we'd lose changes. - throw new FileNotFoundError(path); - } - - this.database.move(path, deconflictedPath); + // this.database.move(path, deconflictedPath); await this.fs.rename(path, deconflictedPath); } else { await this.createParentDirectories(path); } + whatevs?.(); + await this.fs.write(path, newContent); } @@ -152,8 +137,7 @@ export class FileOperations { public async move( oldPath: RelativePath, - newPath: RelativePath, - documentId?: DocumentId + newPath: RelativePath ): Promise { if (oldPath === newPath) { return; @@ -165,26 +149,14 @@ export class FileOperations { `Conflict when moving '${oldPath}' to '${newPath}', the latter already exists, deconflicting by moving it to '${deconflictedPath}'` ); - const document = - this.database.getLatestDocumentByRelativePath(newPath); - - if ( - document?.metadata !== undefined && - document.documentId === documentId - ) { - // This can happen if the document got moved both locally and remotely - // to the same file path. In this case, we shouldn't deconflict, however, - // we also can't overwrite otherwise we'd lose changes. - throw new FileNotFoundError(newPath); - } - - this.database.move(newPath, deconflictedPath); + // this.database.move(newPath, deconflictedPath); + // this.database.move(oldPath, newPath); await this.fs.rename(newPath, deconflictedPath); } else { + // this.database.move(oldPath, newPath); await this.createParentDirectories(newPath); } - this.database.move(oldPath, newPath); await this.fs.rename(oldPath, newPath); } @@ -226,17 +198,12 @@ export class FileOperations { ); stem = stem.replace(FileOperations.PARENTHESES_REGEX, ""); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - while (true) { - const newName = - currentCount === 0 - ? `${directory}${stem}${extension}` - : `${directory}${stem} (${currentCount})${extension}`; - if (await this.fs.exists(newName)) { - currentCount++; - } else { - return newName; - } - } + let newName; + do { + currentCount++; + newName = `${directory}${stem} (${currentCount})${extension}`; + } while (await this.fs.exists(newName)); + + return newName; } } diff --git a/frontend/sync-client/src/sync-operations/unrestricted-syncer.ts b/frontend/sync-client/src/sync-operations/unrestricted-syncer.ts index 84b4f070..80949615 100644 --- a/frontend/sync-client/src/sync-operations/unrestricted-syncer.ts +++ b/frontend/sync-client/src/sync-operations/unrestricted-syncer.ts @@ -36,42 +36,44 @@ export class UnrestrictedSyncer { proposedDocumentId: DocumentId, getLatestDocument: () => DocumentRecord ): Promise { - let latestDocument = getLatestDocument(); + let document = getLatestDocument(); return this.executeSync( - [latestDocument.relativePath], + [document.relativePath], SyncType.CREATE, SyncSource.PUSH, async () => { + document = getLatestDocument(); + const contentBytes = await this.operations.read( - latestDocument.relativePath + document.relativePath ); // this can throw FileNotFoundError const contentHash = hash(contentBytes); const response = await this.syncService.create({ documentId: proposedDocumentId, - relativePath: latestDocument.relativePath, + relativePath: document.relativePath, contentBytes }); - latestDocument = getLatestDocument(); + document = getLatestDocument(); this.history.addHistoryEntry({ status: SyncStatus.SUCCESS, source: SyncSource.PUSH, - relativePath: latestDocument.relativePath, + relativePath: document.relativePath, message: `Successfully uploaded locally created file`, type: SyncType.CREATE }); this.database.setDocument( { - relativePath: latestDocument.relativePath, + relativePath: document.relativePath, documentId: response.documentId, parentVersionId: response.vaultUpdateId, hash: contentHash }, - latestDocument.identity + document.identity ); this.tryIncrementVaultUpdateId(response.vaultUpdateId); @@ -88,6 +90,8 @@ export class UnrestrictedSyncer { SyncType.DELETE, SyncSource.PUSH, async () => { + document = getLatestDocument(); + const response = await this.syncService.delete({ documentId: document.documentId, relativePath: document.relativePath @@ -132,6 +136,9 @@ export class UnrestrictedSyncer { SyncType.UPDATE, SyncSource.PUSH, async () => { + document = getLatestDocument(); + const originalRelativePath = document.relativePath; + if (document.metadata === undefined || document.isDeleted) { this.logger.debug( `Document ${document.relativePath} has been already deleted, no need to update it` @@ -194,8 +201,6 @@ export class UnrestrictedSyncer { }); if (response.isDeleted) { - await this.operations.delete(document.relativePath); - this.history.addHistoryEntry({ status: SyncStatus.SUCCESS, source: SyncSource.PULL, @@ -216,21 +221,25 @@ export class UnrestrictedSyncer { document.identity ); + await this.operations.delete(document.relativePath); + this.tryIncrementVaultUpdateId(response.vaultUpdateId); return; } - if (response.relativePath != document.relativePath) { + let actualPath = document.relativePath; + + if (response.relativePath != originalRelativePath) { // this.database.getNewResolvedDocumentByRelativePath( // response.relativePath, // promise // ); + actualPath = response.relativePath; await this.operations.move( document.relativePath, - response.relativePath, - response.documentId + response.relativePath ); // this can throw FileNotFoundError } @@ -239,7 +248,7 @@ export class UnrestrictedSyncer { contentHash = hash(responseBytes); await this.operations.write( - response.relativePath, + actualPath, contentBytes, responseBytes ); @@ -253,12 +262,10 @@ export class UnrestrictedSyncer { }); } - document = getLatestDocument(); - this.database.setDocument( { documentId: response.documentId, - relativePath: document.relativePath, + relativePath: actualPath, parentVersionId: response.vaultUpdateId, hash: contentHash }, @@ -326,6 +333,7 @@ export class UnrestrictedSyncer { ); return; } + if ( localMetadata?.metadata?.parentVersionId ?? -1 >= remoteVersion.vaultUpdateId @@ -338,6 +346,21 @@ export class UnrestrictedSyncer { const contentBytes = deserialize(content); + const [promise, resolve] = createPromise(); + + await this.operations.create( + remoteVersion.relativePath, + contentBytes, + () => + this.database.getNewResolvedDocumentByRelativePath( + remoteVersion.documentId, + remoteVersion.relativePath, + promise + ) + ); + + const document = + this.database.getDocumentByUpdatePromise(promise); this.database.setDocument( { documentId: remoteVersion.documentId, @@ -345,14 +368,10 @@ export class UnrestrictedSyncer { parentVersionId: remoteVersion.vaultUpdateId, hash: hash(contentBytes) }, - localMetadata?.identity - ); - - await this.operations.create( - remoteVersion.relativePath, - contentBytes, - remoteVersion.documentId + document.identity ); + resolve(); + this.database.removeDocumentPromise(promise); this.history.addHistoryEntry({ status: SyncStatus.SUCCESS, diff --git a/frontend/test-client/src/agent/mock-client.ts b/frontend/test-client/src/agent/mock-client.ts index 814d3fc1..6cbcca06 100644 --- a/frontend/test-client/src/agent/mock-client.ts +++ b/frontend/test-client/src/agent/mock-client.ts @@ -82,11 +82,11 @@ export class MockClient implements FileSystemOperations { const newContentUint8Array = new TextEncoder().encode(newContent); this.localFiles.set(path, newContentUint8Array); - const existingPats = currentContent + const existingParts = currentContent .split(" ") .map((part) => part.trim()); const newParts = newContent.split(" ").map((part) => part.trim()); - existingPats.forEach((part) => + existingParts.forEach((part) => // all changes should be additive assert( newParts.includes(part), @@ -106,15 +106,20 @@ export class MockClient implements FileSystemOperations { } public async write(path: RelativePath, content: Uint8Array): Promise { + const hasExisted = this.localFiles.has(path); this.localFiles.set(path, content); this.client.logger.info( `Updated file ${path} with:\n new content: ${new TextDecoder().decode(content)}` ); - void this.client.syncer.syncLocallyUpdatedFile({ - relativePath: path - }); + if (hasExisted) { + void this.client.syncer.syncLocallyUpdatedFile({ + relativePath: path + }); + } else { + void this.client.syncer.syncLocallyCreatedFile(path); + } } public async delete(path: RelativePath): Promise {