Skip to content

feat(devtool): add the RxDB database viewer plugin - #8940

Open
pubkey wants to merge 5 commits into
masterfrom
claude/db-viewer-design-rxdb-oq9mbb
Open

feat(devtool): add the RxDB database viewer plugin#8940
pubkey wants to merge 5 commits into
masterfrom
claude/db-viewer-design-rxdb-oq9mbb

Conversation

@pubkey

@pubkey pubkey commented Aug 10, 2026

Copy link
Copy Markdown
Owner

This PR contains:

  • A NEW FEATURE (the devtool plugin)
  • IMPROVED DOCS
  • IMPROVED TESTS
  • IMPROVED typings

Describe the problem you have without this PR

There is no way to look at the data of a running RxDatabase. Debugging means console.loging query results, and there is nothing that shows which index a query used, what replication actually sent, or what a write changed.

This adds rxdb/plugins/devtool, a database viewer and editor implemented from the design handoff for the RxDB devtool. It is framework free, built with plain DOM, and the stylesheet plus every glyph ship inside the plugin, so there are no external assets and no font files.

import { mountRxDBDevtool } from 'rxdb/plugins/devtool';
const devtool = mountRxDBDevtool(db);

Chrome and navigation

  • 44px top bar with database identity, ⌘K command palette, Refresh and help. The RxDB wordmark is dropped on the TanStack surface, and the embedded surface gains drag, dock and fullscreen controls.
  • 200px left rail with collections and their counts, replication status glyphs ( running, idle, error, stopped) and the tools.
  • Content toolbar and Mango query bar scoped to the current collection, with a favourites and history dropdown.
  • Connecting, failed and connected screens for a remote peer, plus the read-only banner for a static dump. The failed screen names the stage that failed and offers the export path instead of a retry button.

Panels

  • Table grid with multi-select, inline cell editing, sorting and paging at 100 rows per page.
  • JSON view with syntax highlighting and the green "updated Ns ago" highlight while observing.
  • Document drawer that stages edits and previews the exact upsert() in a WILL RUN block, with the changed lines highlighted, before Apply runs anything.
  • Blast-radius confirmation for bulk deletes that states the match count, that deletes replicate to peers, and that tombstones remain until cleanup. The delete button stays disabled until the collection name is typed.
  • Live activity map drawing app → collections → remote with 60s rolling counters, sparklines, particle lanes, burst bands above ~200 events/s, node pulses rate-limited to 4 Hz, and the instances and live-queries sub panels.
  • Schema panel sampling stored documents and reporting their real types and presence against the declared schema, with a violations list.
  • Query lab explaining the used index, its bounds, examined and discarded counts, and findings for uncovered fields, $regex scans and in-memory re-sorts.
  • Replication panel with per direction state, checkpoint and the live feed of documents that crossed the wire.
  • Changes panel with a unified diff of every write.
  • Storage panel with document and tombstone counts, attachment bytes, and cleanup.
  • Stacked read-only layout below 640px.

How the data is real

Writes come from database.$. Pull and push come from received$ and sent$ on the states in REPLICATION_STATE_BY_COLLECTION. Reads and live query re-emits are derived from the query cache (_execOverDatabaseCount and result identity), because RxDB emits no read events; those two counters therefore update once per second, which the docs state. Tombstones are counted below the RxCollection via storageInstance.count(), since RxQuery filters deleted documents out.

Notes for review

  • mountRxDBDevtool() throws the new error code DVT1 when there is no DOM.
  • RxDatabase.mountDevtool() is added as a pluginMissing stub, matching registerWebMCP().
  • isLeader() is read behind a try/catch, because it throws when the leader-election plugin is not added. The Instances panel reports leadership as unknown in that case rather than claiming a roster RxDB does not publish.
  • The design specifies a virtualised grid. With the page size fixed at 100 rows, all rows of a page are rendered directly; no virtualisation layer was added.
  • The design sheet flags two open conflicts, and both are resolved the way the sheet suggests: the Live map keeps violet ↑ ↓ for replication while the Replication and Changes panels colour each direction separately, and RxDB pink stays in the chrome and selection only, never on an edge, glyph or rate.

Todos

  • Tests
  • Documentation
  • Typings
  • Changelog

Verification

  • npm run lint clean.
  • npx tsc --noEmit clean for src/.
  • Full fast memory suite: 1423 passing, 0 failing.
  • New test/unit/devtool.test.ts: 16 passing, covering the rolling metrics window, selector parsing with caret position, path get/set, cell input parsing, the document diff, the design tokens, and the no-DOM error.
  • Rendered every screen against a real memory-storage database under jsdom and drove the write paths end to end: query bar filtering, a broken selector keeping previous results visible, a drawer edit reaching the database through Apply, the Changes diff of that write, the bulk-delete gate actually deleting, the resulting tombstone count and cleanup button, and observe mode picking up a live insert.

Generated by Claude Code

Implements the database viewer devtool from the design handoff as a
framework free plugin that renders against a running RxDatabase.

Chrome and navigation:
- 44px top bar with database identity, command palette, refresh and help
- 200px left rail with collections, replication status glyphs and tools
- content toolbar and Mango query bar scoped to the current collection
- connecting, failed and connected screens for a remote peer, plus the
  read-only banner for a static dump

Panels:
- table grid with selection, inline cell editing, sorting and paging
- JSON view with syntax highlighting and the observe highlight
- document drawer that stages edits and previews the exact upsert in a
  WILL RUN block before Apply
- blast-radius confirmation that requires typing the collection name
- Live activity map with 60s counters, sparklines, particle lanes,
  burst bands, instances and live-queries sub panels
- Schema panel sampling stored documents against the declared schema
- Query lab explaining the used index, examined and discarded counts
- Replication panel with per direction state, checkpoint and live feed
- Changes panel with a unified diff of every write
- Storage panel with tombstone counts and cleanup
- stacked read-only layout below 640px

The stylesheet and all glyphs ship inside the plugin, there are no
external assets and no font files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018adcs7t6gXavJ9D7hB14KX
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

✅ Verify Test Reproduction: Tests FAILED without the fix (expected)

This confirms the changed tests correctly reproduce the bug that the source changes fix.

This workflow runs the changed tests without the source fix to verify they reproduce the bug.

Show output
...(truncated, showing last 200 of 244 lines)
[1] npm run build:esm exited with code 0
[2] Successfully compiled 88 files with Babel (4009ms).
[2] npm run build:test exited with code 0
[0] Successfully compiled 274 files with Babel (4816ms).
[0] npm run build:cjs exited with code 0
# transpiling DONE (4 CPUs)

 Exception during run: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/runner/work/rxdb/rxdb/plugins/devtool/index.mjs' imported from /home/runner/work/rxdb/rxdb/test_tmp/unit/devtool.test.js
    at finalizeResolution (node:internal/modules/esm/resolve:271:11)
    at moduleResolve (node:internal/modules/esm/resolve:865:10)
    at defaultResolve (node:internal/modules/esm/resolve:992:11)
    at #cachedDefaultResolve (node:internal/modules/esm/loader:701:20)
    at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:721:38)
    at ModuleLoader.resolveSync (node:internal/modules/esm/loader:759:56)
    at #resolve (node:internal/modules/esm/loader:683:17)
    at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:603:35)
    at ModuleJob.syncLink (node:internal/modules/esm/module_job:163:33)
    at ModuleJob.link (node:internal/modules/esm/module_job:253:17)
    at new ModuleJob (node:internal/modules/esm/module_job:232:26)
    at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:572:11)
    at afterResolve (node:internal/modules/esm/loader:607:52)
    at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:613:12)
    ... collapsed 6 duplicate lines matching above lines ...
    at node:internal/modules/esm/loader:632:32
    at TracingChannel.tracePromise (node:diagnostics_channel:362:14)
    at ModuleLoader.import (node:internal/modules/esm/loader:628:21)
    at defaultImportModuleDynamicallyForScript (node:internal/modules/esm/utils:239:31)
    at importModuleDynamicallyCallback (node:internal/modules/esm/utils:263:12)
    at exports.doImport (/home/runner/work/rxdb/rxdb/node_modules/mocha/lib/nodejs/esm-utils.js:36:43)
    at formattedImport (/home/runner/work/rxdb/rxdb/node_modules/mocha/lib/nodejs/esm-utils.js:10:28)
    at Object.requireModule [as requireOrImport] (/home/runner/work/rxdb/rxdb/node_modules/mocha/lib/nodejs/esm-utils.js:102:36)
    at exports.loadFilesAsync (/home/runner/work/rxdb/rxdb/node_modules/mocha/lib/nodejs/esm-utils.js:155:34)
    at Mocha.loadFilesAsync (/home/runner/work/rxdb/rxdb/node_modules/mocha/lib/mocha.js:429:19)
    at singleRun (/home/runner/work/rxdb/rxdb/node_modules/mocha/lib/cli/run-helpers.js:174:15)
    at exports.runMocha (/home/runner/work/rxdb/rxdb/node_modules/mocha/lib/cli/run-helpers.js:247:10)
    at exports.handler (/home/runner/work/rxdb/rxdb/node_modules/mocha/lib/cli/run.js:384:11)
    at /home/runner/work/rxdb/rxdb/node_modules/mocha/node_modules/yargs/build/index.cjs:1:8992
    at /home/runner/work/rxdb/rxdb/node_modules/mocha/node_modules/yargs/build/index.cjs:1:4972 {
  code: 'ERR_MODULE_NOT_FOUND',
  url: 'file:///home/runner/work/rxdb/rxdb/plugins/devtool/index.mjs'
}

=== test:browser:dexie output ===

> rxdb@17.4.0 test:browser:dexie
> npm run transpile && cross-env CI=true DEFAULT_STORAGE=dexie   karma start ./config/karma.conf.cjs --single-run --browsers ChromeHeadless


> rxdb@17.4.0 transpile
> npm run build:version && node scripts/transpile.mjs && cp ./scripts/module_package.json test_tmp/package.json


> rxdb@17.4.0 build:version
> node ./scripts/update-version-variable.mjs

# transpiling.. (this takes some time on first run)
Successfully compiled 1 file with Babel (175ms).
Successfully compiled 1 file with Babel (192ms).
# transpiling DONE (4 CPUs)
staticFilesPath: /home/runner/work/rxdb/rxdb/docs-src/static/files
# Use CI settings.
# Karma effective config: {
  "basePath": "",
  "frameworks": [
    "mocha",
    "webpack",
    "detectBrowsers"
  ],
  "webpack": "[webpack config omitted]",
  "preprocessors": {
    "../test_tmp/unit.test.js": [
      "webpack",
      "sourcemap"
    ]
  },
  "files": [
    "../test_tmp/unit.test.js"
  ],
  "port": 9876,
  "colors": true,
  "autoWatch": false,
  "proxies": {
    "/files": "http://localhost:18001/files"
  },
  "detectBrowsers": {
    "enabled": true,
    "usePhantomJS": false,
    "postDetection": "[function]"
  },
  "plugins": [
    "karma-mocha",
    "karma-webpack",
    "karma-chrome-launcher",
    "karma-safari-launcher",
    "karma-firefox-launcher",
    "karma-opera-launcher",
    "karma-detect-browsers",
    "karma-spec-reporter",
    "karma-sourcemap-loader"
  ],
  "client": {
    "mocha": {
      "bail": true,
      "timeout": 120000
    },
    "env": "[process.env omitted]"
  },
  "browserDisconnectTimeout": 300000,
  "browserDisconnectTolerance": 4,
  "browserNoActivityTimeout": 300000,
  "captureTimeout": 300000,
  "processKillTimeout": 120000,
  "singleRun": true,
  "reporters": [
    "spec"
  ],
  "concurrency": 1,
  "retryLimit": 3,
  "browserConsoleLogOptions": {
    "level": "debug",
    "format": "%b %T: %m",
    "terminal": true
  }
}
Server listening on port: 18001
�[32m10 08 2026 15:27:32.775:INFO [framework.detect-browsers]: �[39mwhich.sync(process.env[browser.ENV_CMD]):  /usr/bin/google-chrome
�[32m10 08 2026 15:27:32.777:INFO [framework.detect-browsers]: �[39mThe following browsers will be used: [ 'ChromeHeadless' ]
Webpack bundling...
asset �[1m�[32mcommons.js�[39m�[22m 10.4 MiB �[1m�[32m[emitted]�[39m�[22m (name: commons) (id hint: commons)
asset �[1m�[32mruntime.js�[39m�[22m 9.7 KiB �[1m�[32m[emitted]�[39m�[22m (name: runtime)
asset �[1m�[32munit.test.3184013999.js�[39m�[22m 1.12 KiB �[1m�[32m[emitted]�[39m�[22m (name: unit.test.3184013999)
Entrypoint �[1munit.test.3184013999�[39m�[22m 10.4 MiB = �[1m�[32mruntime.js�[39m�[22m 9.7 KiB �[1m�[32mcommons.js�[39m�[22m 10.4 MiB �[1m�[32munit.test.3184013999.js�[39m�[22m 1.12 KiB

�[1m�[31mERROR�[39m�[22m in �[1m./test_tmp/unit/devtool.test.js�[39m�[22m �[1m�[32m4:0-258�[39m�[22m
�[1mModule �[1m�[31mnot found�[39m�[22m�[1m: �[1m�[31mError�[39m�[22m�[1m: Can't resolve '../../plugins/devtool/index.mjs' in '/home/runner/work/rxdb/rxdb/test_tmp/unit'�[39m�[22m
resolve '../../plugins/devtool/index.mjs' in '/home/runner/work/rxdb/rxdb/test_tmp/unit'
  using description file: /home/runner/work/rxdb/rxdb/test_tmp/package.json (relative path: ./unit)
    Field 'browser' doesn't contain a valid alias configuration
    using description file: /home/runner/work/rxdb/rxdb/package.json (relative path: ./plugins/devtool/index.mjs)
      Field 'browser' doesn't contain a valid alias configuration
�[1m�[31m      /home/runner/work/rxdb/rxdb/plugins/devtool/index.mjs doesn't exist�[39m�[22m
 @ ./test_tmp/unit.test.js 64:0-32

webpack 5.107.2 compiled with �[1m�[31m1 error�[39m�[22m in 3651 ms
�[32m10 08 2026 15:27:37.369:INFO [karma-server]: �[39mKarma v6.4.4 server started at http://localhost:9876/
�[32m10 08 2026 15:27:37.370:INFO [launcher]: �[39mLaunching browsers ChromeHeadless with concurrency 1
�[32m10 08 2026 15:27:37.373:INFO [launcher]: �[39mStarting browser ChromeHeadless
�[32m10 08 2026 15:27:43.823:INFO [Chrome Headless 150.0.0.0 (Linux 0.0.0)]: �[39mConnected on socket Ia7z4n2KoKoQ8Z4TAAAB with id 76679812
Chrome Headless 150.0.0.0 (Linux 0.0.0) LOG LOG: �[36m'DEFAULT_STORAGE: dexie'�[39m
Chrome Headless 150.0.0.0 (Linux 0.0.0) LOG LOG: �[36m'# use RxStorage: dexie'�[39m
Chrome Headless 150.0.0.0 (Linux 0.0.0) LOG LOG: �[36m'######## init.test.js ########'�[39m
Chrome Headless 150.0.0.0 (Linux 0.0.0) WARN LOG: �[36m'-------------- RxDB dev-mode warning -------------------------------
you are seeing this because you use the RxDB dev-mode plugin https://rxdb.info/dev-mode.html?console=dev-mode 
This is great in development mode, because it will run many checks to ensure
that you use RxDB correct. If you see this in production mode,
you did something wrong because the dev-mode plugin will decrease the performance.

🤗 Hint: To get the most out of RxDB, check out the Premium Plugins
to get access to faster storages and more professional features: https://rxdb.info/premium/?console=dev-mode 

💬 Need help? The RxDB Discord is the fastest place to reach maintainers: https://rxdb.info/chat/?console=dev-mode 

You can disable this warning by calling disableWarnings() from the dev-mode plugin.
---------------------------------------------------------------------'�[39m
Chrome Headless 150.0.0.0 (Linux 0.0.0) LOG LOG: �[36m'###### PLATFORM: ######'�[39m
Chrome Headless 150.0.0.0 (Linux 0.0.0) LOG LOG: �[36m'USER-AGENT: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36'�[39m
Chrome Headless 150.0.0.0 (Linux 0.0.0) LOG LOG: �[36m'STORAGE: dexie'�[39m
�[31mChrome Headless 150.0.0.0 (Linux 0.0.0) ERROR�[39m
  Uncaught Error: Cannot find module '../../plugins/devtool/index.mjs'
  at webpack://rxdb/./test_tmp/unit/devtool.test.js?:6:146
  
  Error: Cannot find module '../../plugins/devtool/index.mjs'
      at webpackMissingModule (webpack://rxdb/./test_tmp/unit/devtool.test.js?:6:50)
      at eval (webpack://rxdb/./test_tmp/unit/devtool.test.js?:6:156)
      at Module../test_tmp/unit/devtool.test.js (/tmp/_karma_webpack_110758/commons.js:23102:1)
      at __webpack_require__ (/tmp/_karma_webpack_110758/runtime.js:37:42)
      at eval (webpack://rxdb/./test_tmp/unit.test.js?:52:80)
      at Module../test_tmp/unit.test.js (/tmp/_karma_webpack_110758/commons.js:22937:1)
      at __webpack_require__ (/tmp/_karma_webpack_110758/runtime.js:37:42)
      at __webpack_exec__ (unit.test.3184013999.js:24:48)
      at unit.test.3184013999.js:25:55
      at __webpack_require__.O (/tmp/_karma_webpack_110758/runtime.js:74:23)

Chrome Headless 150.0.0.0 (Linux 0.0.0): Executed 0 of 0�[31m ERROR�[39m (0.901 secs / 0 secs)

Chrome Headless 150.0.0.0 (Linux 0.0.0) ERROR LOG: �[36m'init.test.ts: browser uncaught error:'�[39m
Chrome Headless 150.0.0.0 (Linux 0.0.0) ERROR LOG: �[36m'Uncaught Error: Cannot find module '../../plugins/devtool/index.mjs''�[39m
Chrome Headless 150.0.0.0 (Linux 0.0.0) ERROR LOG: �[36m'Error: Cannot find module '../../plugins/devtool/index.mjs'
    at webpackMissingModule (webpack://rxdb/./test_tmp/unit/devtool.test.js?:6:50)
    at eval (webpack://rxdb/./test_tmp/unit/devtool.test.js?:6:156)
    at Module../test_tmp/unit/devtool.test.js (http://localhost:9876/absolute/tmp/_karma_webpack_110758/commons.js?ffae6013de6d32ee738a9e542c681de113a2968d:23102:1)
    at __webpack_require__ (http://localhost:9876/absolute/tmp/_karma_webpack_110758/runtime.js?bbee3e1134bcbcd028f882ecfdf4c5d01b426378:37:42)
    at eval (webpack://rxdb/./test_tmp/unit.test.js?:52:80)
    at Module../test_tmp/unit.test.js (http://localhost:9876/absolute/tmp/_karma_webpack_110758/commons.js?ffae6013de6d32ee738a9e542c681de113a2968d:22937:1)
    at __webpack_require__ (http://localhost:9876/absolute/tmp/_karma_webpack_110758/runtime.js?bbee3e1134bcbcd028f882ecfdf4c5d01b426378:37:42)
    at __webpack_exec__ (http://localhost:9876/absoluteunit.test.3184013999.js?db447ea27b895b1663771968a3603d9fa35ab3b2:24:48)
    at http://localhost:9876/absoluteunit.test.3184013999.js?db447ea27b895b1663771968a3603d9fa35ab3b2:25:55
    at __webpack_require__.O (http://localhost:9876/absolute/tmp/_karma_webpack_110758/runtime.js?bbee3e1134bcbcd028f882ecfdf4c5d01b426378:74:23)'�[39m
Chrome Headless 150.0.0.0 (Linux 0.0.0) ERROR LOG: �[36m'at webpack://rxdb/./test_tmp/unit/devtool.test.js?:6:146'�[39m


View full workflow run

pubkey and others added 4 commits August 10, 2026 12:55
The comment on findBareWord() and the broken-selector testcase both
spelled out a misspelled boolean literally, which codespell reports.
The testcase now uses another bare word that is equally invalid JSON.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018adcs7t6gXavJ9D7hB14KX
Three fixes found by driving the devtool in a browser against a
seeded database.

A filled RxJsonSchema sorts its properties alphabetically, so the grid
was picking its columns in alphabetical order. On a todos collection
that dropped `title` from the grid entirely and gave the wide column to
`dueDate`. Column selection moved into pickGridColumns() and now gives
the wide column to a string without a maxLength, which is free text,
and prefers the required fields for the narrow columns. Added testcases.

Schema and Query lab read the collection from the navigation, which is
a tool while they are open, so they always analysed the alphabetically
first collection instead of the one that was on screen. The store now
remembers the last opened collection and both panels use it.

The app node of the Live map drew the full location pathname, which
pushed the node out of its column on a long url. Only the tail is drawn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018adcs7t6gXavJ9D7hB14KX
`check-types` compiles the test folder with strict null checks, where
Array.find() is possibly undefined. The tauri example fails the same
way because its tsc reaches the root test folder too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018adcs7t6gXavJ9D7hB14KX
Adds a db-viewer component that mounts the devtool over the running
example app and tears it down again on close. Building the example
surfaced that `rxdb/plugins/devtool` imported its own option and handle
types without re-exporting them, so consumers could not type against
the plugin entry point. They are re-exported now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018adcs7t6gXavJ9D7hB14KX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant