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
This commit is contained in:
parent
4254204685
commit
6e68366bc6
|
|
@ -125,6 +125,7 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- Store crash recovery snapshots of any size. Snapshots of documents over 127 MiB failed to save to IndexedDB and stayed in memory for the rest of the session.
|
||||
- Keep an opened `.fig` file saved until it is edited. Laying out its first page, which recomputes auto-layout sizes and positions, and showing another page for the first time, which loads its layers from the file, marked it unsaved, so closing it asked to save changes nobody made.
|
||||
- Run crash recovery and autosave after edits, not whenever the canvas redraws. Opening a document, laying out a page, or loading a font started a recovery snapshot or an autosave, which encoded the whole document again once another page had loaded. In Safari, where every opened file gets recovery snapshots, a large page froze the browser for minutes after it first appeared.
|
||||
- Open image-heavy `.fig` files without the canvas running out of memory (#924). Decoded images stay within a fixed budget, and documents with many large images draw previews sized to the view, decoded in the background a few at a time, while exports keep the full images.
|
||||
|
|
|
|||
|
|
@ -27,10 +27,30 @@ interface RecoveryDatabase extends DBSchema {
|
|||
}
|
||||
fig: {
|
||||
key: string
|
||||
value: Uint8Array
|
||||
value: StoredFig
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A snapshot as IndexedDB stores it. Chromium refuses a single value over 127 MiB, which a
|
||||
* document's archive passes easily, but keeps a Blob as a file at any size, and faster.
|
||||
* Snapshots from earlier versions are byte arrays.
|
||||
*/
|
||||
type StoredFig = Blob | Uint8Array
|
||||
|
||||
/** A Blob of the snapshot without copying it; bytes over a shared buffer are copied first. */
|
||||
function figBlob(bytes: Uint8Array): Blob {
|
||||
const plain =
|
||||
bytes.buffer instanceof ArrayBuffer
|
||||
? new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
||||
: bytes.slice()
|
||||
return new Blob([plain])
|
||||
}
|
||||
|
||||
async function figBytesOf(stored: StoredFig): Promise<Uint8Array> {
|
||||
return stored instanceof Blob ? new Uint8Array(await stored.arrayBuffer()) : stored
|
||||
}
|
||||
|
||||
export function createIdbRecoveryStore(): RecoveryStore {
|
||||
const database = openAppDatabase(recoveryDatabase)
|
||||
|
||||
|
|
@ -43,13 +63,13 @@ export function createIdbRecoveryStore(): RecoveryStore {
|
|||
async read(id: string) {
|
||||
const db = await database
|
||||
const transaction = db.transaction(['meta', 'fig'])
|
||||
const [metadata, figBytes] = await Promise.all([
|
||||
const [metadata, stored] = await Promise.all([
|
||||
transaction.objectStore('meta').get(id),
|
||||
transaction.objectStore('fig').get(id)
|
||||
])
|
||||
await transaction.done
|
||||
return metadata && figBytes
|
||||
? ({ ...metadata, figBytes: Uint8Array.from(figBytes) } satisfies RecoverySnapshot)
|
||||
return metadata && stored
|
||||
? ({ ...metadata, figBytes: await figBytesOf(stored) } satisfies RecoverySnapshot)
|
||||
: null
|
||||
},
|
||||
|
||||
|
|
@ -65,7 +85,7 @@ export function createIdbRecoveryStore(): RecoveryStore {
|
|||
}
|
||||
await Promise.all([
|
||||
transaction.objectStore('meta').put(metadata),
|
||||
transaction.objectStore('fig').put(Uint8Array.from(input.figBytes), input.id),
|
||||
transaction.objectStore('fig').put(figBlob(input.figBytes), input.id),
|
||||
transaction.done
|
||||
])
|
||||
return metadata
|
||||
|
|
|
|||
|
|
@ -46,6 +46,19 @@ describe('document recovery store', () => {
|
|||
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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue