Handle InvalidStateError when reading workspace layout from OPFS #159

Closed
opened 2026-05-30 22:29:19 +00:00 by steve · 0 comments
Owner

Problem

Browser OPFS / File System Access vault reads can throw InvalidStateError with
the message "An operation that depends on state cached in an interface object was
made but the state had changed since it was read from disk." Startup fails at
Loading layout when reading /.obsidian/workspace.json, and SQLite WASM
app-database startup can fail with the same stale OPFS handle error before
falling back to IndexedDB. The demo workspace seed path can hit the same stale-
handle behavior while writing fixture files.

Goal

Retry stale browser file-handle errors transparently, recover SQLite WASM OPFS
startup after stale handles, and keep demo workspace seeding progress visible
during fixture writes.

Scope

  • BrowserHandleVaultAdapter retry policy for InvalidStateError
  • SqliteWasmAppDatabaseCore retry/reset for stale OPFS SAH pool startup
  • Demo vault chooser progress UI wired to seed current/total
  • Playwright coverage for empty storage → new OPFS vault boot (worker enabled)

Non-goals

  • Closing this issue until validated in the running browser workspace

Acceptance Criteria

  • Opening the demo workspace shows per-file seed progress (Step N of M plus
    current path) whenever fixture seeding runs
  • Layout load survives transient OPFS InvalidStateError during startup
  • Browser storage unit tests cover stale getFile() retry

Implementation Notes

  • Initial fix adds InvalidStateError to recoverable handle errors and wraps
    stat() getFile() in the retry loop
  • Follow-up in working copy: 5 attempts with backoff, cause-chain detection,
    demo progress bar wired to seed counters, setTimeout(0) yield between seed
    progress updates, OPFS root handle refresh on stale errors, and mkdir
    wrapped in the same retry path

Suggested Files or Specs To Inspect

  • packages/api/src/lib/storage/browser.ts
  • packages/workspace/src/lib/feature/app/demo-vault.ts
  • packages/workspace/src/lib/components/app/VaultBootstrap.svelte
  • packages/api/src/lib/workspace.svelte.ts (loadLayout)

Validation Commands

pnpm --filter @lapis-notes/api test src/lib/__tests__/browser-storage.test.ts
pnpm --filter @lapis-notes/api test src/lib/__tests__/app-database.test.ts -t isRecoverableSqliteStartupError
pnpm --filter @lapis-notes/workspace test:e2e e2e/opfs-vault-boot.spec.ts

Follow-up Tasks

Implementation Summary

Retry InvalidStateError/stale OPFS handles in vault adapter and SQLite WASM startup; pass vault profile on new-vault create; add opfs-vault-boot e2e (empty storage → OPFS vault, SQLite owner, re-clear). Validated with unit tests and Playwright opfs-vault-boot.spec.ts.

## Problem Browser OPFS / File System Access vault reads can throw `InvalidStateError` with the message "An operation that depends on state cached in an interface object was made but the state had changed since it was read from disk." Startup fails at **Loading layout** when reading `/.obsidian/workspace.json`, and SQLite WASM app-database startup can fail with the same stale OPFS handle error before falling back to IndexedDB. The demo workspace seed path can hit the same stale- handle behavior while writing fixture files. ## Goal Retry stale browser file-handle errors transparently, recover SQLite WASM OPFS startup after stale handles, and keep demo workspace seeding progress visible during fixture writes. ## Scope - `BrowserHandleVaultAdapter` retry policy for `InvalidStateError` - `SqliteWasmAppDatabaseCore` retry/reset for stale OPFS SAH pool startup - Demo vault chooser progress UI wired to seed `current/total` - Playwright coverage for empty storage → new OPFS vault boot (worker enabled) ## Non-goals - Closing this issue until validated in the running browser workspace ## Acceptance Criteria - Opening the demo workspace shows per-file seed progress (`Step N of M` plus current path) whenever fixture seeding runs - Layout load survives transient OPFS `InvalidStateError` during startup - Browser storage unit tests cover stale `getFile()` retry ## Implementation Notes - Initial fix adds `InvalidStateError` to recoverable handle errors and wraps `stat()` `getFile()` in the retry loop - Follow-up in working copy: 5 attempts with backoff, cause-chain detection, demo progress bar wired to seed counters, `setTimeout(0)` yield between seed progress updates, OPFS root handle refresh on stale errors, and `mkdir` wrapped in the same retry path ## Suggested Files or Specs To Inspect - `packages/api/src/lib/storage/browser.ts` - `packages/workspace/src/lib/feature/app/demo-vault.ts` - `packages/workspace/src/lib/components/app/VaultBootstrap.svelte` - `packages/api/src/lib/workspace.svelte.ts` (`loadLayout`) ## Validation Commands ```bash pnpm --filter @lapis-notes/api test src/lib/__tests__/browser-storage.test.ts pnpm --filter @lapis-notes/api test src/lib/__tests__/app-database.test.ts -t isRecoverableSqliteStartupError pnpm --filter @lapis-notes/workspace test:e2e e2e/opfs-vault-boot.spec.ts ``` ## Related Issues ## Follow-up Tasks ## Implementation Summary Retry InvalidStateError/stale OPFS handles in vault adapter and SQLite WASM startup; pass vault profile on new-vault create; add opfs-vault-boot e2e (empty storage → OPFS vault, SQLite owner, re-clear). Validated with unit tests and Playwright opfs-vault-boot.spec.ts. <!-- backlog:task_id=TASK-OPFS-INVALID-STATE-RETRY source_spec=spec/src/20-packages/api/index.md -->
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lapis-notes/lapis#159
No description provided.