....
This commit is contained in:
parent
3dab48f58b
commit
c869f0a880
2 changed files with 461 additions and 578 deletions
|
|
@ -17,16 +17,14 @@ import { mergeContent } from "../sync-operations/content";
|
|||
import { hash } from "../utils/hash";
|
||||
import { isBinary } from "../utils/is-binary";
|
||||
import { isFileTypeMergeable } from "../utils/is-file-type-mergeable";
|
||||
import { decodeText } from "../utils/decode-text";
|
||||
import type { ServerConfig } from "../services/server-config";
|
||||
import {
|
||||
applyLocalChange,
|
||||
unappliedChanges,
|
||||
type LocalChange
|
||||
} from "../sync-operations/local-changes";
|
||||
import {
|
||||
FileNotFoundError,
|
||||
LocalChangesDuringReconciliation
|
||||
} from "../errors/errors";
|
||||
import { LocalChangesDuringReconciliation } from "../errors/errors";
|
||||
|
||||
export interface FileWrite {
|
||||
expected?: HashedSnapshot;
|
||||
|
|
@ -62,15 +60,6 @@ export class FileOperations {
|
|||
this.paths = new FilePaths(fs, isProtectedFile);
|
||||
}
|
||||
|
||||
public async read(path: RelativePath): Promise<Uint8Array> {
|
||||
const snapshot = await this.fs.read(path);
|
||||
|
||||
if (!snapshot)
|
||||
throw new FileNotFoundError(`File not found: ${path}`, path);
|
||||
|
||||
return snapshot.content;
|
||||
}
|
||||
|
||||
public async snapshot(
|
||||
path: RelativePath
|
||||
): Promise<HashedSnapshot | undefined> {
|
||||
|
|
@ -84,10 +73,10 @@ export class FileOperations {
|
|||
);
|
||||
}
|
||||
|
||||
public async captureChange(
|
||||
public captureChange(
|
||||
change: LocalChange,
|
||||
previous: readonly LocalChange[]
|
||||
): Promise<void> {
|
||||
): void {
|
||||
const state = structuredClone(this.applying ?? this.database.state);
|
||||
|
||||
if (state.lastSeenUpdateId === undefined) return;
|
||||
|
|
@ -249,27 +238,23 @@ export class FileOperations {
|
|||
isBinary(value.content)
|
||||
)
|
||||
) {
|
||||
const decoder = new TextDecoder("utf-8", {
|
||||
fatal: true,
|
||||
ignoreBOM: true
|
||||
});
|
||||
const parent = decoder.decode(base.content);
|
||||
const remote = decoder.decode(replacement.content);
|
||||
const parent = decodeText(base.content);
|
||||
const remote = decodeText(replacement.content);
|
||||
const text = await this.fs.atomicUpdateText(path, (latest) =>
|
||||
reconcile(parent, latest, remote)
|
||||
);
|
||||
return hash(new TextEncoder().encode(text));
|
||||
}
|
||||
const result =
|
||||
current && write.expected
|
||||
current && base
|
||||
? await mergeContent(
|
||||
path,
|
||||
write.expected,
|
||||
base,
|
||||
await hashSnapshot(current),
|
||||
write.replacement,
|
||||
replacement,
|
||||
extensions
|
||||
)
|
||||
: write.replacement;
|
||||
: replacement;
|
||||
await this.fs.write(path, {
|
||||
content: result.content,
|
||||
cursors: result.cursors
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
import { v4 as uuid } from "uuid";
|
||||
import type { FileSystemOperations } from "./filesystem-operations";
|
||||
import type { DocumentId, EngineState, RelativePath } from "../persistence/database";
|
||||
import type {
|
||||
DocumentId,
|
||||
EngineState,
|
||||
RelativePath
|
||||
} from "../persistence/database";
|
||||
import { allocatePortablePath, isInternalPath } from "../utils/portable-path";
|
||||
|
||||
/** Resolve occupied rename destinations without discarding another file. */
|
||||
export class FilePaths {
|
||||
public constructor(
|
||||
private readonly fs: FileSystemOperations,
|
||||
private readonly isProtectedFile: (path: RelativePath, size: number) => boolean
|
||||
) { }
|
||||
private readonly isProtectedFile: (
|
||||
path: RelativePath,
|
||||
size: number
|
||||
) => boolean
|
||||
) {}
|
||||
|
||||
public async prepareDestination(
|
||||
next: EngineState,
|
||||
|
|
@ -18,7 +25,7 @@ export class FilePaths {
|
|||
mutate: (action: () => Promise<void>) => Promise<void>
|
||||
): Promise<RelativePath> {
|
||||
try {
|
||||
for (; ;) {
|
||||
for (;;) {
|
||||
const obstruction = await this.obstruction(path);
|
||||
if (!obstruction) break;
|
||||
const children =
|
||||
|
|
@ -29,8 +36,8 @@ export class FilePaths {
|
|||
for (const child of children) {
|
||||
const occupant = Object.keys(current.local).find(
|
||||
(key) =>
|
||||
current.documents[key]?.observedHash !== undefined &&
|
||||
current.local[key] === child
|
||||
current.documents[key]?.observedHash !==
|
||||
undefined && current.local[key] === child
|
||||
);
|
||||
const info = await this.fs.stat(child);
|
||||
if (
|
||||
|
|
@ -65,7 +72,9 @@ export class FilePaths {
|
|||
next.local[occupant.id] = displaced;
|
||||
}
|
||||
if (obstruction.kind === "directory")
|
||||
await mutate(() => this.fs.deleteDirectory(obstruction.path));
|
||||
await mutate(() =>
|
||||
this.fs.deleteDirectory(obstruction.path)
|
||||
);
|
||||
}
|
||||
await this.ensureParents(path);
|
||||
} catch (error) {
|
||||
|
|
@ -113,14 +122,13 @@ export class FilePaths {
|
|||
id: DocumentId,
|
||||
path: RelativePath
|
||||
): Promise<RelativePath> {
|
||||
const occupied = { ...next.local };
|
||||
delete occupied[id];
|
||||
const files = (await this.fs.listFilesRecursively()).filter(
|
||||
(path) => !isInternalPath(path)
|
||||
const occupied = Object.entries(next.local).flatMap(
|
||||
([other, occupiedPath]) => (other === id ? [] : [occupiedPath])
|
||||
);
|
||||
for (const [i, diskPath] of files.entries())
|
||||
occupied[`disk-${i}`] = diskPath;
|
||||
return allocatePortablePath(path, id, occupied);
|
||||
const files = (await this.fs.listFilesRecursively()).filter(
|
||||
(diskPath) => !isInternalPath(diskPath)
|
||||
);
|
||||
return allocatePortablePath(path, id, [...occupied, ...files]);
|
||||
}
|
||||
|
||||
private async ensureParents(path: RelativePath): Promise<void> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue