Files
app/src/services/tldraw/sharedStoreService.ts
T
CC Worker 65bce2c52d fix(auto-save): dirty-flag pattern in SharedStoreService
Replaces JSON.stringify snapshot comparison with a store.listen() dirty flag.
Eliminates 5-15ms main-thread serialize on every poll tick when canvas is idle.
2026-05-31 21:09:38 +00:00

128 lines
4.2 KiB
TypeScript

// External imports
import { TLStore, TLEditorSnapshot, loadSnapshot, getSnapshot } from '@tldraw/tldraw';
// Local imports
import { logger } from '../../debugConfig';
import { LoadingState } from './snapshotService';
import { storageService, StorageKeys } from '../auth/localStorageService';
interface AutoSaveConfig {
checkInterval: number; // Changed to required
saveInterval: number; // Changed to required
}
const DEFAULT_CONFIG: AutoSaveConfig = {
checkInterval: 5000,
saveInterval: 30000
};
export class SharedStoreService {
private lastSaveTime: number = Date.now();
private autoSaveInterval: ReturnType<typeof setTimeout> | null = null;
private config: AutoSaveConfig;
private isDirty = false;
private dirtyListener: (() => void) | null = null;
constructor(private store: TLStore, config?: Partial<AutoSaveConfig>) {
this.config = {
...DEFAULT_CONFIG,
...config
};
this.dirtyListener = store.listen(() => {
this.isDirty = true;
});
logger.debug('shared-store-service', '🏗️ Initializing SharedStoreService');
}
public startAutoSave(setLoadingState: (state: LoadingState) => void): void {
if (this.autoSaveInterval) {
this.stopAutoSave();
}
this.autoSaveInterval = setInterval(() => {
this.checkAndSave(setLoadingState);
}, this.config.checkInterval);
logger.debug('shared-store-service', '⏰ Auto-save started', {
checkInterval: this.config.checkInterval,
saveInterval: this.config.saveInterval
});
}
public stopAutoSave(): void {
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
this.autoSaveInterval = null;
logger.debug('shared-store-service', '⏹️ Auto-save stopped');
}
}
private async checkAndSave(setLoadingState: (state: LoadingState) => void): Promise<void> {
if (!this.isDirty) {
return;
}
this.isDirty = false;
const now = Date.now();
if (now - this.lastSaveTime >= this.config.saveInterval) {
await this.saveSnapshot(getSnapshot(this.store), setLoadingState);
this.lastSaveTime = now;
}
}
public async saveSnapshot(
snapshot: Partial<TLEditorSnapshot>,
setLoadingState: (state: LoadingState) => void
): Promise<void> {
try {
storageService.set(StorageKeys.LOCAL_SNAPSHOT, snapshot);
setLoadingState({ status: 'ready', error: '' });
logger.debug('shared-store-service', '✅ Snapshot saved successfully');
} catch (error) {
logger.error('shared-store-service', '❌ Failed to save snapshot:', error);
setLoadingState({
status: 'error',
error: error instanceof Error ? error.message : 'Failed to save snapshot'
});
}
}
public async loadSnapshot(
snapshot: Partial<TLEditorSnapshot>,
setLoadingState: (state: LoadingState) => void
): Promise<void> {
try {
setLoadingState({ status: 'loading', error: '' });
loadSnapshot(this.store, snapshot);
setLoadingState({ status: 'ready', error: '' });
logger.debug('shared-store-service', '✅ Snapshot loaded successfully');
} catch (error) {
logger.error('shared-store-service', '❌ Failed to load snapshot:', error);
this.store.clear();
setLoadingState({
status: 'error',
error: error instanceof Error ? error.message : 'Failed to load snapshot'
});
}
}
public getStore(): TLStore {
return this.store;
}
public clear(): void {
this.stopAutoSave();
this.dirtyListener?.();
this.dirtyListener = null;
this.store.clear();
this.isDirty = false;
logger.debug('shared-store-service', '🧹 Store cleared');
}
}
export const createSharedStore = (
store: TLStore,
config?: Partial<AutoSaveConfig>
): SharedStoreService => {
return new SharedStoreService(store, config);
};