Skip to content
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 1.3.7

### Bug Fixes

- **VS Code for Web (vscode.dev / github.dev): databases stuck on the loading screen — actual fix** ([#418]). Every SQLite database (`.db`, `.sqlite`, `.gpkg`, …) hung forever on the loading screen in the web build. Root cause (confirmed in real vscode.dev): VS Code Web enforces Trusted Types (`require-trusted-types-for 'script'`) on the extension host with a fixed policy allowlist, so the SQLite engine's `new Worker(blobUrl)` was blocked (`Failed to construct 'Worker': This document requires 'TrustedScriptURL' assignment`) — the worker never started and every database operation timed out silently. In browser mode the sql.js engine now runs **in-process in the extension host** (no Web Worker), which Trusted Types permits; desktop VS Code continues to use a worker thread. The engine's WASM memory is released when a database is closed.

Note: the 1.3.6 web fix (a `parentPort` adapter) addressed a real but downstream bug and did **not** resolve #418, because the worker is blocked at construction before that code runs. 1.3.7 supersedes it.

[#418]: https://github.com/zknpr/SQLite-Explorer/issues/418

## 1.3.6

### Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "sqlite-explorer",
"displayName": "SQLite Explorer",
"description": "A powerful SQLite database viewer and editor for VS Code",
"version": "1.3.6",
"version": "1.3.7",
"publisher": "zknpr",
"license": "MIT",
"repository": {
Expand Down
15 changes: 15 additions & 0 deletions src/core/sqlite-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,21 @@ export function createWorkerEndpoint() {

async writeToFile(path: string): Promise<void> {
return requireEngine().writeToFile(path);
},

/**
* Release the active engine and its underlying sql.js WASM heap.
*
* Safe to call when no database is active (no-op) and idempotent. The
* in-process browser connection wires this to its bundle's [Symbol.dispose]
* so closing a database frees the WASM instance instead of leaking it; the
* Node worker path tears down the whole worker thread instead.
*/
dispose(): void {
if (activeEngine) {
activeEngine.shutdown();
activeEngine = null;
}
}
};
}
176 changes: 144 additions & 32 deletions src/workerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type {
import { Worker } from './platform/threadPool';
import type { DatabaseConnectionBundle } from './connectionTypes';
import { getMaximumFileSizeBytes, getQueryTimeout } from './config';
import { createWorkerEndpoint } from './core/sqlite-db';

// Native worker support (only in Node.js environment)
let nativeSupport: {
Expand All @@ -42,7 +43,7 @@ let nativeSupport: {
} | null = null;

// Dynamically import native worker in Node.js environment
if (!import.meta.env.VSCODE_BROWSER_EXT) {
if (!import.meta.env?.VSCODE_BROWSER_EXT) {
try {
// Use dynamic import for native worker
nativeSupport = require('./nativeWorker');
Expand Down Expand Up @@ -108,7 +109,7 @@ export async function createDatabaseConnection(
_reporter?: TelemetryReporter
): Promise<DatabaseConnectionBundle> {
// Try native SQLite first (desktop Node.js only)
if (!import.meta.env.VSCODE_BROWSER_EXT && nativeSupport) {
if (!import.meta.env?.VSCODE_BROWSER_EXT && nativeSupport) {
const extensionPath = extensionUri.fsPath;
if (await nativeSupport.isNativeAvailable(extensionPath)) {
try {
Expand Down Expand Up @@ -151,8 +152,9 @@ export async function createDatabaseConnection(
/**
* Create a database connection using sql.js (WebAssembly).
*
* The worker runs sql.js in a separate thread to prevent
* blocking the main extension host during database operations.
* Browser VS Code runs sql.js in-process to avoid CSP-blocked workers.
* Desktop VS Code keeps sql.js in a worker thread to avoid blocking the
* extension host during database operations.
*
* @param extensionUri - Extension installation directory URI
* @param _reporter - Optional telemetry reporter
Expand All @@ -162,27 +164,142 @@ async function createWasmDatabaseConnection(
extensionUri: vsc.Uri,
_reporter?: TelemetryReporter
): Promise<DatabaseConnectionBundle> {
// Spawn worker thread
// Browser: Web Workers can't load from vscode-vfs:// URIs directly.
// Use fetch to load the worker script as a Blob and create a Blob URL.
// Node.js: Use require path directly.
let workerThread: InstanceType<typeof Worker>;

if (import.meta.env.VSCODE_BROWSER_EXT) {
// Browser environment: fetch worker script and create Blob URL
const workerScriptUri = vsc.Uri.joinPath(extensionUri, 'out', 'worker-browser.js');
const workerContent = await vsc.workspace.fs.readFile(workerScriptUri);
const blob = new Blob([workerContent as BlobPart], { type: 'application/javascript' });
const blobUrl = URL.createObjectURL(blob);
workerThread = new Worker(blobUrl);
} else {
// Node.js environment: use file path directly
const workerScriptPath = path.resolve(__dirname, './worker.cjs');
workerThread = new Worker(workerScriptPath);
if (import.meta.env?.VSCODE_BROWSER_EXT === true) {
return createInProcessWasmDatabaseConnection(extensionUri);
}

// Create IPC proxy for worker communication
// Browser Workers use addEventListener, Node.js Workers use .on()
return createWorkerBackedWasmDatabaseConnection(extensionUri);
}

/**
* Create a browser-safe WASM database connection.
*
* VS Code Web enforces Trusted Types (require-trusted-types-for 'script') on the
* browser extension host with a fixed trusted-types policy allowlist, so
* `new Worker(blobUrl)` with a plain string URL is blocked ("This document
* requires 'TrustedScriptURL' assignment") and the worker never starts. (The CSP
* does allow worker-src 'self' blob:; the blocker is Trusted Types, not
* worker-src. WebAssembly instantiation is permitted.) This bundle therefore runs
* the sql.js endpoint directly in the extension host and lets thrown
* initialization/query errors reject the original operation.
*/
async function createInProcessWasmDatabaseConnection(
extensionUri: vsc.Uri
): Promise<DatabaseConnectionBundle> {
const endpoint = createWorkerEndpoint();

return {
workerMethods: {
...endpoint,
// Free the in-process sql.js WASM heap when the document is disposed.
// Unlike the Node worker path (which terminates the whole thread), the
// browser engine lives in the extension host, so closing a database must
// explicitly shut the engine down or its WASM memory leaks until reload.
[Symbol.dispose]: () => { endpoint.dispose(); }
},
Comment on lines +192 to +199
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();
        }
      }
    },


/**
* Establish a database connection in the browser extension host.
*
* The browser path must read binary content through VS Code's workspace
* filesystem because local file paths are not available in vscode.dev.
*/
async establishConnection(
fileUri: vsc.Uri,
displayName: string,
forceReadOnly?: boolean,
autoCommit?: boolean
) {
// Browser mode always reads database bytes through the VS Code filesystem.
// There is no file-path fast path because the web extension host cannot
// access local disk paths directly.
const [dbContent, walContent] = await loadDatabaseFiles(fileUri);

// Preload sql.js WASM bytes from the extension assets directory so
// WebAssembly instantiation does not depend on worker-relative URLs.
const wasmUri = vsc.Uri.joinPath(extensionUri, 'assets', 'sqlite3.wasm');
const wasmContent = await vsc.workspace.fs.readFile(wasmUri);

const initConfig: DatabaseInitConfig = {
content: dbContent,
walContent,
maxSize: getMaximumFileSizeBytes(),
resourceMap: {},
wasmBinary: wasmContent,
readOnlyMode: forceReadOnly ?? false,
queryTimeout: getQueryTimeout()
};

const result = await endpoint.initializeDatabase(displayName, initConfig);

const operationsFacade: DatabaseOperations = {
engineKind: Promise.resolve('wasm'),
executeQuery: (sql: string, params?: CellValue[]) =>
endpoint.runQuery(sql, params),
serializeDatabase: (name: string) => endpoint.exportDatabase(name),
applyModifications: async () => {},
undoModification: async () => {},
redoModification: async () => {},
flushChanges: async () => {},
discardModifications: async () => {},
Comment on lines +240 to +244
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 👍 / 👎.

updateCell: (table: string, rowId: string | number, column: string, value: CellValue) =>
endpoint.updateCell(table, rowId, column, value),
insertRow: (table: string, data: Record<string, CellValue>) =>
endpoint.insertRow(table, data),
insertRowBatch: (table: string, rows: Record<string, CellValue>[]) =>
endpoint.insertRowBatch(table, rows),
deleteRows: (table: string, rowIds: (string | number)[]) =>
endpoint.deleteRows(table, rowIds),
deleteColumns: (table: string, columns: string[], dropDependentIndexes?: string[]) =>
endpoint.deleteColumns(table, columns, dropDependentIndexes),
findDependentIndexes: (table: string, columns: string[]) =>
endpoint.findDependentIndexes(table, columns),
createTable: (table: string, columns: ColumnDefinition[]) =>
endpoint.createTable(table, columns),
updateCellBatch: (table: string, updates: CellUpdate[]) =>
endpoint.updateCellBatch(table, updates),
addColumn: (table: string, column: string, type: string, defaultValue?: string) =>
endpoint.addColumn(table, column, type, defaultValue),
fetchTableData: (table: string, options: TableQueryOptions) =>
endpoint.fetchTableData(table, options),
fetchTableCount: (table: string, options: TableCountOptions) =>
endpoint.fetchTableCount(table, options),
fetchSchema: () =>
endpoint.fetchSchema(),
getTableInfo: (table: string) =>
endpoint.getTableInfo(table),
getPragmas: () =>
endpoint.getPragmas(),
setPragma: (pragma: string, value: CellValue) =>
endpoint.setPragma(pragma, value),
ping: () =>
endpoint.ping(),
writeToFile: (path: string) =>
endpoint.writeToFile(path)
};

return {
databaseOps: operationsFacade,
isReadOnly: result.isReadOnly ?? false
};
}
};
}

/**
* Create a Node.js WASM database connection backed by worker_threads.
*
* Desktop VS Code is not constrained by the browser extension host CSP, so it
* keeps the existing worker/RPC path to avoid blocking the extension host.
*/
async function createWorkerBackedWasmDatabaseConnection(
extensionUri: vsc.Uri
): Promise<DatabaseConnectionBundle> {
// Node.js environment: use file path directly
const workerScriptPath = path.resolve(__dirname, './worker.cjs');
const workerThread: InstanceType<typeof Worker> = new Worker(workerScriptPath);

// Create IPC proxy for Node.js worker communication
// Route worker log messages to the VS Code output channel for visibility.
// Falls back to console if no output channel is available (e.g., during tests).
const logHandler = (level: 'log' | 'warn' | 'error', args: unknown[]) => {
Expand All @@ -204,14 +321,9 @@ async function createWasmDatabaseConnection(
}
},
on: (event: 'message', handler: (data: unknown) => void) => {
if (import.meta.env.VSCODE_BROWSER_EXT) {
// Browser: Web Worker uses addEventListener with MessageEvent wrapper
workerThread.addEventListener(event, (e: MessageEvent) => handler(e.data));
} else {
// Node.js: worker_threads uses .on() with direct data
(workerThread as unknown as { on(event: string, handler: (data: unknown) => void): void })
.on(event, handler);
}
// Node.js worker_threads uses .on() with direct message payloads.
(workerThread as unknown as { on(event: string, handler: (data: unknown) => void): void })
.on(event, handler);
}
},
['initializeDatabase', 'runQuery', 'exportDatabase', 'updateCell', 'insertRow', 'insertRowBatch', 'deleteRows', 'deleteColumns', 'findDependentIndexes', 'createTable', 'updateCellBatch', 'addColumn', 'fetchTableData', 'fetchTableCount', 'fetchSchema', 'getTableInfo', 'getPragmas', 'setPragma', 'ping', 'writeToFile'],
Expand Down Expand Up @@ -248,7 +360,7 @@ async function createWasmDatabaseConnection(
// Read database and WAL files
// Optimization: If running in Node and file is local, pass path to worker instead of reading content here
// This avoids blocking the extension host and transferring large buffers
const isNode = !import.meta.env.VSCODE_BROWSER_EXT;
const isNode = !import.meta.env?.VSCODE_BROWSER_EXT;
const isLocal = fileUri.scheme === 'file';

let dbContent: Uint8Array | null = null;
Expand Down
Loading
Loading