Delete sql.js: remove db.ts, wasm, dependency, and all sql.js code paths

- Remove paperforge/plugin/src/services/db.ts (sql.js wrapper, initDatabase/searchMetadata/SearchResultItem)
- Remove paperforge/plugin/sql-wasm.wasm (644KB binary)
- Remove sql.js and @types/sql.js from package.json; npm install to update lockfile
- dashboard.ts: remove import of db.ts, remove _sqlJsInitialized/_sqlJsFailed state,
  remove the sql.js search path block entirely — all searches go straight to CLI spawn.
  Clean up stale comments.
- Bundle drops from 346KB to 167KB; 6 test files / 58 tests still pass
This commit is contained in:
LLLin000 2026-07-10 23:07:26 +08:00
parent f52124bcf7
commit 32e69a6c7b
6 changed files with 43 additions and 9832 deletions

File diff suppressed because one or more lines are too long

View file

@ -7,12 +7,8 @@
"": {
"name": "paperforge-plugin",
"version": "1.0.0",
"dependencies": {
"sql.js": "^1.14.1"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/sql.js": "^1.4.11",
"builtin-modules": "^3.3.0",
"esbuild": "^0.25.0",
"husky": "^9.1.7",
@ -1004,13 +1000,6 @@
"@types/tern": "*"
}
},
"node_modules/@types/emscripten": {
"version": "1.41.5",
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
"integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@ -1029,17 +1018,6 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/sql.js": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@types/sql.js/-/sql.js-1.4.11.tgz",
"integrity": "sha512-QXIx38p2ZThJaK9vP5ZdqdlRe1FG9I8SmCZOS7FHfB/2qPAjZwkL7/vlfPg6N/oWHuuOaGg/P/IRwfP2W0kWVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/emscripten": "*",
"@types/node": "*"
}
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
@ -2439,12 +2417,6 @@
"node": ">=0.10.0"
}
},
"node_modules/sql.js": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz",
"integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==",
"license": "MIT"
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",

View file

@ -13,7 +13,6 @@
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/sql.js": "^1.4.11",
"builtin-modules": "^3.3.0",
"esbuild": "^0.25.0",
"husky": "^9.1.7",
@ -24,8 +23,5 @@
"prettier": "^3.8.4",
"typescript": "^5.4.0",
"vitest": "^2.1.0"
},
"dependencies": {
"sql.js": "^1.14.1"
}
}

Binary file not shown.

View file

@ -1,119 +0,0 @@
import * as fs from "fs";
import * as path from "path";
import initSqlJs from "sql.js";
import type {
Database as SqlJsDatabase,
Statement as SqlJsStatement,
SqlValue,
} from "sql.js";
// ── Types ──
export interface SearchResultItem {
zotero_key: string;
title: string;
first_author: string;
year: string;
journal: string;
domain: string;
abstract: string;
rank: number;
}
// ── Module-level state (lazy init) ──
let _db: SqlJsDatabase | null = null;
let _queryStmt: SqlJsStatement | null = null;
// ── FTS5 query transform ──
// FTS5 requires explicit operators between terms: "rotator cuff" → "rotator AND cuff"
// Quoted phrases like '"rotator cuff"' are passed through verbatim.
function transformFtsQuery(input: string): string {
input = input.trim();
if (!input) return "";
// Already a quoted phrase — pass through as-is
if (input.startsWith('"') && input.endsWith('"')) {
return input;
}
const terms = input.split(/\s+/).filter(Boolean);
if (terms.length === 0) return "";
return terms.join(" AND ");
}
// ── Database init (lazy) ──
function sqlValueToString(v: SqlValue): string {
if (v === null || v === undefined) return "";
if (typeof v === "string") return v;
if (v instanceof Uint8Array) return new TextDecoder().decode(v);
return String(v);
}
function sqlValueToNumber(v: SqlValue): number {
if (v === null || v === undefined) return 0;
if (typeof v === "number") return v;
return Number(v);
}
export async function initDatabase(vaultPath: string): Promise<void> {
const dbPath = path.join(
vaultPath,
"System",
"PaperForge",
"indexes",
"paperforge.db"
);
if (!fs.existsSync(dbPath)) {
throw new Error(`PaperForge database not found at ${dbPath}`);
}
const SQL = await initSqlJs({
locateFile: (file: string) => path.join(__dirname, file),
});
const buffer = fs.readFileSync(dbPath);
_db = new SQL.Database(new Uint8Array(buffer));
_queryStmt = _db.prepare(
`SELECT zotero_key, title, first_author, year, journal, domain, abstract, rank
FROM paper_fts
WHERE paper_fts MATCH ?
ORDER BY rank
LIMIT ?`
);
}
// ── Search ──
export function searchMetadata(
query: string,
limit: number = 20
): SearchResultItem[] | null {
if (!_db || !_queryStmt) return null;
const transformed = transformFtsQuery(query);
if (!transformed) return [];
_queryStmt.bind([transformed, limit]);
const results: SearchResultItem[] = [];
while (_queryStmt.step()) {
const row = _queryStmt.getAsObject();
results.push({
zotero_key: sqlValueToString(row.zotero_key),
title: sqlValueToString(row.title),
first_author: sqlValueToString(row.first_author),
year: sqlValueToString(row.year),
journal: sqlValueToString(row.journal),
domain: sqlValueToString(row.domain),
abstract: sqlValueToString(row.abstract),
rank: sqlValueToNumber(row.rank),
});
}
_queryStmt.reset();
return results;
}

View file

@ -51,11 +51,6 @@ import {
restoreVersion,
compareVersions,
} from "../services/version-history";
import {
initDatabase,
searchMetadata,
type SearchResultItem,
} from "../services/db";
// ── Interface for plugin ref used by static open ──
@ -101,8 +96,6 @@ export class PaperForgeStatusView extends ItemView {
_searchInput: HTMLInputElement | null = null;
_searchResultsEl: HTMLElement | null = null;
_searchTimer: ReturnType<typeof setTimeout> | undefined = undefined;
_sqlJsInitialized: boolean = false;
_sqlJsFailed: boolean = false;
constructor(leaf: WorkspaceLeaf) {
super(leaf);
@ -2396,7 +2389,7 @@ export class PaperForgeStatusView extends ItemView {
cls: "paperforge-search-results",
});
// Detect @ mode prefix + debounced metadata search via sql.js
// Detect @ mode prefix + debounced metadata search
this._searchInput.addEventListener("input", () => {
const val = this._searchInput?.value || "";
if (val.startsWith("@") && !val.startsWith("@ ")) {
@ -2411,7 +2404,7 @@ export class PaperForgeStatusView extends ItemView {
// Debounced metadata search for non-@ queries
if (!val.startsWith("@") && val.trim()) {
this._searchTimer = setTimeout(() => {
this.executeSearch({ source: "sqljs" });
this.executeSearch();
}, 200);
}
});
@ -2424,12 +2417,12 @@ export class PaperForgeStatusView extends ItemView {
clearTimeout(this._searchTimer);
this._searchTimer = undefined;
}
this.executeSearch({ source: "cli" });
this.executeSearch();
}
});
}
async executeSearch(options: { source?: "auto" | "cli" | "sqljs" } = {}) {
async executeSearch() {
if (!this._searchInput || !this._searchResultsEl) return;
const raw = this._searchInput.value.trim();
if (!raw) return;
@ -2440,7 +2433,7 @@ export class PaperForgeStatusView extends ItemView {
const mode = isDeep ? "retrieve" : "search";
// Resolve vault path (used by both sql.js and CLI paths)
// Resolve vault path for CLI search
const adapter = this.app.vault.adapter;
let vaultPath = "";
if (adapter && typeof adapter === "object" && "basePath" in adapter) {
@ -2449,34 +2442,7 @@ export class PaperForgeStatusView extends ItemView {
}
this._searchResultsEl.empty();
// ── sql.js metadata search path (non-@ queries only) ──
if (
mode === "search" &&
(options.source === "auto" || options.source === "sqljs")
) {
if (vaultPath) {
try {
if (!this._sqlJsInitialized && !this._sqlJsFailed) {
await initDatabase(vaultPath);
this._sqlJsInitialized = true;
}
if (this._sqlJsInitialized) {
const results = searchMetadata(query, 20);
if (results !== null) {
this.renderSearchResults(results, false);
return;
}
}
} catch (e) {
console.error("PaperForge sql.js search failed:", e);
this._sqlJsFailed = true;
}
}
// sql.js unavailable — fall through to CLI
}
// ── CLI search path (deep search or sql.js fallback) ──
// ── CLI search path (deep search only) ──
this._searchResultsEl.createEl("div", {
cls: "paperforge-search-loading",
text: "Searching...",
@ -2515,10 +2481,10 @@ export class PaperForgeStatusView extends ItemView {
undefined,
undefined
);
const deepFlag = mode === "retrieve" ? ["--deep"] : [];
const child = spawn(
pythonExe,
[...pyExtra, "-m", "paperforge", mode, query, "--json"],
[...pyExtra, "-m", "paperforge", mode, query, ...deepFlag, "--json"],
{ cwd: vaultPath, timeout: 30000 }
);
@ -2561,11 +2527,9 @@ export class PaperForgeStatusView extends ItemView {
const d = (parsed as Record<string, unknown>).data;
if (d && typeof d === "object") {
const dd = d as Record<string, unknown>;
// search output: data.matches
// Unified PFResult v1: data.matches
if ("matches" in dd && Array.isArray(dd.matches)) {
results = dd.matches as unknown[];
} else if ("results" in dd && Array.isArray(dd.results)) {
results = dd.results as unknown[];
}
}
}
@ -2674,21 +2638,10 @@ export class PaperForgeStatusView extends ItemView {
cls: "paperforge-search-result-meta",
});
if (typeof rec["authors"] === "string") {
if (typeof rec["first_author"] === "string" && rec["first_author"]) {
meta.createEl("span", {
cls: "paperforge-search-result-author",
text: rec["authors"],
});
} else if (Array.isArray(rec["authors"])) {
meta.createEl("span", {
cls: "paperforge-search-result-author",
text: (rec["authors"] as string[]).slice(0, 3).join("; "),
});
}
if (typeof rec["year"] === "number" || typeof rec["year"] === "string") {
meta.createEl("span", {
cls: "paperforge-search-result-year",
text: String(rec["year"]),
text: rec["first_author"],
});
}
if (typeof rec["journal"] === "string" && rec["journal"]) {
@ -2727,10 +2680,10 @@ export class PaperForgeStatusView extends ItemView {
// @ mode: matched text
if (
isDeep &&
typeof rec["matched_text"] === "string" &&
rec["matched_text"]
typeof rec["text"] === "string" &&
rec["text"]
) {
const mt = rec["matched_text"] as string;
const mt = rec["text"] as string;
card.createEl("div", {
cls: "paperforge-search-result-source",
text: mt.length > 300 ? mt.slice(0, 300) + "..." : mt,