Files
SPipe/src/backupStore.ts
T
bjkim1 21ebb16fb7 SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음)
- README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리
- 패치내역을 docs/로 이동, _worklog.md 추가
- 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
2026-10-30 11:00:46 +09:00

70 lines
2.6 KiB
TypeScript

// 자동 백업: versioned INP (+ asset attributes) copies of the model kept in a
// separate IndexedDB, so a bad edit hours ago can still be undone after the
// autosave (which keeps only the latest state) has overwritten it.
import type { AssetStore } from './assets';
const DB_NAME = 'spipe-backups';
const STORE = 'backups';
export const MAX_BACKUPS = 30;
export interface BackupRecord {
id: string;
savedAt: string;
fileName: string;
reason: 'auto' | 'manual';
inp: string;
assets?: AssetStore;
nodes: number;
links: number;
}
function open(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = () => {
if (!req.result.objectStoreNames.contains(STORE)) req.result.createObjectStore(STORE, { keyPath: 'id' });
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error ?? new Error('백업 저장소를 열 수 없습니다.'));
});
}
async function withStore<T>(mode: IDBTransactionMode, fn: (store: IDBObjectStore) => IDBRequest<T> | void): Promise<T | undefined> {
const db = await open();
try {
return await new Promise<T | undefined>((resolve, reject) => {
const tx = db.transaction(STORE, mode);
const req = fn(tx.objectStore(STORE));
tx.oncomplete = () => resolve(req ? req.result : undefined);
tx.onerror = () => reject(tx.error ?? new Error('백업 저장소 오류'));
tx.onabort = () => reject(tx.error ?? new Error('백업 저장 중단'));
});
} finally {
db.close();
}
}
export async function listBackups(): Promise<BackupRecord[]> {
const all = ((await withStore<BackupRecord[]>('readonly', (s) => s.getAll())) ?? []) as BackupRecord[];
return all.sort((a, b) => (a.savedAt < b.savedAt ? 1 : -1));
}
/** Save a backup unless it is identical to the newest one; trims to MAX_BACKUPS. */
export async function saveBackup(rec: Omit<BackupRecord, 'id' | 'savedAt'>): Promise<boolean> {
const existing = await listBackups();
const last = existing[0];
if (last && last.inp === rec.inp && JSON.stringify(last.assets ?? null) === JSON.stringify(rec.assets ?? null)) return false;
const savedAt = new Date().toISOString();
await withStore('readwrite', (s) => {
s.put({ ...rec, id: `${savedAt}_${Math.random().toString(36).slice(2, 7)}`, savedAt });
for (const old of existing.slice(MAX_BACKUPS - 1)) s.delete(old.id);
});
return true;
}
export async function deleteBackup(id: string): Promise<void> {
await withStore('readwrite', (s) => {
s.delete(id);
});
}