mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
195328f7a6
* Persist saved annotate file edits in drafts * Handle stale saved file edit context * Test source edit conflict actions * Return source metadata for single-file docs * Fix saved file edit conflict races * Handle deleted source files in annotate edits * Tighten annotate missing-file recovery * Reset edit state for missing file reopen * Tighten source edit restore path handling * Harden source edit recovery paths * Harden source edit disk reconciliation * Fix live file tree startup delay * Document file tree watcher startup ordering * Preserve source save through missing files and symlinks * Harden missing source file recovery * Tighten annotate source edit boundaries
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
import { pathIsInsideDir } from '@plannotator/shared/browser-paths';
|
|
import {
|
|
canApplyEditableDocumentDiskSnapshot,
|
|
getEditableDocumentKnownDiskHash,
|
|
type DiskSnapshotReconcileResult,
|
|
type EditableDocumentRecord,
|
|
type EnabledSourceSaveCapability,
|
|
} from './editableDocuments';
|
|
import type { SourceDocumentSnapshotResult } from './sourceDocumentClient';
|
|
|
|
export type OpenSourceDocumentRecord = EditableDocumentRecord & { sourceSave: EnabledSourceSaveCapability };
|
|
|
|
export type SourceDocumentReconcileEvent =
|
|
| {
|
|
type: 'file-missing';
|
|
result: { record: EditableDocumentRecord; clearedSavedChange: boolean; alreadyMissing: boolean };
|
|
}
|
|
| { type: 'clean-updated'; result: Extract<DiskSnapshotReconcileResult, { type: 'clean-updated' }> }
|
|
| { type: 'status-updated'; result: Extract<DiskSnapshotReconcileResult, { type: 'status-updated' }> }
|
|
| { type: 'conflict'; result: Extract<DiskSnapshotReconcileResult, { type: 'conflict' }> };
|
|
|
|
interface ReconcileSourceDocumentsOptions {
|
|
changedDir?: string;
|
|
documents: OpenSourceDocumentRecord[];
|
|
sequenceByKey: Map<string, number>;
|
|
getDocument: (key: string) => EditableDocumentRecord | null;
|
|
fetchSnapshot: (path: string) => Promise<SourceDocumentSnapshotResult>;
|
|
markFileMissing: (key: string) => { record: EditableDocumentRecord; clearedSavedChange: boolean; alreadyMissing: boolean } | null;
|
|
reconcileDiskSnapshot: (input: {
|
|
key: string;
|
|
text: string;
|
|
sourceSave: EnabledSourceSaveCapability;
|
|
}) => DiskSnapshotReconcileResult;
|
|
onEvent: (event: SourceDocumentReconcileEvent) => void;
|
|
}
|
|
|
|
export async function reconcileSourceDocuments({
|
|
changedDir,
|
|
documents,
|
|
sequenceByKey,
|
|
getDocument,
|
|
fetchSnapshot,
|
|
markFileMissing,
|
|
reconcileDiskSnapshot,
|
|
onEvent,
|
|
}: ReconcileSourceDocumentsOptions): Promise<boolean> {
|
|
const docs = documents.filter((doc) => !changedDir || pathIsInsideDir(doc.sourceSave.path, changedDir));
|
|
let changed = false;
|
|
|
|
for (const doc of docs) {
|
|
const startRecord = getDocument(doc.key);
|
|
if (startRecord?.saveStatus === 'saving') continue;
|
|
const expectedDiskHash = getEditableDocumentKnownDiskHash(startRecord);
|
|
const seq = (sequenceByKey.get(doc.key) ?? 0) + 1;
|
|
sequenceByKey.set(doc.key, seq);
|
|
const snapshotResult = await fetchSnapshot(doc.sourceSave.path);
|
|
if (sequenceByKey.get(doc.key) !== seq) continue;
|
|
const currentRecord = getDocument(doc.key);
|
|
if (!canApplyEditableDocumentDiskSnapshot(currentRecord, expectedDiskHash)) continue;
|
|
|
|
if (snapshotResult.status === 'missing') {
|
|
const result = markFileMissing(doc.key);
|
|
if (!result) continue;
|
|
if (!result.alreadyMissing || result.clearedSavedChange) changed = true;
|
|
onEvent({ type: 'file-missing', result });
|
|
continue;
|
|
}
|
|
|
|
if (snapshotResult.status === 'unavailable') continue;
|
|
|
|
const { snapshot } = snapshotResult;
|
|
const result = reconcileDiskSnapshot({
|
|
key: doc.key,
|
|
text: snapshot.markdown,
|
|
sourceSave: snapshot.sourceSave,
|
|
});
|
|
|
|
if (result.type === 'clean-updated') {
|
|
changed = true;
|
|
onEvent({ type: 'clean-updated', result });
|
|
} else if (result.type === 'status-updated') {
|
|
changed = true;
|
|
onEvent({ type: 'status-updated', result });
|
|
} else if (result.type === 'conflict') {
|
|
changed = true;
|
|
onEvent({ type: 'conflict', result });
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|