Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions db/sqlite_open_wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ func openSQLiteDatabase(cfg SQLiteOpenConfig) (*SQLiteOpenResult, error) {
values.Set("vfs", wasmSQLiteVFS)
values.Set("mode", "rwc")

// A wallet-grade database silently degrading to the in-memory VFS
// would lose every write on page close, so fail closed when no
// persistent OPFS VFS can be opened. The most common trigger is
// another tab of the same origin holding the exclusive OPFS handles;
// failing here surfaces that as a clear locked-database error instead
// of a later migration failure against a throwaway database.
values.Set("require_persistent", "true")

pragmas := make([]string, 0, len(cfg.Pragmas)+1)
for _, pragma := range cfg.Pragmas {
switch strings.ToLower(pragma.Name) {
Expand Down Expand Up @@ -87,7 +95,7 @@ func openWASMSQLiteWithRetry(dsn string) (*sql.DB, error) {
}

_ = db.Close()
if !isWASMCantOpen(err) {
if !isWASMRetryableOpen(err) {
return nil, err
}

Expand All @@ -98,10 +106,16 @@ func openWASMSQLiteWithRetry(dsn string) (*sql.DB, error) {
return nil, lastErr
}

// isWASMCantOpen identifies the SQLite error returned while OPFS still holds a
// file lock from a just-unloaded page runtime.
func isWASMCantOpen(err error) bool {
// isWASMRetryableOpen identifies the SQLite errors returned while OPFS still
// holds a file lock from a just-unloaded page runtime: SQLITE_CANTOPEN while
// the previous runtime's handles are still being torn down, and SQLITE_BUSY
// now that require_persistent surfaces lock contention as an open failure
// instead of an in-memory fallback. A tab whose lock holder never goes away
// exhausts the retries and returns the locked-database error to the caller.
func isWASMRetryableOpen(err error) bool {
return strings.Contains(err.Error(), "SQLITE_CANTOPEN") ||
strings.Contains(err.Error(), "SQLITE_BUSY") ||
strings.Contains(err.Error(), "database is locked") ||
strings.Contains(err.Error(), "unable to open database file")
}

Expand Down
19 changes: 15 additions & 4 deletions lwwallet/walletdb_wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func openWASMWalletDB(dbDir string) (walletdb.DB, error) {
if err == nil {
return db, nil
}
if !isWASMWalletCantOpen(err) {
if !isWASMWalletRetryableOpen(err) {
return nil, fmt.Errorf("open OPFS wallet database: %w",
err)
}
Expand All @@ -111,6 +111,12 @@ func wasmWalletDBDSN(dbDir string) string {
values.Set("file", wasmWalletDBFileName(dbDir))
values.Set("vfs", "opfs")
values.Set("mode", "rwc")

// The wallet database must never silently degrade to the in-memory
// VFS (every write would be lost on page close), so fail closed when
// no persistent OPFS VFS can be opened, e.g. while another tab of the
// same origin holds the exclusive OPFS handles.
values.Set("require_persistent", "true")
values.Set("busy_timeout", wasmWalletDBBusyTimeoutMS)
values.Set("journal_mode", "WAL")
values.Set(
Expand Down Expand Up @@ -142,9 +148,14 @@ func wasmWalletDBFileName(dbDir string) string {
return fmt.Sprintf(wasmWalletDBFileNamePattern, hasher.Sum64())
}

// isWASMWalletCantOpen identifies the SQLite error returned while OPFS still
// holds the wallet database from a just-unloaded page runtime.
func isWASMWalletCantOpen(err error) bool {
// isWASMWalletRetryableOpen identifies the SQLite errors returned while OPFS
// still holds the wallet database from a just-unloaded page runtime:
// SQLITE_CANTOPEN while the previous runtime's handles are still being torn
// down, and SQLITE_BUSY now that require_persistent surfaces lock contention
// as an open failure instead of an in-memory fallback.
func isWASMWalletRetryableOpen(err error) bool {
return strings.Contains(err.Error(), "SQLITE_CANTOPEN") ||
strings.Contains(err.Error(), "SQLITE_BUSY") ||
strings.Contains(err.Error(), "database is locked") ||
strings.Contains(err.Error(), "unable to open database file")
}