Roll back the last N transactions in a session.
(sessionId: string, count: number)
| 391 | |
| 392 | async storeBlob(hash: string, content: string): Promise<void> { |
| 393 | const blobPath = path.join(this.blobsDir, hash.slice(0, 2), hash); |
| 394 | await fs.mkdir(path.dirname(blobPath), { recursive: true }); |
| 395 | try { |
| 396 | await fs.access(blobPath); |
| 397 | return; |
| 398 | } catch {} |
| 399 | await writeFileAtomic(blobPath, content, { encoding: 'utf-8' }); |
| 400 | } |
| 401 | |
| 402 | async loadBlob(hash: string): Promise<string> { |
| 403 | const blobPath = path.join(this.blobsDir, hash.slice(0, 2), hash); |
| 404 | return await fs.readFile(blobPath, 'utf-8'); |
| 405 | } |
| 406 | |
| 407 | /** Rollback every committed transaction in a session, newest first. */ |
| 408 | async rollbackSession(sessionId: string): Promise<{ filesRestored: number; txnsRolled: number }> { |
| 409 | const rows = this.db.prepare(` |
| 410 | SELECT * FROM transactions WHERE session_id = ? AND status = 'committed' |
| 411 | ORDER BY id DESC |
| 412 | `).all(sessionId) as any[]; |
| 413 | |
| 414 | let filesRestored = 0; |
| 415 | const txnIds = new Set<number>(); |
| 416 | |
| 417 | for (const row of rows) { |
| 418 | try { |
| 419 | if (row.before_hash) { |
| 420 | const content = await this.loadBlob(row.before_hash); |
| 421 | await fs.mkdir(path.dirname(row.path), { recursive: true }); |
| 422 | await writeFileAtomic(row.path, content, { encoding: 'utf-8' }); |
| 423 | filesRestored++; |
| 424 | } else if (row.operation === 'write' || row.operation === 'create') { |
| 425 | await fs.unlink(row.path).catch(() => {}); |
| 426 | filesRestored++; |
| 427 | } |
| 428 | txnIds.add(row.txn_id); |
| 429 | } catch (e: any) { |
| 430 | logger.warn('Failed to rollback op', { id: row.id, err: e.message }); |
| 431 | } |
| 432 | } |
| 433 | |
| 434 | const tx = this.db.transaction(() => { |
no test coverage detected