Skip to content

Dexie RxStorage: reference counting in dexie-helper never closes Dexie connections — reopening a database name after deletion fails (DatabaseClosedError + DB8) #8793

Closed

Description

@Cr4ftingMine

Affected versions

17.3.0 and 17.4.0 (current), Dexie-based RxStorage. See the analysis below for why
this is version-independent.

Summary

While debugging a DatabaseClosedError after a close → delete → reopen cycle in our
app, I ended up in src/plugins/storage-dexie/dexie-helper.ts and - unless I'm
misreading the code - the reference counting around DEXIE_STATE_DB_BY_NAME /
REF_COUNT_PER_DEXIE_DB seems to be broken in two independent ways:

  1. The counter is never incremented anywhere. The only writes to
    REF_COUNT_PER_DEXIE_DB in the entire package are set(x, 0) at creation and
    set(x, count - 1) in closeDexieDb(). Starting from 0, the counter can only
    ever be 0, -1, -2, ... so the condition newCount === 0 (which guards
    state.dexieDb.close() and the cache eviction) can never become true. The close
    branch is unreachable.

  2. The initial counter is stored under the wrong key. Inside the
    getFromMapOrCreate factory callback, REF_COUNT_PER_DEXIE_DB.set(state, 0)
    runs before const state = ... has been assigned (the callback executes
    synchronously during that assignment), so the key is effectively undefined.
    closeDexieDb() later looks up the actual state promise, gets undefined, and
    computes undefined - 1 = NaN. The (prevCount as any) - 1 cast in the source
    silences the TypeScript error that would have flagged this. This part appears to
    have changed with the "REFACTOR Map cache code" commit (REFACTOR Map cache code #4604), which moved those
    lines into the factory callback.

Consequences

  • RxStorageInstanceDexie.close() never closes the underlying Dexie/IndexedDB
    connections (connection leak), and the DEXIE_STATE_DB_BY_NAME entry is never
    removed.
  • If the application afterwards deletes the IndexedDB databases (e.g.
    indexedDB.deleteDatabase(...) on logout, to remove user data from a shared
    device), the delete request is blocked by the leaked connections; Dexie
    force-closes them ("Another connection wants to delete database ..."). The module
    cache now holds force-closed Dexie instances.
  • Re-creating an RxDatabase with the same name in the same JS context then receives
    the closed Dexie instance from the cache: the first storage operation throws
    Dexie's DatabaseClosedError ("Database has been closed"), and a retry fails with
    RxDB error DB8 because the half-created database is still registered.

Reproduction (browser, dexie storage, no plugins required)

import { createRxDatabase, removeRxDatabase } from 'rxdb';
import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';

const schema = {
    version: 0, primaryKey: 'id', type: 'object',
    properties: { id: { type: 'string', maxLength: 100 } }, required: ['id']
};
const open = async (name) => {
    const db = await createRxDatabase({ name, storage: getRxStorageDexie(), multiInstance: true });
    await db.addCollections({ docs: { schema } });
    return db;
};

const name = 'repro' + Date.now();
let db = await open(name);
await db.docs.insert({ id: 'a' });
await db.close();                                   // leaks the Dexie connections
await removeRxDatabase(name, getRxStorageDexie());
for (const d of await indexedDB.databases()) {      // app-level cleanup of the per-collection DBs
    if (d.name?.includes(name)) {
        await new Promise(res => {
            const req = indexedDB.deleteDatabase(d.name);
            req.onsuccess = req.onerror = () => res();
            // logs "Another connection wants to delete database ..." for every sub-DB,
            // which shows that close() did not release the connections
        });
    }
}
db = await open(name);                              // throws DatabaseClosedError

Runtime trace of the counter

To verify the analysis at runtime, I instrumented Map.prototype.set/delete in
the reproduction and logged every access whose call stack originates in the dexie
helper (function names containing DexieDb).

Trace with 17.3.0 as published (reproduction fails with DatabaseClosedError)
set(key="rxdb-dexie-reproâ€Ķ--0--_rxdb_internal", value=undefined)  in getDexieDbWithTables
set(key=undefined, value=0)                                       in getDexieDbWithTables
set(key="rxdb-dexie-reproâ€Ķ--0--_rxdb_internal", value=<Promise>)  in getDexieDbWithTables
set(key="rxdb-dexie-reproâ€Ķ--0--docs", value=undefined)            in getDexieDbWithTables
set(key=undefined, value=0)                                       in getDexieDbWithTables
set(key="rxdb-dexie-reproâ€Ķ--0--docs", value=<Promise>)            in getDexieDbWithTables
set(key=<Promise>, value=NaN)                                     in closeDexieDb
set(key=<Promise>, value=NaN)                                     in closeDexieDb
set(key=<Promise>, value=NaN)                                     in closeDexieDb
set(key=<Promise>, value=NaN)                                     in closeDexieDb
set(key=<Promise>, value=NaN)                                     in closeDexieDb

The initial count is stored under the key undefined (defect 2). Every
closeDexieDb() call writes NaN (undefined - 1), meaning the else-branch ran
and dexieDb.close() was never reached. No increment and no delete appear
anywhere in the trace: connections and cache entries are never released.

Same trace with the suggested fix applied (reproduction passes)
set(key=<Promise>, value=0)                                       in getDexieDbWithTables
set(key="rxdb-dexie-reproâ€Ķ--0--_rxdb_internal", value=<Promise>)  in getDexieDbWithTables
set(key=<Promise>, value=1)                                       in getDexieDbWithTables
set(key=<Promise>, value=0)                                       in getDexieDbWithTables
set(key="rxdb-dexie-reproâ€Ķ--0--docs", value=<Promise>)            in getDexieDbWithTables
set(key=<Promise>, value=1)                                       in getDexieDbWithTables
delete(key=<Promise>)                                             in closeDexieDb
delete(key="rxdb-dexie-reproâ€Ķ--0--docs")                          in closeDexieDb
delete(key=<Promise>)                                             in closeDexieDb
delete(key="rxdb-dexie-reproâ€Ķ--0--_rxdb_internal")                in closeDexieDb
â€Ķ (same balanced cycle repeats for removeRxDatabase and for the reopen)

Counts go 0 → 1 per storage instance, and on close both the counter and the
name-cache entry are removed. The "Another connection wants to delete" warnings
disappear as well, since the connections are now released before the delete.

Bug checklist

  • Different browsers: reproduced in both Firefox and Chromium.
  • Different storages: not applicable - the defect is located in
    dexie-helper.ts, which only the Dexie storage executes.
  • eventReduce: false: still fails with the same error.
  • Key-compression: not used (default off).
  • multiInstance: false: still fails with the same error.

Suggested fix

  • Key the initial count on value (not the still-unassigned state).
  • Increment the count on every getDexieDbWithTables() call. It is called exactly
    once per storage instance (createDexieStorageInstance), and closeDexieDb runs
    exactly once per instance close, so the counting stays balanced.
  • When the last reference is released, also delete the DEXIE_STATE_DB_BY_NAME
    entry — otherwise the next open of the same name still receives the closed
    instance.

I have this change running as a patch-package patch in our app, and with it the
scenario above works. I can follow up with a PR containing the fix and a regression
test based on test/unit/bug-report.test.ts.

Activity

  1. pubkey commented on Jul 15, 2026

    @pubkey
    Owner

    Can you make a PR with a test case that reproduces your problem?
    You can start with this template.

  2. Cr4ftingMine commented on Jul 15, 2026

    @Cr4ftingMine
    Author

    Done: #8797 - the first commit contains the failing test, the second one a suggested fix.

  3. stale commented on Jul 22, 2026

    @stale

    This issue has been automatically marked as stale because it has not had recent activity. It will be closed soon. Please update it or it may be closed to keep our repository organized. The best way is to add some more information or make a pull request with a test case. Also you might get help in fixing it at the RxDB Community Chat If you know you will continue working on this, just write any message to the issue (like "ping") to remove the stale tag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions