This commit is contained in:
Andras Schmelczer 2026-09-26 20:22:16 +01:00
commit b07aa589ff

View file

@ -6,9 +6,10 @@ import { diff } from "reconcile-text";
import type {
Database,
EngineState,
StoredSnapshot
PendingRequest
} from "../persistence/database";
import { emptyState } from "../persistence/database";
import { toStoredSnapshot, type StoredSnapshot } from "../snapshot";
import type {
FileOperations,
FileWrite
@ -17,7 +18,6 @@ import type { Settings } from "../persistence/settings";
import type { SyncService } from "../services/sync-service";
import type { ServerConfig } from "../services/server-config";
import type { WebSocketManager } from "../services/websocket-manager";
import type { DocumentVersion } from "../services/types/DocumentVersion";
import type { DocumentVersionWithoutContent } from "../services/types/DocumentVersionWithoutContent";
import type { EventBatch } from "../services/types/EventBatch";
import type { EventRecord } from "../services/types/EventRecord";
@ -27,7 +27,7 @@ import type { PushContent } from "../services/types/PushContent";
import type { Logger } from "../tracing/logger";
import { EventListeners } from "../utils/data-structures/event-listeners";
import { VaultHistoryStatus } from "../types/vault-history-status";
import { mergeContent, toStoredSnapshot } from "./content";
import { mergeContent } from "./content";
import { scanLocalFiles, type LocalChange } from "./scan";
import {
mergeFileManifests,
@ -83,7 +83,7 @@ export class Syncer {
private readonly localChangeStorage: {
entries: LocalChange[];
save: (entries: LocalChange[]) => Promise<void>;
} = { entries: [], save: async () => { } }
} = { entries: [], save: async () => {} }
) {
this.changes = localChangeStorage.entries;
websocket.onWebSocketStatusChanged.add((connected) => {
@ -104,7 +104,7 @@ export class Syncer {
}
public get isFirstSyncComplete(): boolean {
return this.database.state.initialized;
return this.database.state.lastSeenUpdateId !== undefined;
}
public get hasLocalChanges(): boolean {
@ -123,7 +123,7 @@ export class Syncer {
public isDocumentUpToDate(path: string): boolean {
if (
!this.database.state.initialized ||
this.database.state.lastSeenUpdateId === undefined ||
!this.hasScanned ||
this.pendingPaths.has(path) ||
this.unsyncablePaths.has(path) ||
@ -136,17 +136,16 @@ export class Syncer {
if (!record) return false;
const document = this.database.state.documents[record.documentId];
if (
!document?.materialized ||
document?.observedHash === undefined ||
!document.base ||
document.bootstrap ||
document.observedHash !== document.base.hash ||
this.database.state.fileManifest.entries[record.documentId] !== path
) {
return false;
}
const remote = this.database.state.remoteHeads[record.documentId];
if (remote && remote.vaultUpdateId > document.base.vaultUpdateId)
const remote = document.remote;
if (remote && remote.vaultUpdateId > (document.base.vaultUpdateId ?? 0))
return false;
const { pending } = this.database.state;
@ -159,7 +158,7 @@ export class Syncer {
if (
pending?.type === "fileManifest" &&
pending.request.entries[record.documentId] !==
this.database.state.fileManifest.entries[record.documentId]
this.database.state.fileManifest.entries[record.documentId]
) {
return false;
}
@ -323,15 +322,13 @@ export class Syncer {
try {
await this.reloadLocalState();
if (
this.database.state.historyRecovery ||
(this.database.state.initialized &&
this.service.vaultHistoryStatus === VaultHistoryStatus.MISSING)
this.isFirstSyncComplete &&
this.service.vaultHistoryStatus === VaultHistoryStatus.MISSING
)
await this.recoverServerHistory();
if (!this.isFirstSyncComplete) await this.service.resetHistory();
await this.serverConfig.initialize();
if (!this.database.state.initialized) {
await this.bootstrap();
}
if (!this.isFirstSyncComplete) await this.bootstrap();
if (!this.stopped) this.onReadyForEvents.trigger();
do {
this.dirty = false;
@ -341,31 +338,9 @@ export class Syncer {
await this.finishPending();
}
const batch = await this.service.getEvents(
this.database.state.lastSeenUpdateId
this.database.state.lastSeenUpdateId!
);
await this.incorporateEventBatch(batch);
// Previously excluded content becomes eligible after a setting
// change. Its saved obligation survives advancing the cursor.
if (Object.keys(this.database.state.excluded ?? {}).length)
await this.incorporateFileManifest(
this.database.state.fileManifest
);
else await this.scan();
for (const head of Object.values(
this.database.state.remoteHeads
)) {
const doc = this.database.state.documents[head.documentId];
if (
this.database.state.local[head.documentId] !==
undefined &&
(!doc?.materialized ||
!doc.base ||
doc.bootstrap ||
doc.base.vaultUpdateId < head.vaultUpdateId)
) {
await this.incorporateContent(head);
}
}
if (await this.preparePush()) {
await this.finishPending();
this.dirty = true;
@ -399,52 +374,40 @@ export class Syncer {
this.onServerHistoryChanged.trigger();
this.contentCache.reset();
this.hasScanned = false;
if (!this.database.state.historyRecovery) {
await this.scan();
const guard = this.localGuard();
const previous = this.database.state;
const next = emptyState(previous.vaultKey);
next.historyRecovery = true;
next.lastAppliedLocalChangeId = previous.lastAppliedLocalChangeId;
next.protectedPaths = previous.protectedPaths;
for (const [id, path] of Object.entries(previous.local)) {
const info = await this.files.fs.stat(path);
if (info?.kind !== "file") continue;
const doc = previous.documents[id];
if (
this.settings.isIgnored(path) ||
this.settings.isOversized(info.size)
) {
next.local[id] = path;
next.documents[id] = {
materialized: true,
bootstrap: true
};
(next.excluded ??= {})[id] =
previous.fileManifest.entries[id] ?? null;
continue;
}
const snapshot = await this.files.snapshot(path);
if (!snapshot) throw new LocalChangesDuringReconciliation();
// An unsent edit from the abandoned history is a separate local
// document. It must never be diffed against a reused version ID.
const clean = doc?.base?.hash === snapshot.hash;
const localId = doc?.base && !clean ? uuid() : id;
next.local[localId] = path;
next.documents[localId] = {
materialized: true,
observedHash: snapshot.hash,
recoveryBase: clean ? snapshot : doc?.recoveryBase
};
await this.scan();
const guard = this.localGuard();
const previous = this.database.state;
const next = emptyState(previous.vaultKey);
next.lastAppliedLocalChangeId = previous.lastAppliedLocalChangeId;
for (const [id, path] of Object.entries(previous.local)) {
const info = await this.files.fs.stat(path);
if (info?.kind !== "file") continue;
const doc = previous.documents[id];
if (
this.settings.isIgnored(path) ||
this.settings.isOversized(info.size)
) {
next.local[id] = path;
next.documents[id] = { observedHash: doc?.observedHash ?? "" };
(next.excluded ??= {})[id] =
previous.fileManifest.entries[id] ?? null;
continue;
}
guard();
// Persist the reset BEFORE releasing the old checkpoint. After a
// crash no old request can be retried against the replacement log.
await this.database.commit(next);
const snapshot = await this.files.snapshot(path);
if (!snapshot) throw new LocalChangesDuringReconciliation();
const clean = doc?.base?.hash === snapshot.hash;
const localId = doc?.base && !clean ? uuid() : id;
next.local[localId] = path;
next.documents[localId] = {
observedHash: snapshot.hash,
base: clean
? { vaultUpdateId: null, hash: snapshot.hash }
: undefined
};
}
await this.service.resetHistory();
const next = this.next();
delete next.historyRecovery;
guard();
// An absent cursor durably abandons all old requests and version IDs.
// The next bootstrap clears the transport checkpoint before any request.
await this.database.commit(next);
}
@ -511,20 +474,11 @@ export class Syncer {
}
private async bootstrap(): Promise<void> {
let vaultSnapshot = this.database.state.bootstrap;
if (!vaultSnapshot) {
vaultSnapshot = await this.service.getVaultSnapshot();
validateFileManifest(vaultSnapshot.fileManifest.entries);
await this.database.commit({
...this.next(),
bootstrap: vaultSnapshot
});
}
await this.incorporateFileManifest(
vaultSnapshot.fileManifest,
vaultSnapshot.headEventId,
vaultSnapshot
);
const vaultSnapshot = await this.service.getVaultSnapshot();
await this.reconcile(vaultSnapshot.fileManifest, {
cursor: vaultSnapshot.headEventId,
initial: vaultSnapshot
});
}
private async scan(initial?: VaultSnapshot): Promise<void> {
@ -548,13 +502,15 @@ export class Syncer {
}
private async incorporateEventBatch(batch: EventBatch): Promise<void> {
let cursor = this.database.state.lastSeenUpdateId;
let cursor = this.database.state.lastSeenUpdateId ?? 0;
let manifest = this.database.state.fileManifest;
const heads = { ...this.database.state.remoteHeads };
const heads: Record<string, DocumentVersionWithoutContent> = {};
for (const doc of Object.values(this.database.state.documents))
if (doc.remote) heads[doc.remote.documentId] = doc.remote;
const receipts: EventRecord[] = [];
// Fold every page before touching local files. If fetching is interrupted,
// discard this work and retry from the last committed cursor.
for (; ;) {
for (;;) {
const after = cursor;
for (const event of batch.events) {
if (event.eventId <= cursor) continue;
@ -579,7 +535,7 @@ export class Syncer {
} else if (
!heads[event.document.documentId] ||
heads[event.document.documentId].vaultUpdateId <
event.document.vaultUpdateId
event.document.vaultUpdateId
) {
heads[event.document.documentId] = this.documentHead(
event.document
@ -596,7 +552,6 @@ export class Syncer {
if (end === batch.headEventId) break;
batch = await this.service.getEvents(cursor);
}
if (cursor === this.database.state.lastSeenUpdateId) return;
// Recover our submitted bases before folding later remote edits. A
// permanent retry failure cannot cancel an earlier in-flight attempt.
const next = this.next();
@ -608,13 +563,15 @@ export class Syncer {
if (!pending) continue;
if (event.type === "content" && pending.type === "content") {
const doc = next.documents[pending.documentId];
if (!doc.base || doc.base.vaultUpdateId < event.eventId) {
if (
!doc.base ||
(doc.base.vaultUpdateId ?? 0) < event.eventId
) {
doc.base = {
...event.document,
vaultUpdateId: event.eventId,
hash: pending.snapshot.hash
};
doc.bootstrap = false;
delete doc.rejected;
this.clearRejections(next, pending.documentId);
await this.cacheContent(
event.eventId,
next.local[pending.documentId],
@ -627,7 +584,7 @@ export class Syncer {
) {
if (next.fileManifest.fileManifestId < event.eventId) {
next.fileManifest = event.fileManifest;
delete next.rejectedManifest;
this.clearRejections(next, "manifest");
}
} else
throw new PermanentSyncError(
@ -639,13 +596,7 @@ export class Syncer {
if (acknowledged) await this.database.commit(next);
// Three-way merging compares snapshots. Replaying intermediate remote
// edits can discard a local edit even when the remote ends at its base.
await this.incorporateFileManifest(
manifest,
cursor,
undefined,
false,
heads
);
await this.reconcile(manifest, { cursor, heads });
}
private documentHead(
@ -679,34 +630,30 @@ export class Syncer {
details:
from && to && from !== to
? {
type: SyncType.MOVE,
relativePath: to,
movedFrom: from
}
type: SyncType.MOVE,
relativePath: to,
movedFrom: from
}
: !from && to
? { type: SyncType.CREATE, relativePath: to }
: from && !to
? { type: SyncType.DELETE, relativePath: from }
: {
type: SyncType.UPDATE,
relativePath: to ?? from
}
? { type: SyncType.CREATE, relativePath: to }
: from && !to
? { type: SyncType.DELETE, relativePath: from }
: {
type: SyncType.UPDATE,
relativePath: to ?? from
}
});
}
}
private async remoteSnapshot(
head: DocumentVersionWithoutContent,
path: string,
supplied?: DocumentVersion
head: Pick<
DocumentVersionWithoutContent,
"documentId" | "vaultUpdateId"
>,
path: string
): Promise<StoredSnapshot> {
const content =
supplied !== undefined
? base64ToBytes(supplied.contentBase64)
: await this.service.getDocumentVersionContent({
documentId: head.documentId,
vaultUpdateId: head.vaultUpdateId
});
const content = await this.service.getDocumentVersionContent(head);
await this.cacheContent(head.vaultUpdateId, path, content);
return toStoredSnapshot({ content });
}
@ -714,12 +661,9 @@ export class Syncer {
private async contentWrite(
next: EngineState,
head: DocumentVersionWithoutContent,
path: string,
supplied?: DocumentVersion
path: string
): Promise<FileWrite | undefined> {
const document = (next.documents[head.documentId] ??= {
materialized: false
});
const document = (next.documents[head.documentId] ??= {});
if (
next.excluded?.[head.documentId] !== undefined ||
this.settings.isIgnored(path) ||
@ -730,7 +674,7 @@ export class Syncer {
const currentPath = this.database.state.local[head.documentId];
const local =
currentPath &&
(await this.files.fs.stat(currentPath))?.kind === "file"
(await this.files.fs.stat(currentPath))?.kind === "file"
? await this.files.snapshot(currentPath)
: undefined;
if (
@ -739,16 +683,27 @@ export class Syncer {
) {
return undefined;
}
if (
document.base?.vaultUpdateId === null &&
local &&
local.hash !== document.base.hash
)
throw new LocalChangesDuringReconciliation();
const base =
document.recoveryBase ??
(local !== undefined &&
document.base !== undefined &&
document.bootstrap !== true
? await this.remoteSnapshot(document.base, path)
: undefined);
local && document.base
? document.base.vaultUpdateId === null
? local
: await this.remoteSnapshot(
{
documentId: head.documentId,
vaultUpdateId: document.base.vaultUpdateId
},
path
)
: undefined;
// Cache the new head after the older merge base so it remains the most
// recently used version and can back the follow-up local diff.
const remote = await this.remoteSnapshot(head, path, supplied);
const remote = await this.remoteSnapshot(head, path);
let result = remote;
if (local !== undefined) {
result = await mergeContent(
@ -759,74 +714,30 @@ export class Syncer {
(await this.serverConfig.getConfig()).mergeableFileExtensions
);
}
document.base = { ...this.documentHead(head), hash: remote.hash };
delete document.recoveryBase;
document.bootstrap = false;
document.materialized = true;
document.base = {
vaultUpdateId: head.vaultUpdateId,
hash: remote.hash
};
delete document.remote;
document.observedHash = result.hash;
return local?.contentBase64 === result.contentBase64
? undefined
: { expected: local, replacement: result };
}
private async incorporateContent(
head: DocumentVersionWithoutContent,
cursor?: number,
supplied?: DocumentVersion,
clearPending = false
): Promise<void> {
await this.scan();
const guard = this.localGuard();
const next = this.next();
if (
!next.remoteHeads[head.documentId] ||
next.remoteHeads[head.documentId].vaultUpdateId < head.vaultUpdateId
) {
next.remoteHeads[head.documentId] = this.documentHead(head);
}
if (cursor !== undefined) {
next.lastSeenUpdateId = cursor;
}
if (clearPending) {
delete next.pending;
}
const path = next.local[head.documentId];
const document = next.documents[head.documentId];
const wasMaterialized = document?.materialized;
const writes: Record<string, FileWrite> = {};
if (
path !== undefined &&
(!document?.base ||
!document.materialized ||
document.bootstrap ||
document.base.vaultUpdateId < head.vaultUpdateId)
) {
const write = await this.contentWrite(next, head, path, supplied);
if (write) {
writes[head.documentId] = write;
}
}
await this.files.apply(next, writes, guard);
if (path !== undefined && writes[head.documentId]) {
this.history.addHistoryEntry({
status: SyncStatus.SUCCESS,
message: "Remote content applied",
details: {
type: wasMaterialized ? SyncType.UPDATE : SyncType.CREATE,
relativePath: path
},
author: head.userId,
timestamp: new Date(head.updatedDate)
});
}
}
private async incorporateFileManifest(
private async reconcile(
remote: FileManifest,
cursor?: number,
initial?: VaultSnapshot,
clearPending = false,
heads?: EngineState["remoteHeads"]
{
cursor,
initial,
clearPending = false,
heads
}: {
cursor?: number;
initial?: VaultSnapshot;
clearPending?: boolean;
heads?: Record<string, DocumentVersionWithoutContent>;
} = {}
): Promise<void> {
validateFileManifest(remote.entries);
await this.scan(initial);
@ -852,7 +763,8 @@ export class Syncer {
const previousManifest = next.fileManifest.entries;
next.local = mergeFileManifests(base, next.local, remote.entries);
next.fileManifest = remote;
if (heads) next.remoteHeads = heads;
for (const head of Object.values(heads ?? {}))
(next.documents[head.documentId] ??= {}).remote = head;
for (const [id, oldPath] of Object.entries(before)) {
const path = next.local[id];
if (
@ -877,41 +789,43 @@ export class Syncer {
next.local,
remote.entries,
Object.keys(next.excluded ?? {}),
next.protectedPaths
[...this.unsyncablePaths]
);
if (initial) {
next.initialized = true;
delete next.bootstrap;
for (const head of initial.documents) {
next.remoteHeads[head.documentId] = this.documentHead(head);
}
for (const head of initial.documents)
(next.documents[head.documentId] ??= {}).remote =
this.documentHead(head);
}
const writes: Record<string, FileWrite> = {};
for (const [id, path] of Object.entries(next.local)) {
next.documents[id] ??= { materialized: false };
if (before[id] === undefined)
next.documents[id].materialized = false;
const doc = (next.documents[id] ??= {});
if (before[id] === undefined) delete doc.observedHash;
if (
doc.observedHash !== undefined &&
doc.remote &&
doc.remote.vaultUpdateId <= (doc.base?.vaultUpdateId ?? 0)
)
delete doc.remote;
if (
next.excluded?.[id] === undefined &&
!this.settings.isIgnored(path) &&
remote.entries[id] !== undefined &&
(remote.entries[id] !== undefined ||
doc.remote !== undefined) &&
(!this.database.state.local[id] ||
!next.documents[id].base ||
next.documents[id].bootstrap ||
!next.documents[id].materialized ||
(next.remoteHeads[id]?.vaultUpdateId ?? 0) >
(next.documents[id].base?.vaultUpdateId ?? 0))
!doc.base ||
doc.observedHash === undefined ||
(doc.remote?.vaultUpdateId ?? 0) >
(doc.base.vaultUpdateId ?? 0))
) {
const knownHead =
next.remoteHeads[id] ??
initial?.documents.find((doc) => doc.documentId === id);
const head = knownHead ?? (await this.service.getDocumentMetadata(id));
const knownHead = doc.remote;
const head =
knownHead ?? (await this.service.getDocumentMetadata(id));
if (!head) {
throw new PermanentSyncError(
`File manifest references missing content: ${id}`
);
}
next.remoteHeads[id] = this.documentHead(head);
doc.remote = this.documentHead(head);
const write = await this.contentWrite(next, head, path);
if (write) {
writes[id] = write;
@ -932,7 +846,7 @@ export class Syncer {
this.database.state.local,
this.database.state.fileManifest.entries,
Object.keys(this.database.state.excluded ?? {}),
this.database.state.protectedPaths
[...this.unsyncablePaths]
);
if (!sameFileManifest(canonical, this.database.state.local)) {
await this.files.apply(
@ -960,11 +874,11 @@ export class Syncer {
if (
this.settings.isIgnored(path) ||
this.database.state.excluded?.[id] !== undefined ||
!this.database.state.documents[id]?.materialized ||
this.database.state.documents[id]?.observedHash === undefined ||
this.unsyncablePaths.has(path) ||
this.settings.isOversized(
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Document heads are a sparse ID map at runtime.
this.database.state.remoteHeads[id]?.contentSize ?? 0
this.database.state.documents[id]?.remote?.contentSize ?? 0
)
) {
continue;
@ -978,10 +892,15 @@ export class Syncer {
) {
continue;
}
const { base, rejected } = this.database.state.documents[id];
const { base } = this.database.state.documents[id];
if (
base?.hash === snapshot.hash ||
rejected?.hash === snapshot.hash
this.rejections.some(
(request) =>
request.type === "content" &&
request.documentId === id &&
request.snapshot.hash === snapshot.hash
)
) {
continue;
}
@ -997,7 +916,7 @@ export class Syncer {
parentVersionId: base?.vaultUpdateId ?? null,
content: await this.pushContent(
path,
base?.vaultUpdateId,
base?.vaultUpdateId ?? undefined,
snapshot
)
}
@ -1013,11 +932,11 @@ export class Syncer {
entries,
this.database.state.fileManifest.entries
) &&
(!this.database.state.rejectedManifest ||
!sameFileManifest(
entries,
this.database.state.rejectedManifest.entries
))
!this.rejections.some(
(request) =>
request.type === "fileManifest" &&
sameFileManifest(entries, request.request.entries)
)
) {
guard();
await this.database.commit({
@ -1058,7 +977,7 @@ export class Syncer {
this.database.state.fileManifest.entries,
Object.keys(this.database.state.excluded ?? {}),
[
...(this.database.state.protectedPaths ?? []),
...this.unsyncablePaths,
...Object.keys(this.database.state.excluded ?? {}).flatMap(
(id) =>
this.database.state.local[id]
@ -1069,50 +988,59 @@ export class Syncer {
);
}
private get rejections(): PendingRequest[] {
return (this.database.state.unconfirmed ?? []).filter(
(request) => request.rejection !== undefined
);
}
private get rejectionMessage(): string | undefined {
const { state } = this.database;
for (const [id, path] of Object.entries(state.local ?? {})) {
const doc = state.documents[id];
if (
doc?.rejected &&
doc.observedHash === doc.rejected.hash &&
doc.base?.hash !== doc.rejected.hash &&
!this.settings.isIgnored(path) &&
state.excluded?.[id] === undefined
for (const request of this.rejections) {
if (request.type === "content") {
const id = request.documentId;
const path = state.local[id];
const doc = state.documents[id];
if (
path !== undefined &&
doc?.observedHash === request.snapshot.hash &&
doc.base?.hash !== request.snapshot.hash &&
!this.settings.isIgnored(path) &&
state.excluded?.[id] === undefined
)
return request.rejection;
} else if (
sameFileManifest(
this.manifestToSubmit(),
request.request.entries
) &&
!sameFileManifest(
state.fileManifest.entries,
request.request.entries
)
)
return doc.rejected.message;
return request.rejection;
}
if (
state.rejectedManifest &&
sameFileManifest(
this.manifestToSubmit(),
state.rejectedManifest.entries
) &&
!sameFileManifest(
state.fileManifest.entries,
state.rejectedManifest.entries
}
private clearRejections(state: EngineState, id?: string): void {
for (const request of state.unconfirmed ?? [])
if (
id === undefined ||
(request.type === "content"
? request.documentId
: "manifest") === id
)
)
return state.rejectedManifest.message;
return undefined;
delete request.rejection;
}
/** An explicit reset or settings change permits fresh attempts after rejection. */
public async retryRejectedRequests(): Promise<void> {
await this.database.recoverPersistence();
const next = structuredClone(this.database.state);
let changed = false;
if (next.rejectedManifest) {
delete next.rejectedManifest;
changed = true;
}
for (const doc of Object.values(next.documents)) {
if (doc.rejected) {
delete doc.rejected;
changed = true;
}
}
if (changed) await this.database.commit(next);
if (!this.rejections.length) return;
const next = this.next();
this.clearRejections(next);
await this.database.commit(next);
}
private async finishPending(): Promise<void> {
@ -1144,16 +1072,6 @@ export class Syncer {
if (pending.rejection && !pending.response) {
const next = this.next();
if (pending.type === "content") {
next.documents[pending.documentId].rejected = {
hash: pending.snapshot.hash,
message: pending.rejection
};
} else
next.rejectedManifest = {
entries: pending.request.entries,
message: pending.rejection
};
(next.unconfirmed ??= []).push(pending);
delete next.pending;
await this.database.commit(next);
@ -1165,7 +1083,7 @@ export class Syncer {
relativePath:
pending.type === "content"
? (next.local[pending.documentId] ??
pending.documentId)
pending.documentId)
: "File manifest"
}
});
@ -1175,15 +1093,10 @@ export class Syncer {
if (pending.type === "fileManifest") {
const response = pending.response!;
if (response.type === "StaleBase") {
await this.incorporateFileManifest(
response,
undefined,
undefined,
true
);
await this.reconcile(response, { clearPending: true });
} else {
const next = this.next();
delete next.rejectedManifest;
this.clearRejections(next, "manifest");
next.fileManifest = {
fileManifestId: response.fileManifestId,
entries: pending.request.entries
@ -1206,15 +1119,15 @@ export class Syncer {
details:
after === undefined
? {
type: SyncType.DELETE,
relativePath: before
}
type: SyncType.DELETE,
relativePath: before
}
: before === undefined
? {
? {
type: SyncType.CREATE,
relativePath: after
}
: {
: {
type: SyncType.MOVE,
relativePath: after,
movedFrom: before
@ -1225,25 +1138,19 @@ export class Syncer {
} else {
const response = pending.response!;
if (response.type === "StaleBase") {
await this.incorporateContent(
response,
undefined,
undefined,
true
);
await this.reconcile(this.database.state.fileManifest, {
clearPending: true,
heads: { [pending.documentId]: this.documentHead(response) }
});
} else {
const next = this.next();
const doc = (next.documents[pending.documentId] ??= {
materialized: false
});
const doc = (next.documents[pending.documentId] ??= {});
doc.base = {
...this.documentHead(response),
vaultUpdateId: response.vaultUpdateId,
hash: pending.snapshot.hash
};
doc.bootstrap = false;
delete doc.rejected;
next.remoteHeads[pending.documentId] =
this.documentHead(response);
this.clearRejections(next, pending.documentId);
delete doc.remote;
delete next.pending;
await this.database.commit(next);
const path = next.local[pending.documentId];