0xMiden / 0xMiden/web-sdk

`Failed to initialize IdxdbStore`: main thread and worker race each other during the client-version store reset

Open
#287 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
1
Forks
21
Avg merge
12h 14m
Merged PRs (30d)
41

Description

### Packages versions

- `@miden-sdk/miden-sdk`: 0.15.9
- `@miden-sdk/react`: 0.15.9
- Not browser-specific (reported on Chrome and Safari)

### Bug description

Creating a `WebClient` with the default `useWorker: true` initializes WASM twice — once on the main thread and once in the web worker — and both instances call `openDatabase(storeName, clientVersion)` concurrently against the **same** database. With `@miden-sdk/react`'s `MidenProvider` no `storeName` is passed, so both open `MidenClientDB`.

That is harmless until `MidenDatabase.ensureClientVersion` (idxdb-store, `schema.ts`) takes its reset path, which happens when the stored `clientVersion` differs from the shipped one in major or minor:

```js
this.dexie.close();
await this.dexie.delete();
await this.dexie.open();
await this.persistClientVersion(clientVersion);
```

`dexie.delete()` fires `versionchange` on every *other* open connection, and Dexie's default handler closes it ("Another connection wants to delete database … Closing db now"). With both instances inside `ensureClientVersion` at the same time, whichever reaches `delete()` second closes the connection the first one just reopened, and any `settings` read/write in flight at that moment rejects with `DatabaseClosedError`. `logWebStoreError` re-throws it and the Rust wrapper reports `Failed to initialize IdxdbStore`.

Two consequences:

- `MidenProvider` has no retry — it stores the error in `initError` and renders `errorComponent` for good, so the app stays dead until the user reloads.
- Both instances run `delete()`, so one can drop the database the other just recreated and populated.

Only users with a store written by an older client are affected, which is why this surfaces in production but never on a fresh profile.

**Possible fix.** The exclusive section only needs to run once per database. Holding a Web Lock (`navigator.locks.request`) around `MidenDatabase.open()` / `ensureClientVersion` would make the second caller wait and then observe the already-updated `clientVersion`, skipping the reset entirely. The SDK already depends on Web Locks in `syncLock.js`, and since they serialize across tabs too, this would also cover the multi-tab variant of the same race.

### How can this be reproduced?

Precondition: an existing store whose `clientVersion` differs in major/minor from the SDK build being loaded.

**How users hit it?** Open an app on 0.14.x so the store is created, upgrade to 0.15.x, reload.

On reload both instances enter the reset path together. It is a race, so it does not fail on every attempt — the window is between one instance's `dexie.delete()` and the other's `settings` access. Passing `useWorker: false` (a single WASM instance, so a single connection) never reproduces it.

### Relevant log output

```shell
Failed to initialize: Failed to initialize IdxdbStore
```

Contributor guide

Open the contributing guide

Research direction

The issue is in the idxdb-store schema.ts file, specifically the ensureClientVersion method. Look at how WebClient initializes with useWorker: true, causing concurrent database opens. Examine the syncLock.js for Web Lock usage. The fix involves adding a lock around MidenDatabase.open() to serialize access. Test by simulating a version mismatch and reloading with multiple instances.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript, wasm
Domain
backend, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.