openpencil/tests/engine/app/document/recovery/store.test.ts
Danila Poyarkov 6e68366bc6
fix(recovery): store snapshots of any size (#966)
* fix(recovery): snapshot a document only once it has unsaved edits

Opening a document lays it out, which bumps the scene version, so crash
recovery wrote a full .fig snapshot of every document opened from a file
without a writable source, edited or not. For the 401 MiB file from #924
that copied the whole archive a few seconds after opening and killed the
tab. Recovery now asks the document's change tracking, which ignores
layout, whether anything is unsaved.

* fix(recovery): store snapshots as Blobs so large documents fit

Chromium refuses a single IndexedDB value over 127 MiB, so a snapshot of
any larger document failed and recovery fell back to memory for the
rest of the session, holding another copy of the document. A Blob is
kept as a file at any size: a 421 MB snapshot now writes in IndexedDB.
Writes also no longer copy the archive first, and snapshots stored as
byte arrays by earlier versions still read back.

* test(recovery): drop the scene version from the large-buffer snapshot
2026-10-07 21:30:07 +00:00

74 lines
2.5 KiB
TypeScript

import 'fake-indexeddb/auto'
import { afterAll, beforeEach, describe, expect, test } from 'bun:test'
import { IDBFactory } from 'fake-indexeddb'
import { createIdbRecoveryStore } from '@/app/document/recovery/idb'
import { createMemoryRecoveryStore } from '@/app/document/recovery/memory'
const bytes = new Uint8Array([1, 2, 3, 4])
describe('document recovery store', () => {
// Every file in a shard shares this process and its `indexedDB` global.
// Another suite may leave the production recovery store's connection open
// (createEditorStore() opens it), which would block a deleteDatabase() here
// and queue our open() behind it until the test times out. A fresh factory
// per test isolates this suite from that and from its own previous test.
const sharedFactory = indexedDB
beforeEach(() => {
globalThis.indexedDB = new IDBFactory()
})
afterAll(() => {
globalThis.indexedDB = sharedFactory
})
test('stores metadata and FIG bytes atomically in IndexedDB', async () => {
const store = createIdbRecoveryStore()
const metadata = await store.write({
id: 'recovery-1',
documentName: 'Agent draft',
figBytes: bytes
})
expect(metadata).toMatchObject({
id: 'recovery-1',
documentName: 'Agent draft',
byteLength: 4,
formatVersion: 1
})
expect(await store.list()).toEqual([metadata])
expect(await store.read('recovery-1')).toEqual({ ...metadata, figBytes: bytes })
await store.remove('recovery-1')
expect(await store.list()).toEqual([])
expect(await store.read('recovery-1')).toBeNull()
})
test('stores a view into a larger buffer as its own bytes', async () => {
const store = createIdbRecoveryStore()
const buffer = new Uint8Array([9, 9, 1, 2, 3, 9])
await store.write({
id: 'recovery-1',
documentName: 'Agent draft',
figBytes: buffer.subarray(2, 5)
})
const stored = await store.read('recovery-1')
expect(stored?.figBytes).toEqual(new Uint8Array([1, 2, 3]))
expect(stored?.figBytes.buffer.byteLength).toBe(3)
})
test('memory store owns input and output bytes', async () => {
const store = createMemoryRecoveryStore()
const input = new Uint8Array(bytes)
await store.write({ id: 'one', documentName: 'Draft', figBytes: input })
input[0] = 99
const first = await store.read('one')
expect(first?.figBytes[0]).toBe(1)
if (first) first.figBytes[0] = 88
expect((await store.read('one'))?.figBytes[0]).toBe(1)
})
})