Skip to content

fix(web): run sql.js in-process in VS Code Web (Trusted Types blocks workers) (#418)#420

Open
zknpr wants to merge 3 commits into
mainfrom
fix/418-web-inprocess-engine
Open

fix(web): run sql.js in-process in VS Code Web (Trusted Types blocks workers) (#418)#420
zknpr wants to merge 3 commits into
mainfrom
fix/418-web-inprocess-engine

Conversation

@zknpr
Copy link
Copy Markdown
Owner

@zknpr zknpr commented May 31, 2026

Summary

Real fix for #418 — in VS Code for Web (vscode.dev / github.dev) every SQLite database hangs forever on the loading screen.

This supersedes #419 / v1.3.6, which did NOT fix the bug. I incorrectly closed #418 on the strength of an isolated-worker repro that didn't enforce vscode.dev's CSP. #418 is reopened.

Real root cause (captured from published 1.3.6 in real vscode.dev)

Webview console, seen twice and reproduced directly:

TypeError: Failed to construct 'Worker': This document requires 'TrustedScriptURL' assignment.
This document requires 'TrustedScriptURL' assignment. The action has been blocked.

VS Code Web applies a CSP with require-trusted-types-for 'script' (Trusted Types) and a fixed trusted-types policy allowlist on the extension host. workerFactory.ts spawned the SQLite worker via new Worker(blobUrl) with a plain string URL — Trusted Types requires a TrustedScriptURL, and the allowlist contains only VS Code's own policy names, so worker construction is blocked. The worker never starts → every host→worker RPC times out → "stuck loading", for all DB file types (.db, .sqlite, .gpkg, …). Desktop and the website demo were unaffected (different worker paths).

Measured live in the failing tab: new Worker(blob)BLOCKED (TrustedScriptURL); worker-src 'self' blob: is present (so the block is Trusted Types, not worker-src); WebAssembly instantiation → ALLOWED.

PR #419's parentPort change fixed a real but downstream bug — the worker is killed at construction, before that code runs — so it could not fix #418.

Fix

In browser mode, run the sql.js engine in-process in the extension host (createWorkerEndpoint() from core/sqlite-db) — no Worker, no blob URL, no RPC. This sidesteps Trusted Types and worker-src entirely, and WASM is permitted in that context. Errors propagate so a failure surfaces instead of hanging. Desktop is unchanged (keeps the worker_threads path; not CSP-constrained). sql.js is now bundled into out/extension-browser.js.

Tests

  • tests/unit/workerFactory_browser.test.ts (new): drives the browser path with VSCODE_BROWSER_EXT=true, asserts it uses the in-process endpoint, constructs no Worker (the mock throws if it does), and passes raw Uint8Array values through (no Transfer).
  • Existing workerFactory.test.ts (desktop worker path) unchanged and passing.

Verification

  • node scripts/build.mjs ✓ — out/extension-browser.js now contains the sql.js/emscripten runtime; 0 new Worker on the browser path.
  • npx tsc --noEmit -p tsconfig.json ✓ · npm test334/334 pass.

⚠️ Not yet verified in real vscode.dev

Real vscode.dev only runs the Marketplace-published build; a local/dev build can't be sideloaded there. So this needs publishing (e.g. 1.3.7) to confirm end-to-end in production. 1.3.6 is already spent (and broken). After publish, opening a DB in vscode.dev should render the grid.

Closes #418.

Summary by CodeRabbit

  • Bug Fixes

    • Improved runtime environment detection for browser and desktop scenarios with corrected extension environment checks
    • Optimized in-process database connection handling for browser-based extensions
  • Tests

    • Added comprehensive test suite validating browser WASM database connection initialization and endpoint behavior

…orkers (#418)

Real root cause of #418 (captured from published 1.3.6 in real vscode.dev):

    Failed to construct 'Worker': This document requires 'TrustedScriptURL' assignment.

VS Code Web enforces Trusted Types (require-trusted-types-for 'script') on the
extension host with a fixed trusted-types policy allowlist. workerFactory spawned
the SQLite worker via new Worker(blobUrl) with a plain string URL, which Trusted
Types blocks — the worker never starts, every RPC times out, and the viewer hangs
on the loading screen for all DB file types. (worker-src 'self' blob: IS allowed;
the block is Trusted Types, not worker-src. WebAssembly instantiation IS allowed.)

PR #419's parentPort fix addressed a real but downstream bug; the worker is killed
at construction before that code runs, so #419 could not fix #418.

Fix: in browser mode, run the sql.js engine in-process in the extension host via
createWorkerEndpoint() — no Worker, no blob URL, no RPC. Errors propagate so a
failure surfaces instead of hanging. Desktop keeps the worker_threads path
unchanged (not CSP-constrained). sql.js is now bundled into extension-browser.js.

Verified: node scripts/build.mjs OK (extension-browser.js now contains the
sql.js/emscripten runtime; 0 'new Worker' on the browser path); tsc --noEmit
clean; npm test passes (exit 0). Added tests/unit/workerFactory_browser.test.ts
covering the in-process path (no Worker constructed; init failure propagates).

Real-vscode.dev measurements confirming the approach: new Worker(blob) -> BLOCKED
(TrustedScriptURL); WebAssembly -> ALLOWED.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sq-lite-explorer Ready Ready Preview, Comment May 31, 2026 10:46pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Review Change Stack

Warning

Review limit reached

@zknpr, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 35 minutes and 23 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b319429f-2e2c-4154-a296-dda8800d3ec8

📥 Commits

Reviewing files that changed from the base of the PR and between 82c0023 and aebe8b8.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • package.json
  • src/core/sqlite-db.ts
  • src/workerFactory.ts
📝 Walkthrough

Walkthrough

This PR adds browser-specific WASM database support for VS Code Web (vscode.dev). The workerFactory now detects the browser environment and initializes an in-process SQLite connection by loading database and WASM bytes from extension assets, while updating environment detection to use optional chaining and simplifying Node worker message handling. New tests validate the browser connection flow without constructing real workers or RPC proxies.

Changes

Browser WASM Database Loading

Layer / File(s) Summary
Browser WASM connection implementation
src/workerFactory.ts
Adds createInProcessWasmDatabaseConnection factory that loads DB/WAL and sqlite3.wasm bytes from vscode filesystem, initializes the endpoint in-process, and wraps it with a DatabaseOperations facade that routes queries/IO while stubbing modification methods as no-ops for read-only browser execution.
Environment detection safeguards
src/workerFactory.ts
Applies optional chaining (import.meta.env?.VSCODE_BROWSER_EXT) consistently across native backend gate, native selection condition, and worker-backed connection checks for safe undefined handling.
Worker message subscription simplification
src/workerFactory.ts
Updates Node worker message handling to use simplified .on(...) handler path instead of previous browser Web Worker event listener branching.
Browser WASM connection tests
tests/unit/workerFactory_browser.test.ts
Adds test harness that transpiles workerFactory via esbuild and loads it in isolated context with mocked vscode/worker/RPC modules; establishes per-test filesystem stubs returning in-memory DB/WASM bytes; validates in-process endpoint initialization with correct sizes and operation pass-through.

🎯 3 (Moderate) | ⏱️ ~25 minutes


🐰 With browser support so fine,
WASM in the extension does shine,
No workers to fight,
Just bytes loaded right,
vscode.dev now loads your data in time!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: running sql.js in-process in VS Code Web to avoid Trusted Types blocking workers, directly addressing the core issue.
Linked Issues check ✅ Passed The code changes directly address issue #418: fixing the hanging behavior in vscode.dev by implementing in-process sql.js execution instead of worker-based approach.
Out of Scope Changes check ✅ Passed All changes are within scope: workerFactory.ts modifications for browser/desktop WASM selection and new test file validating browser in-process behavior align with fixing the vscode.dev hanging issue.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/418-web-inprocess-engine

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the WASM database connection creation to support running sql.js in-process in browser environments to bypass CSP restrictions on web workers, while maintaining worker-backed connections for Node.js. It also adds a unit test suite for the browser WASM connection. The review feedback identifies a potential memory leak in the browser connection due to a no-op [Symbol.dispose] implementation, and highlights an issue in the test setup where the esbuild define configuration fails to match the newly introduced optional chaining syntax (import.meta.env?.VSCODE_BROWSER_EXT).

Comment thread src/workerFactory.ts
Comment on lines +188 to +191
workerMethods: {
...endpoint,
[Symbol.dispose]: () => {}
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In browser mode, createWorkerEndpoint() is called per connection bundle, meaning each bundle has its own isolated activeEngine instance. Currently, [Symbol.dispose] is a no-op (() => {}), which means the active WASM database engine is never shut down when the connection bundle is disposed. This leads to a permanent memory leak of the sql.js WASM heap for every opened and closed database in the browser extension host.

To fix this, please add a dispose or shutdown method to the object returned by createWorkerEndpoint() in src/core/sqlite-db.ts that calls activeEngine?.shutdown(), and invoke it here in [Symbol.dispose].

    workerMethods: {
      ...endpoint,
      [Symbol.dispose]: () => {
        if ('dispose' in endpoint && typeof (endpoint as any).dispose === 'function') {
          (endpoint as any).dispose();
        }
      }
    },

Comment on lines +29 to +31
define: {
'import.meta.env.VSCODE_BROWSER_EXT': 'true'
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the source code in src/workerFactory.ts was updated to use optional chaining (import.meta.env?.VSCODE_BROWSER_EXT), esbuild's define replacement for import.meta.env.VSCODE_BROWSER_EXT will not match the optional chaining syntax. This can cause the test to fall back to the worker-backed path and fail.

Defining import.meta.env as an object literal expression ensures that both standard and optional chaining member accesses evaluate correctly during test execution.

Suggested change
define: {
'import.meta.env.VSCODE_BROWSER_EXT': 'true'
}
define: {
'import.meta.env': '({ VSCODE_BROWSER_EXT: true })'
}

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 82c00230dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/workerFactory.ts Outdated
return {
workerMethods: {
...endpoint,
[Symbol.dispose]: () => {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Release the in-process engine on dispose

In the VS Code Web path, DatabaseDocument.dispose() still calls workerMethods[Symbol.dispose](), but this new in-process branch makes that a no-op. createWorkerEndpoint() keeps the sql.js WasmDatabaseEngine in its activeEngine closure and only calls shutdown() when the same endpoint is reinitialized, so closing a database without reopening through that bundle never calls Database.close(). In vscode.dev, opening and closing several large DBs can retain WASM heap until the page is reloaded; please wire this dispose path to shut down the endpoint/engine instead of doing nothing.

Useful? React with 👍 / 👎.

Addresses the WASM-heap leak flagged independently by Gemini (high) and Codex
(P2) on PR #420.

The browser in-process connection set workerMethods[Symbol.dispose] to a no-op,
but DatabaseDocument.dispose() relies on it to release resources. createWorker-
Endpoint() only shut its activeEngine down on re-init, so closing a database tab
in vscode.dev never called Database.close() — the sql.js WASM heap leaked per
open/close until page reload.

- core/sqlite-db.ts: add endpoint.dispose() that shuts down and clears the
  active engine (idempotent; safe with no active engine).
- workerFactory.ts: browser bundle [Symbol.dispose] now calls endpoint.dispose().
  Desktop is unaffected (it terminates the worker thread instead).
- api_coverage.test.ts: add a regression test — verified to FAIL if dispose() is
  reverted to a no-op.

Also corrected the stale CSP comment: the real blocker measured in vscode.dev is
Trusted Types (require-trusted-types-for 'script'), not default-src/worker-src
(worker-src 'self' blob: is actually granted).

Verified: build OK (extension-browser.js still worker-free); tsc --noEmit clean;
npm test 335/335 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zknpr
Copy link
Copy Markdown
Owner Author

zknpr commented May 31, 2026

Review feedback addressed (pushed in 8eb8f8b)

Thanks @gemini-code-assist, @codex, @coderabbitai.

✅ Fixed — WASM heap leak on dispose (Gemini high + Codex P2, same issue)

Confirmed real. The in-process bundle's [Symbol.dispose] was a no-op, and createWorkerEndpoint() only shut its activeEngine down on re-init — so closing a DB tab in vscode.dev never called Database.close(), leaking the sql.js WASM heap per open/close.

  • core/sqlite-db.ts: added endpoint.dispose() that shuts down and clears activeEngine (idempotent; safe with no active engine).
  • workerFactory.ts: browser [Symbol.dispose] now calls endpoint.dispose(). Desktop unaffected (it terminates the worker thread).
  • api_coverage.test.ts: regression test — verified to FAIL if dispose() is reverted to a no-op.

⏭️ Rejected — Gemini's define: { 'import.meta.env': '({ VSCODE_BROWSER_EXT: true })' } suggestion

This is incorrect and would break the build. I verified empirically with the repo's esbuild:

  1. The suggested value throws: error: Invalid define value (must be an entity name or JS literal): ({ VSCODE_BROWSER_EXT: true }). esbuild define only accepts an entity name or a JS literal, not a parenthesized expression.
  2. The premise is also wrong: the existing dotted define already substitutes optional-chained access correctly. esbuild matches the member path regardless of ?.:
    • input import.meta.env?.VSCODE_BROWSER_EXT === true → output true
    • input !import.meta.env?.VSCODE_BROWSER_EXT → output false

The browser test passes for the right reason (it asserts no Worker is constructed, which only happens on the in-process browser path). No change needed; applying the suggestion would regress CI.

Note on comments

Also corrected a stale code comment: the real vscode.dev blocker (measured live) is Trusted Types (require-trusted-types-for 'script'), not default-src/worker-srcworker-src 'self' blob: is actually granted; new Worker(stringURL) fails the TrustedScriptURL requirement.

Verification

node scripts/build.mjs ✓ (browser bundle still worker-free) · tsc --noEmit ✓ · npm test334/334 pass.

⚠️ Still pending: real end-to-end verification in vscode.dev requires publishing (1.3.7), since the Marketplace build can't be sideloaded.

@gemini-code-assist
Copy link
Copy Markdown

Thank you for the detailed follow-up and for verifying the fix in your environment. Your analysis regarding the esbuild define behavior and the Trusted Types blocker is spot on—I appreciate the correction and the thorough verification steps you've taken. It's great to see the [Symbol.dispose] implementation successfully addressing the WASM heap leak. Your approach to isolating the browser path while keeping the worker-based path for desktop is the correct architectural decision here. I'm glad the regression tests are passing and that you've identified the root cause so clearly.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Tip

For best results, initiate chat on the files or code changes.

@zknpr: Excellent, thorough handling of all the review points. The WASM heap-leak fix is well-structured — making dispose() idempotent, fixing both layers (sqlite-db.ts and workerFactory.ts), and adding a regression test that verifiably catches the regression is the right approach. The rejection of Gemini's define suggestion is technically correct (esbuild does not accept parenthesized expressions in define values, and the member-path substitution already handles optional chaining). The Trusted Types clarification is also important — worker-src isn't the constraint, the TrustedScriptURL type enforcement is.

The 335/335 pass rate and clean tsc --noEmit look good. Happy to re-review the commit once you're ready, or trigger a full re-review now.

🐇 ✨


✏️ Learnings added
Learnt from: zknpr
URL: https://github.com/zknpr/SQLite-Explorer/pull/420

Timestamp: 2026-05-31T22:32:21.617Z
Learning: In the SQLite Explorer VS Code extension (zknpr/SQLite-Explorer), the real vscode.dev/github.dev blocker for sql.js workers is Trusted Types (`require-trusted-types-for 'script'`), NOT `default-src` or `worker-src`. `worker-src 'self' blob:` is actually granted. `new Worker(stringURL)` fails because it requires a `TrustedScriptURL` object, not a plain string URL.

Learnt from: zknpr
URL: https://github.com/zknpr/SQLite-Explorer/pull/420

Timestamp: 2026-05-31T22:32:21.617Z
Learning: In the SQLite Explorer VS Code extension (zknpr/SQLite-Explorer), esbuild's `define` option only accepts entity names or JS literals — parenthesized expressions like `({ VSCODE_BROWSER_EXT: true })` are invalid and throw a build error. Additionally, esbuild's dotted-path define (e.g., `import.meta.env.VSCODE_BROWSER_EXT`) correctly substitutes optional-chained access (`import.meta.env?.VSCODE_BROWSER_EXT`) at build time.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3188b33c6f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/workerFactory.ts
Comment on lines +240 to +244
applyModifications: async () => {},
undoModification: async () => {},
redoModification: async () => {},
flushChanges: async () => {},
discardModifications: async () => {},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Forward modification operations in web WASM facade

In the new VS Code Web in-process WASM path, these modification methods resolve without calling the underlying WasmDatabaseEngine. When a user in vscode.dev restores a hot-exit backup (applyModifications), invokes undo/redo, or reverts unsaved edits (discardModifications), the document tracker/UI will treat the operation as successful while the in-memory database remains unchanged, and auto-save can then persist the wrong state. The worker-backed facade already had similar stubs, but this commit adds them to the now-active browser path, making those flows reachable in Web.

Useful? React with 👍 / 👎.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

@zknpr: All three items are cleanly handled:

  • Heap leak fix: The two-layer dispose (sqlite-db.ts endpoint method + workerFactory.ts caller) with idempotency guarantees is the right structure. The regression test that verifiably fails on revert is exactly what's needed here — good hygiene.
  • esbuild define rejection: Your empirical verification is correct. Parenthesized object literals are not valid esbuild define values, and the member-path substitution already handles optional chaining; the suggestion would have regressed CI.
  • Trusted Types comment correction: Important accuracy improvement — the distinction between worker-src (permitted) and TrustedScriptURL enforcement (the actual blocker) matters for anyone reading the code later.

One small correction from my previous reply: I mistakenly wrote "335/335" — the correct count is 334/334 as you reported.

Happy to trigger a full re-review of commit 8eb8f8b.

🐇 ✨


✏️ Learnings added
Learnt from: zknpr
URL: https://github.com/zknpr/SQLite-Explorer/pull/420

Timestamp: 2026-05-31T22:35:26.621Z
Learning: In the SQLite Explorer VS Code extension (zknpr/SQLite-Explorer), the WASM heap leak fix pattern on dispose uses two layers: (1) `endpoint.dispose()` method added to the worker endpoint in `core/sqlite-db.ts` that shuts down and clears `activeEngine` (idempotent, safe with no active engine), and (2) the browser `[Symbol.dispose]` in `workerFactory.ts` calls `endpoint.dispose()`. Desktop (worker thread) dispose is unaffected. A regression test in `api_coverage.test.ts` verifies that dispose is NOT a no-op — it is confirmed to FAIL if `dispose()` is reverted to a no-op.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Actions performed

Full review triggered.

@chatgpt-codex-connector
Copy link
Copy Markdown

Summary

  • Added dispose?() to the browser workerFactory test endpoint fixture so the mocked in-process endpoint can model the production disposal hook. tests/unit/workerFactory_browser.test.tsL15-L23
  • Added regression coverage that creates the browser connection bundle, invokes workerMethods[Symbol.dispose](), and asserts the in-process endpoint’s dispose() callback is called exactly once. tests/unit/workerFactory_browser.test.tsL172-L192
  • Committed the change as 9c0ea7e (test(web): cover in-process endpoint disposal) and created a PR titled test(web): cover in-process endpoint disposal.

Testing

  • npm test -- tests/unit/workerFactory_browser.test.ts
  • npx tsc --noEmit -p tsconfig.json
  • npx tsx --tsconfig tsconfig.test.json --test tests/unit/workerFactory_browser.test.ts

View task →

Bumps version 1.3.6 -> 1.3.7 and adds the CHANGELOG entry for the real
VS Code Web fix (in-process sql.js engine; Trusted Types blocked the worker).
Notes that 1.3.6's parentPort fix was downstream and did not resolve #418.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

[Bug]: Cannot load gpkg in vscode.dev

1 participant