File size: 4,844 Bytes
94193b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | import { checkpointManager, Checkpoint } from './checkpoint';
import { getActiveVFS } from './index';
import { logger } from '@/lib/utils';
interface DirtyEvent {
projectId: string;
dirty: boolean;
}
class SaveManager {
private dirtyProjects = new Set<string>();
private listeners = new Set<(event: DirtyEvent) => void>();
private suppressionCounts = new Map<string, number>();
private manualCheckpoints = new Map<string, string>();
subscribe(listener: (event: DirtyEvent) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private emit(projectId: string): void {
const event: DirtyEvent = { projectId, dirty: this.isDirty(projectId) };
for (const listener of this.listeners) {
try {
listener(event);
} catch (error) {
logger.error('[SaveManager] Listener error', error);
}
}
}
private setDirty(projectId: string, dirty: boolean): void {
const isDirty = this.dirtyProjects.has(projectId);
if (dirty && !isDirty) {
this.dirtyProjects.add(projectId);
this.emit(projectId);
} else if (!dirty && isDirty) {
this.dirtyProjects.delete(projectId);
this.emit(projectId);
}
}
markDirty(projectId: string): void {
if (this.isSuppressed(projectId)) {
return;
}
this.setDirty(projectId, true);
}
markClean(projectId: string): void {
this.setDirty(projectId, false);
}
isDirty(projectId: string): boolean {
return this.dirtyProjects.has(projectId);
}
beginSuppression(projectId: string): void {
const current = this.suppressionCounts.get(projectId) ?? 0;
this.suppressionCounts.set(projectId, current + 1);
}
endSuppression(projectId: string): void {
const current = this.suppressionCounts.get(projectId) ?? 0;
if (current <= 1) {
this.suppressionCounts.delete(projectId);
return;
}
this.suppressionCounts.set(projectId, current - 1);
}
async runWithSuppressedDirty<T>(projectId: string, fn: () => Promise<T>): Promise<T> {
this.beginSuppression(projectId);
try {
return await fn();
} finally {
this.endSuppression(projectId);
}
}
private isSuppressed(projectId: string): boolean {
return (this.suppressionCounts.get(projectId) ?? 0) > 0;
}
async save(projectId: string, description?: string): Promise<Checkpoint> {
const activeVFS = getActiveVFS();
await activeVFS.init();
const project = await activeVFS.getProject(projectId);
const fallbackDescription = `Manual save @ ${new Date().toLocaleTimeString()}`;
const checkpoint = await checkpointManager.createCheckpoint(projectId, description || fallbackDescription, {
kind: 'manual',
baseRevisionId: project.lastSavedCheckpointId ?? null
});
project.lastSavedCheckpointId = checkpoint.id;
project.lastSavedAt = new Date(checkpoint.timestamp);
await activeVFS.updateProject(project);
// Trigger auto-sync after save (only place that should trigger sync)
// Use the internal triggerAutoSync method which has debouncing
(activeVFS as any).triggerAutoSync?.(projectId);
this.manualCheckpoints.set(projectId, checkpoint.id);
this.markClean(projectId);
return checkpoint;
}
async restoreLastSaved(projectId: string): Promise<boolean> {
const activeVFS = getActiveVFS();
await activeVFS.init();
const project = await activeVFS.getProject(projectId);
const checkpointId = project.lastSavedCheckpointId;
if (!checkpointId) {
logger.warn('[SaveManager] No saved checkpoint to restore', { projectId });
return false;
}
const restored = await this.runWithSuppressedDirty(projectId, async () => {
const exists = await checkpointManager.checkpointExists(checkpointId);
if (!exists) {
logger.warn('[SaveManager] Saved checkpoint missing', { projectId, checkpointId });
return false;
}
const success = await checkpointManager.restoreCheckpoint(checkpointId);
if (!success) {
logger.error('[SaveManager] Failed to restore saved checkpoint', { projectId, checkpointId });
}
return success;
});
if (restored) {
this.markClean(projectId);
}
return restored;
}
getSavedCheckpointId(projectId: string): string | null {
return this.manualCheckpoints.get(projectId) ?? null;
}
async syncProjectSaveState(projectId: string): Promise<void> {
const activeVFS = getActiveVFS();
await activeVFS.init();
const project = await activeVFS.getProject(projectId);
if (project.lastSavedCheckpointId) {
this.manualCheckpoints.set(projectId, project.lastSavedCheckpointId);
} else {
this.manualCheckpoints.delete(projectId);
}
}
}
export const saveManager = new SaveManager();
|