vault-link/frontend/sync-client/src/sync-operations/offline-change-detector.ts
2026-04-28 22:20:31 +01:00

95 lines
3.4 KiB
TypeScript

import type { DocumentRecord, RelativePath } from "./types";
import type { Logger } from "../tracing/logger";
import { hash } from "../utils/hash";
import type { FileOperations } from "../file-operations/file-operations";
import { findMatchingFile } from "../utils/find-matching-file";
import type { SyncEventQueue } from "./sync-event-queue";
import { removeFromArray } from "../utils/remove-from-array";
/**
* Scans the local filesystem and the document database to determine
* which files were created, updated, moved, or deleted while the
* client was offline, then enqueues the appropriate sync events.
*/
export async function scheduleOfflineChanges(
logger: Logger,
operations: FileOperations,
queue: SyncEventQueue,
enqueueCreate: (path: RelativePath) => void,
enqueueUpdate: (args: {
oldPath?: RelativePath;
relativePath: RelativePath;
}) => void,
enqueueDelete: (path: RelativePath) => void
): Promise<void> {
const allLocalFiles = new Set(await operations.listFilesRecursively());
logger.info(`Scheduling sync for ${allLocalFiles.size} local files`);
const allDocuments = queue.allSettledDocuments();
// A doc is "possibly deleted" only if it has no local file. Including
// docs that still exist locally would queue a spurious delete alongside
// the update below.
const locallyPossiblyDeletedFiles: DocumentRecord[] = [];
for (const record of allDocuments.values()) {
if (!allLocalFiles.has(record.path)) {
locallyPossiblyDeletedFiles.push(record);
}
}
const locallyPossibleCreatedFiles: RelativePath[] = [];
const syncedLocalFiles: RelativePath[] = [];
for (const localFile of allLocalFiles) {
if (allDocuments.has(localFile)) {
syncedLocalFiles.push(localFile);
} else {
locallyPossibleCreatedFiles.push(localFile);
}
}
const renamedPaths = new Set<RelativePath>();
for (const path of locallyPossibleCreatedFiles) {
const content = await operations.read(path);
const contentHash = await hash(content);
const matchingDeletedFile = await findMatchingFile(
contentHash,
locallyPossiblyDeletedFiles
);
if (matchingDeletedFile !== undefined) {
logger.debug(
`File ${path} might have been moved from ${matchingDeletedFile.path} while offline, scheduling sync to move it`
);
enqueueUpdate({
oldPath: matchingDeletedFile.path,
relativePath: path
});
removeFromArray(locallyPossiblyDeletedFiles, matchingDeletedFile);
renamedPaths.add(path);
}
}
for (const path of locallyPossibleCreatedFiles) {
if (renamedPaths.has(path)) {continue;}
logger.info(
`File ${path} was created while offline, scheduling sync to create it`
);
enqueueCreate(path);
}
for (const item of locallyPossiblyDeletedFiles) {
logger.info(
`File ${item.path} was deleted while offline, scheduling sync to delete it`
);
enqueueDelete(item.path);
}
for (const path of syncedLocalFiles) {
logger.info(
`File ${path} may have been updated while offline, scheduling sync to update it`
);
enqueueUpdate({ relativePath: path });
}
}