I make Pianory, a browser piano learning and practice app. AI helped draft this article; I checked its claims against the current source and focused tests.
Our local recording store has to persist browser audio safely. The code documents a WebKit IndexedDB problem with Blob values in ephemeral sessions, including Safari private browsing and automated WebKit. New audio rows therefore store an ArrayBuffer of bytes, while an optional Blob field remains readable for older rows. This is a documented implementation choice, not a claim that a unit test reproduces the browser bug.
A subtle detail is when conversion happens. Saving awaits blob.arrayBuffer() before opening the read-write transaction. Awaiting asynchronous conversion inside a transaction risks letting IndexedDB auto-commit before the metadata and audio writes finish. Once bytes are ready, the store adds the metadata and audio row in one transaction.
At read time, it accepts the legacy Blob or reconstructs a Blob from bytes and the stored MIME type. It checks the recording ID, namespace, byte size and MIME type against metadata before returning it. That boundary keeps the application-facing object useful while the persisted form stays explicit.
The focused test checks save/read/export size and MIME type, namespace isolation, and that a failed audio write leaves no metadata row. I ran pnpm exec vitest run src/lib/recordings/store.spec.ts: seven tests passed. They use a mocked IndexedDB, so they do not independently establish Safari or WebKit behavior.
The practical lesson for media persistence is to decide the stored representation and transaction timing deliberately, then reconstruct and validate at the read boundary. Source: src/lib/recordings/store.ts and src/lib/recordings/store.spec.ts in the Pianory repository.
Top comments (2)
Awaiting the conversion before opening the transaction is the key move here. In IndexedDB, any asynchronous microtask gap inside an active transaction lets the event loop tick without pending requests, causing the engine to auto-commit immediately and throw a TransactionInactiveError on the write. Pre-resolving raw bytes before touching db.transaction() keeps the write synchronous and atomic.
The Safari WebKit quirk with Blobs in IndexedDB comes down to how WebKit handles file backing. Instead of storing Blob payloads inline in SQLite, it offloads them to ephemeral disk file handles. In Safari Private Browsing or automated test runners, those file handles frequently lose read permissions or fail to resolve on subsequent reads. Storing plain ArrayBuffers keeps the payload entirely within IndexedDB's structured clone buffer.
The only cleanup detail worth watching on the read path is object URL disposal. If you reconstruct a Blob and bind it to an audio element via URL.createObjectURL(), calling URL.revokeObjectURL() once the audio element loads or unmounts prevents the browser process from holding those decoded bytes in its internal URL registry.
Thanks for the careful read. We resolve blob.arrayBuffer() before db.transaction(), since yielding while a transaction has no pending requests can let it auto-commit; a microtask alone isn't necessarily the boundary. Our current RecordingPanel revokes object URLs when a draft or preview is cleared or hidden and on teardown, while keeping the URL available to the audio control during playback. The WebKit behavior is the documented concern, while the seven mocked IndexedDB tests do not reproduce Safari.