working!!!! (hopefully)

This commit is contained in:
Andras Schmelczer 2025-03-15 12:11:25 +00:00
commit e6766fff42
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
3 changed files with 70 additions and 79 deletions

View file

@ -1,10 +1,6 @@
import type { Logger } from "src/tracing/logger"; import type { Logger } from "src/tracing/logger";
import type { FileSystemOperations } from "./filesystem-operations"; import type { FileSystemOperations } from "./filesystem-operations";
import type { import type { Database, RelativePath } from "src/persistence/database";
Database,
DocumentId,
RelativePath
} from "src/persistence/database";
import { isBinary, isFileTypeMergable, mergeText } from "sync_lib"; import { isBinary, isFileTypeMergable, mergeText } from "sync_lib";
import { import {
FileNotFoundError, FileNotFoundError,
@ -53,12 +49,12 @@ export class FileOperations {
return this.fs.exists(path); 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. // All parent directories are created if they don't exist.
public async create( public async create(
path: RelativePath, path: RelativePath,
newContent: Uint8Array, newContent: Uint8Array,
documentId?: DocumentId whatevs?: any
): Promise<void> { ): Promise<void> {
this.logger.debug(`Creating file: ${path}`); this.logger.debug(`Creating file: ${path}`);
if (await this.fs.exists(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}'` `Didn't expect ${path} to exist, deconflicting by moving it to '${deconflictedPath}'`
); );
const document = // this.database.move(path, deconflictedPath);
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);
await this.fs.rename(path, deconflictedPath); await this.fs.rename(path, deconflictedPath);
} else { } else {
await this.createParentDirectories(path); await this.createParentDirectories(path);
} }
whatevs?.();
await this.fs.write(path, newContent); await this.fs.write(path, newContent);
} }
@ -152,8 +137,7 @@ export class FileOperations {
public async move( public async move(
oldPath: RelativePath, oldPath: RelativePath,
newPath: RelativePath, newPath: RelativePath
documentId?: DocumentId
): Promise<void> { ): Promise<void> {
if (oldPath === newPath) { if (oldPath === newPath) {
return; return;
@ -165,26 +149,14 @@ export class FileOperations {
`Conflict when moving '${oldPath}' to '${newPath}', the latter already exists, deconflicting by moving it to '${deconflictedPath}'` `Conflict when moving '${oldPath}' to '${newPath}', the latter already exists, deconflicting by moving it to '${deconflictedPath}'`
); );
const document = // this.database.move(newPath, deconflictedPath);
this.database.getLatestDocumentByRelativePath(newPath); // this.database.move(oldPath, 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);
await this.fs.rename(newPath, deconflictedPath); await this.fs.rename(newPath, deconflictedPath);
} else { } else {
// this.database.move(oldPath, newPath);
await this.createParentDirectories(newPath); await this.createParentDirectories(newPath);
} }
this.database.move(oldPath, newPath);
await this.fs.rename(oldPath, newPath); await this.fs.rename(oldPath, newPath);
} }
@ -226,17 +198,12 @@ export class FileOperations {
); );
stem = stem.replace(FileOperations.PARENTHESES_REGEX, ""); stem = stem.replace(FileOperations.PARENTHESES_REGEX, "");
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition let newName;
while (true) { do {
const newName = currentCount++;
currentCount === 0 newName = `${directory}${stem} (${currentCount})${extension}`;
? `${directory}${stem}${extension}` } while (await this.fs.exists(newName));
: `${directory}${stem} (${currentCount})${extension}`;
if (await this.fs.exists(newName)) { return newName;
currentCount++;
} else {
return newName;
}
}
} }
} }

View file

@ -36,42 +36,44 @@ export class UnrestrictedSyncer {
proposedDocumentId: DocumentId, proposedDocumentId: DocumentId,
getLatestDocument: () => DocumentRecord getLatestDocument: () => DocumentRecord
): Promise<void> { ): Promise<void> {
let latestDocument = getLatestDocument(); let document = getLatestDocument();
return this.executeSync( return this.executeSync(
[latestDocument.relativePath], [document.relativePath],
SyncType.CREATE, SyncType.CREATE,
SyncSource.PUSH, SyncSource.PUSH,
async () => { async () => {
document = getLatestDocument();
const contentBytes = await this.operations.read( const contentBytes = await this.operations.read(
latestDocument.relativePath document.relativePath
); // this can throw FileNotFoundError ); // this can throw FileNotFoundError
const contentHash = hash(contentBytes); const contentHash = hash(contentBytes);
const response = await this.syncService.create({ const response = await this.syncService.create({
documentId: proposedDocumentId, documentId: proposedDocumentId,
relativePath: latestDocument.relativePath, relativePath: document.relativePath,
contentBytes contentBytes
}); });
latestDocument = getLatestDocument(); document = getLatestDocument();
this.history.addHistoryEntry({ this.history.addHistoryEntry({
status: SyncStatus.SUCCESS, status: SyncStatus.SUCCESS,
source: SyncSource.PUSH, source: SyncSource.PUSH,
relativePath: latestDocument.relativePath, relativePath: document.relativePath,
message: `Successfully uploaded locally created file`, message: `Successfully uploaded locally created file`,
type: SyncType.CREATE type: SyncType.CREATE
}); });
this.database.setDocument( this.database.setDocument(
{ {
relativePath: latestDocument.relativePath, relativePath: document.relativePath,
documentId: response.documentId, documentId: response.documentId,
parentVersionId: response.vaultUpdateId, parentVersionId: response.vaultUpdateId,
hash: contentHash hash: contentHash
}, },
latestDocument.identity document.identity
); );
this.tryIncrementVaultUpdateId(response.vaultUpdateId); this.tryIncrementVaultUpdateId(response.vaultUpdateId);
@ -88,6 +90,8 @@ export class UnrestrictedSyncer {
SyncType.DELETE, SyncType.DELETE,
SyncSource.PUSH, SyncSource.PUSH,
async () => { async () => {
document = getLatestDocument();
const response = await this.syncService.delete({ const response = await this.syncService.delete({
documentId: document.documentId, documentId: document.documentId,
relativePath: document.relativePath relativePath: document.relativePath
@ -132,6 +136,9 @@ export class UnrestrictedSyncer {
SyncType.UPDATE, SyncType.UPDATE,
SyncSource.PUSH, SyncSource.PUSH,
async () => { async () => {
document = getLatestDocument();
const originalRelativePath = document.relativePath;
if (document.metadata === undefined || document.isDeleted) { if (document.metadata === undefined || document.isDeleted) {
this.logger.debug( this.logger.debug(
`Document ${document.relativePath} has been already deleted, no need to update it` `Document ${document.relativePath} has been already deleted, no need to update it`
@ -194,8 +201,6 @@ export class UnrestrictedSyncer {
}); });
if (response.isDeleted) { if (response.isDeleted) {
await this.operations.delete(document.relativePath);
this.history.addHistoryEntry({ this.history.addHistoryEntry({
status: SyncStatus.SUCCESS, status: SyncStatus.SUCCESS,
source: SyncSource.PULL, source: SyncSource.PULL,
@ -216,21 +221,25 @@ export class UnrestrictedSyncer {
document.identity document.identity
); );
await this.operations.delete(document.relativePath);
this.tryIncrementVaultUpdateId(response.vaultUpdateId); this.tryIncrementVaultUpdateId(response.vaultUpdateId);
return; return;
} }
if (response.relativePath != document.relativePath) { let actualPath = document.relativePath;
if (response.relativePath != originalRelativePath) {
// this.database.getNewResolvedDocumentByRelativePath( // this.database.getNewResolvedDocumentByRelativePath(
// response.relativePath, // response.relativePath,
// promise // promise
// ); // );
actualPath = response.relativePath;
await this.operations.move( await this.operations.move(
document.relativePath, document.relativePath,
response.relativePath, response.relativePath
response.documentId
); // this can throw FileNotFoundError ); // this can throw FileNotFoundError
} }
@ -239,7 +248,7 @@ export class UnrestrictedSyncer {
contentHash = hash(responseBytes); contentHash = hash(responseBytes);
await this.operations.write( await this.operations.write(
response.relativePath, actualPath,
contentBytes, contentBytes,
responseBytes responseBytes
); );
@ -253,12 +262,10 @@ export class UnrestrictedSyncer {
}); });
} }
document = getLatestDocument();
this.database.setDocument( this.database.setDocument(
{ {
documentId: response.documentId, documentId: response.documentId,
relativePath: document.relativePath, relativePath: actualPath,
parentVersionId: response.vaultUpdateId, parentVersionId: response.vaultUpdateId,
hash: contentHash hash: contentHash
}, },
@ -326,6 +333,7 @@ export class UnrestrictedSyncer {
); );
return; return;
} }
if ( if (
localMetadata?.metadata?.parentVersionId ?? localMetadata?.metadata?.parentVersionId ??
-1 >= remoteVersion.vaultUpdateId -1 >= remoteVersion.vaultUpdateId
@ -338,6 +346,21 @@ export class UnrestrictedSyncer {
const contentBytes = deserialize(content); 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( this.database.setDocument(
{ {
documentId: remoteVersion.documentId, documentId: remoteVersion.documentId,
@ -345,14 +368,10 @@ export class UnrestrictedSyncer {
parentVersionId: remoteVersion.vaultUpdateId, parentVersionId: remoteVersion.vaultUpdateId,
hash: hash(contentBytes) hash: hash(contentBytes)
}, },
localMetadata?.identity document.identity
);
await this.operations.create(
remoteVersion.relativePath,
contentBytes,
remoteVersion.documentId
); );
resolve();
this.database.removeDocumentPromise(promise);
this.history.addHistoryEntry({ this.history.addHistoryEntry({
status: SyncStatus.SUCCESS, status: SyncStatus.SUCCESS,

View file

@ -82,11 +82,11 @@ export class MockClient implements FileSystemOperations {
const newContentUint8Array = new TextEncoder().encode(newContent); const newContentUint8Array = new TextEncoder().encode(newContent);
this.localFiles.set(path, newContentUint8Array); this.localFiles.set(path, newContentUint8Array);
const existingPats = currentContent const existingParts = currentContent
.split(" ") .split(" ")
.map((part) => part.trim()); .map((part) => part.trim());
const newParts = newContent.split(" ").map((part) => part.trim()); const newParts = newContent.split(" ").map((part) => part.trim());
existingPats.forEach((part) => existingParts.forEach((part) =>
// all changes should be additive // all changes should be additive
assert( assert(
newParts.includes(part), newParts.includes(part),
@ -106,15 +106,20 @@ export class MockClient implements FileSystemOperations {
} }
public async write(path: RelativePath, content: Uint8Array): Promise<void> { public async write(path: RelativePath, content: Uint8Array): Promise<void> {
const hasExisted = this.localFiles.has(path);
this.localFiles.set(path, content); this.localFiles.set(path, content);
this.client.logger.info( this.client.logger.info(
`Updated file ${path} with:\n new content: ${new TextDecoder().decode(content)}` `Updated file ${path} with:\n new content: ${new TextDecoder().decode(content)}`
); );
void this.client.syncer.syncLocallyUpdatedFile({ if (hasExisted) {
relativePath: path void this.client.syncer.syncLocallyUpdatedFile({
}); relativePath: path
});
} else {
void this.client.syncer.syncLocallyCreatedFile(path);
}
} }
public async delete(path: RelativePath): Promise<void> { public async delete(path: RelativePath): Promise<void> {