- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
70 lines
2.6 KiB
TypeScript
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);
|
|
});
|
|
}
|