Skip to content

Commit b7724f5

Browse files
zknprclaude
andauthored
feat: implement 5 improvements from closed PRs (#78, #85, #95, #104, #105) (#108)
1. getNodeFs helper (#78): Extract repeated `typeof require === 'function' ? require('fs') : undefined` into reusable `getNodeFs()` in sqlite-db.ts (3 call sites). 2. Worker logging via RPC (#85): Route worker console.* through RPC LogEnvelope to VS Code output channel instead of losing logs in the worker thread. 3. Native schema fetch optimization (#95): Replace 3 parallel query() calls with single queryBatch() in nativeWorker.ts, with proper error handling instead of optional chaining. 4. Webview state serialization (#104): Replace retainContextWhenHidden with setState/getState persistence. Includes scroll position restoration after grid render, dateFormat/cellEditBehavior state fields, and all UI control value restoration. 5. JSON Patch SQL optimization (#105): Use SQLite's native json_patch() when available (probed at construction time), falling back to JS-side applyMergePatch. Uses COALESCE for NULL column handling. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2720281 commit b7724f5

15 files changed

Lines changed: 353 additions & 97 deletions

File tree

core/ui/modules/api.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@
55

66
const vscodeApi = typeof acquireVsCodeApi !== 'undefined' ? acquireVsCodeApi() : null;
77

8+
/**
9+
* Get saved state from VS Code's webview state persistence.
10+
* @returns {Object|undefined} Previously saved state, or undefined if none
11+
*/
12+
export function getVsCodeState() {
13+
return vscodeApi ? vscodeApi.getState() : undefined;
14+
}
15+
16+
/**
17+
* Save state to VS Code's webview state persistence.
18+
* This survives tab switches when retainContextWhenHidden is false.
19+
* @param {Object} stateObj - State to save
20+
*/
21+
export function saveVsCodeState(stateObj) {
22+
if (vscodeApi) {
23+
vscodeApi.setState(stateObj);
24+
}
25+
}
26+
827
// Default RPC timeout in milliseconds (60s to accommodate large blob operations)
928
const RPC_TIMEOUT_MS = 60000;
1029

core/ui/modules/grid.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Data Grid Rendering and Logic
33
*/
4-
import { state } from './state.js';
4+
import { state, persistState } from './state.js';
55
import { backendApi } from './api.js';
66
import { escapeHtml, escapeIdentifier, formatCellValue, formatCellValueAsText } from './utils.js';
77
import { updateStatus, showLoading, showErrorState, updateToolbarButtons } from './ui.js';
@@ -192,6 +192,13 @@ export function initGridInteraction() {
192192
}
193193
}
194194
});
195+
196+
// Track scroll position for state persistence
197+
container.addEventListener('scroll', () => {
198+
state.scrollPosition.left = container.scrollLeft;
199+
state.scrollPosition.top = container.scrollTop;
200+
persistState();
201+
}, { passive: true });
195202
}
196203

197204
function resolveRowIdType(idStr) {
@@ -727,20 +734,23 @@ export function onFilterChange() {
727734
state.filterQuery = document.getElementById('filterInput').value;
728735
state.currentPageIndex = 0;
729736
loadTableData();
737+
persistState();
730738
}, 300);
731739
}
732740

733741
export function onPageSizeChange() {
734742
state.rowsPerPage = parseInt(document.getElementById('pageSizeSelect').value, 10);
735743
state.currentPageIndex = 0;
736744
loadTableData();
745+
persistState();
737746
}
738747

739748
export function onDateFormatChange() {
740749
const select = document.getElementById('dateFormatSelect');
741750
if (select) {
742751
state.dateFormat = select.value;
743752
renderDataGrid();
753+
persistState();
744754
}
745755
}
746756

@@ -760,6 +770,7 @@ export function onColumnSort(columnName) {
760770
state.sortAscending = true;
761771
}
762772
loadTableData();
773+
persistState();
763774
}
764775

765776
export function applyColumnFilter(columnName) {
@@ -899,6 +910,7 @@ export function toggleColumnPin(event, columnName) {
899910
state.pinnedColumns.add(columnName);
900911
}
901912
renderDataGrid();
913+
persistState();
902914
}
903915

904916
// Row Pinning
@@ -910,6 +922,7 @@ export function toggleRowPin(event, rowId) {
910922
state.pinnedRowIds.add(rowId);
911923
}
912924
renderDataGrid();
925+
persistState();
913926
}
914927

915928
// Column Resizing

core/ui/modules/sidebar.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Sidebar and Schema Logic
33
*/
4-
import { state } from './state.js';
4+
import { state, persistState } from './state.js';
55
import { backendApi } from './api.js';
66
import { escapeHtml } from './utils.js';
77
import { updateStatus } from './ui.js';
@@ -533,6 +533,7 @@ export async function selectTableItem(name, type) {
533533

534534
await loadTableColumns();
535535
await loadTableData(true, false);
536+
persistState();
536537
}
537538

538539
export async function reloadFromDisk() {

core/ui/modules/state.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* Application State
33
*/
4+
import { saveVsCodeState } from './api.js';
5+
46
export const state = {
57
isDbConnected: false,
68
selectedTable: null,
@@ -65,3 +67,33 @@ export const state = {
6567
dateFormat: 'raw', // 'raw', 'local', 'iso', 'relative'
6668
cellEditBehavior: 'inline' // 'inline', 'modal', 'vscode'
6769
};
70+
71+
/**
72+
* Debounced state persistence to VS Code.
73+
* Saves a snapshot of user-facing state so it survives tab switches
74+
* when retainContextWhenHidden is false.
75+
*/
76+
let _persistTimer;
77+
export function persistState() {
78+
if (_persistTimer) clearTimeout(_persistTimer);
79+
_persistTimer = setTimeout(() => {
80+
saveVsCodeState({
81+
selectedTable: state.selectedTable,
82+
selectedTableType: state.selectedTableType,
83+
currentPageIndex: state.currentPageIndex,
84+
rowsPerPage: state.rowsPerPage,
85+
sortedColumn: state.sortedColumn,
86+
sortAscending: state.sortAscending,
87+
filterQuery: state.filterQuery,
88+
columnWidths: state.columnWidths,
89+
columnFilters: state.columnFilters,
90+
pinnedColumns: Array.from(state.pinnedColumns),
91+
pinnedRowIds: Array.from(state.pinnedRowIds),
92+
selectedColumns: Array.from(state.selectedColumns),
93+
sidebarFilter: state.sidebarFilter,
94+
scrollPosition: state.scrollPosition,
95+
dateFormat: state.dateFormat,
96+
cellEditBehavior: state.cellEditBehavior,
97+
});
98+
}, 500);
99+
}

core/ui/modules/web-api.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@
99
// Use parent window for RPC instead of VS Code API
1010
const parentWindow = window.parent;
1111

12+
/**
13+
* No-op in web demo: VS Code state persistence is not available.
14+
* @returns {undefined}
15+
*/
16+
export function getVsCodeState() {
17+
return undefined;
18+
}
19+
20+
/**
21+
* No-op in web demo: VS Code state persistence is not available.
22+
* @param {Object} _stateObj - Ignored
23+
*/
24+
export function saveVsCodeState(_stateObj) {
25+
// No VS Code API available in web demo
26+
}
27+
1228
// Default RPC timeout in milliseconds (60s to accommodate large blob operations)
1329
const RPC_TIMEOUT_MS = 60000;
1430

core/ui/viewer.html

Lines changed: 14 additions & 14 deletions
Large diffs are not rendered by default.

core/ui/viewer.js

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
*/
44
import { state } from './modules/state.js';
55
import { initRpc } from './modules/rpc.js';
6-
import { backendApi } from './modules/api.js';
6+
import { backendApi, getVsCodeState } from './modules/api.js';
77
import {
88
initSidebar,
9-
refreshSchema
9+
refreshSchema,
10+
renderSidebar
1011
} from './modules/sidebar.js';
1112
import {
1213
initExport
@@ -27,6 +28,7 @@ import {
2728
} from './modules/modals.js';
2829
import {
2930
loadTableData,
31+
loadTableColumns,
3032
onSelectAllClick,
3133
initGridInteraction,
3234
initGridControls,
@@ -88,8 +90,74 @@ async function initializeApp() {
8890
// Load schema
8991
await refreshSchema();
9092

91-
updateStatus('Ready');
92-
showEmptyState();
93+
// Restore previously saved webview state (survives tab switches
94+
// when retainContextWhenHidden is false)
95+
const savedState = getVsCodeState();
96+
if (savedState && savedState.selectedTable) {
97+
// Restore scalar state fields
98+
state.selectedTable = savedState.selectedTable;
99+
state.selectedTableType = savedState.selectedTableType || 'table';
100+
state.currentPageIndex = savedState.currentPageIndex || 0;
101+
state.rowsPerPage = savedState.rowsPerPage || 500;
102+
state.sortedColumn = savedState.sortedColumn || null;
103+
state.sortAscending = savedState.sortAscending !== false;
104+
state.filterQuery = savedState.filterQuery || '';
105+
state.columnWidths = savedState.columnWidths || {};
106+
state.columnFilters = savedState.columnFilters || {};
107+
state.sidebarFilter = savedState.sidebarFilter || '';
108+
state.dateFormat = savedState.dateFormat || 'raw';
109+
state.cellEditBehavior = savedState.cellEditBehavior || 'inline';
110+
111+
// Restore Set-based state (serialized as arrays)
112+
state.pinnedColumns = new Set(savedState.pinnedColumns || []);
113+
state.pinnedRowIds = new Set(savedState.pinnedRowIds || []);
114+
state.selectedColumns = new Set(savedState.selectedColumns || []);
115+
116+
// Capture scroll position before rendering (will be applied after grid render)
117+
const savedScroll = savedState.scrollPosition || { top: 0, left: 0 };
118+
state.scrollPosition = { ...savedScroll };
119+
120+
// Restore sidebar filter input value
121+
const sidebarFilterInput = document.getElementById('sidebarFilterInput');
122+
if (sidebarFilterInput) sidebarFilterInput.value = state.sidebarFilter;
123+
124+
// Restore global filter input value
125+
const filterInput = document.getElementById('filterInput');
126+
if (filterInput) filterInput.value = state.filterQuery;
127+
128+
// Restore date format dropdown
129+
const dateFormatSelect = document.getElementById('dateFormatSelect');
130+
if (dateFormatSelect) dateFormatSelect.value = state.dateFormat;
131+
132+
// Restore page size dropdown
133+
const pageSizeSelect = document.getElementById('pageSizeSelect');
134+
if (pageSizeSelect) pageSizeSelect.value = String(state.rowsPerPage);
135+
136+
// Update table name label
137+
const tableNameLabel = document.getElementById('tableNameLabel');
138+
if (tableNameLabel) tableNameLabel.textContent = state.selectedTable;
139+
140+
// Re-render sidebar with restored selection highlight
141+
renderSidebar();
142+
143+
// Load column metadata and table data, then restore scroll position
144+
await loadTableColumns();
145+
await loadTableData(true, false);
146+
147+
// Restore scroll position after the grid has been rendered.
148+
// loadTableData already uses state.scrollPosition for its render call,
149+
// but we also explicitly set it here to handle any race with the DOM update.
150+
const container = document.getElementById('gridContainer');
151+
if (container) {
152+
container.scrollLeft = savedScroll.left;
153+
container.scrollTop = savedScroll.top;
154+
}
155+
156+
updateStatus(`${state.totalRecordCount} records`);
157+
} else {
158+
updateStatus('Ready');
159+
showEmptyState();
160+
}
93161

94162
// Global shortcuts
95163
document.addEventListener('keydown', async (event) => {

src/core/rpc.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,19 @@ interface ResponseEnvelope {
3838
readonly errorText?: string;
3939
}
4040

41+
/**
42+
* Log message forwarded from worker to host.
43+
*/
44+
interface LogEnvelope {
45+
readonly kind: 'log';
46+
readonly level: 'log' | 'warn' | 'error';
47+
readonly args: unknown[];
48+
}
49+
4150
/**
4251
* Union of all protocol message types.
4352
*/
44-
type ProtocolEnvelope = InvocationEnvelope | ResponseEnvelope;
53+
type ProtocolEnvelope = InvocationEnvelope | ResponseEnvelope | LogEnvelope;
4554

4655
// ============================================================================
4756
// State Management
@@ -202,10 +211,16 @@ type ResponseDispatcher = (response: ResponseEnvelope, transfer?: Transferable[]
202211
* @param sendResponse - Optional function to send responses
203212
* @returns true if message was handled, false otherwise
204213
*/
214+
/**
215+
* Callback for handling log messages forwarded from a worker.
216+
*/
217+
type LogHandler = (level: 'log' | 'warn' | 'error', args: unknown[]) => void;
218+
205219
export function processProtocolMessage(
206220
envelope: unknown,
207221
localMethods?: MethodImplementations,
208-
sendResponse?: ResponseDispatcher
222+
sendResponse?: ResponseDispatcher,
223+
onLog?: LogHandler
209224
): boolean {
210225
// Validate envelope structure
211226
if (!envelope || typeof envelope !== 'object') return false;
@@ -270,6 +285,15 @@ export function processProtocolMessage(
270285
return true;
271286
}
272287

288+
// Handle log message forwarded from worker
289+
if (msg.kind === 'log' && 'level' in msg && 'args' in msg) {
290+
const logMsg = msg as LogEnvelope;
291+
if (onLog) {
292+
onLog(logMsg.level, logMsg.args);
293+
}
294+
return true;
295+
}
296+
273297
// Handle incoming response
274298
if (msg.kind === 'result') {
275299
const { correlationId, payload, errorText } = msg;
@@ -313,7 +337,8 @@ interface WorkerPort {
313337
*/
314338
export function connectWorkerPort<T extends object>(
315339
port: WorkerPort,
316-
methodNames: string[]
340+
methodNames: string[],
341+
onLog?: LogHandler
317342
): T {
318343
const dispatcher: MessageDispatcher = (envelope, transfer) => {
319344
// Check if port supports transfer list (Browser/Node worker compatible)
@@ -332,7 +357,7 @@ export function connectWorkerPort<T extends object>(
332357
};
333358

334359
port.on('message', (data) => {
335-
processProtocolMessage(data);
360+
processProtocolMessage(data, undefined, undefined, onLog);
336361
});
337362

338363
return buildMethodProxy<T>(dispatcher, methodNames);

0 commit comments

Comments
 (0)