diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 2375a8311..82f4c572c 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -53,6 +53,7 @@ "hotkeys-js": "^3.8.3", "immer": "^9.0.6", "katex": "0.16.2", + "kysely": "^0.26.3", "mac-scrollbar": "^0.10.3", "marked": "^4.1.0", "pdfjs-dist": "3.6.172", @@ -24324,6 +24325,7 @@ "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { + "@leeoniya/ufuzzy": "^1.0.10", "@microsoft/signalr": "^8.0.0", "@notesnook/logger": "file:../logger", "@readme/data-urls": "^3.0.0", @@ -24334,6 +24336,7 @@ "html-to-text": "^9.0.5", "htmlparser2": "^8.0.1", "katex": "0.16.2", + "kysely": "^0.26.3", "linkedom": "^0.14.17", "liqe": "^1.13.0", "mime-db": "1.52.0", @@ -24344,6 +24347,7 @@ }, "devDependencies": { "@notesnook/crypto": "file:../crypto", + "@types/better-sqlite3": "^7.6.5", "@types/event-source-polyfill": "^1.0.1", "@types/html-to-text": "^9.0.0", "@types/katex": "^0.16.2", @@ -24354,6 +24358,8 @@ "@types/ws": "^8.5.5", "@vitest/coverage-v8": "^0.34.1", "abortcontroller-polyfill": "^1.7.3", + "better-sqlite3": "^8.6.0", + "bson-objectid": "^2.0.4", "cross-env": "^7.0.3", "dotenv": "^16.0.1", "event-source-polyfill": "^1.0.31", @@ -24362,6 +24368,7 @@ "isomorphic-fetch": "^3.0.0", "jsdom": "^22.1.0", "mockdate": "^3.0.5", + "nanoid": "^5.0.1", "otplib": "^12.0.1", "refractor": "^4.8.1", "vitest": "^0.34.1", @@ -24452,6 +24459,10 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "../../packages/core/node_modules/@leeoniya/ufuzzy": { + "version": "1.0.10", + "license": "MIT" + }, "../../packages/core/node_modules/@microsoft/signalr": { "version": "7.0.10", "license": "MIT", @@ -24569,6 +24580,14 @@ "node": ">= 10" } }, + "../../packages/core/node_modules/@types/better-sqlite3": { + "version": "7.6.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "../../packages/core/node_modules/@types/chai": { "version": "4.3.5", "dev": true, @@ -24924,6 +24943,53 @@ "dev": true, "license": "MIT" }, + "../../packages/core/node_modules/base64-js": { + "version": "1.5.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "../../packages/core/node_modules/better-sqlite3": { + "version": "8.6.0", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "../../packages/core/node_modules/bindings": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "../../packages/core/node_modules/bl": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "../../packages/core/node_modules/boolbase": { "version": "1.0.0", "license": "ISC" @@ -24937,6 +25003,34 @@ "concat-map": "0.0.1" } }, + "../../packages/core/node_modules/bson-objectid": { + "version": "2.0.4", + "dev": true, + "license": "Apache-2.0" + }, + "../../packages/core/node_modules/buffer": { + "version": "5.7.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "../../packages/core/node_modules/cac": { "version": "6.7.14", "dev": true, @@ -24997,6 +25091,11 @@ "node": "*" } }, + "../../packages/core/node_modules/chownr": { + "version": "1.1.4", + "dev": true, + "license": "ISC" + }, "../../packages/core/node_modules/combined-stream": { "version": "1.0.8", "dev": true, @@ -25141,6 +25240,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "../../packages/core/node_modules/decompress-response": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "../../packages/core/node_modules/deep-eql": { "version": "4.1.3", "dev": true, @@ -25152,6 +25265,14 @@ "node": ">=6" } }, + "../../packages/core/node_modules/deep-extend": { + "version": "0.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "../../packages/core/node_modules/deepmerge": { "version": "4.3.1", "license": "MIT", @@ -25167,6 +25288,14 @@ "node": ">=0.4.0" } }, + "../../packages/core/node_modules/detect-libc": { + "version": "2.0.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "../../packages/core/node_modules/discontinuous-range": { "version": "1.0.0", "license": "MIT" @@ -25237,6 +25366,14 @@ "node": ">=12" } }, + "../../packages/core/node_modules/end-of-stream": { + "version": "1.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "../../packages/core/node_modules/entities": { "version": "4.5.0", "license": "BSD-2-Clause", @@ -25302,6 +25439,14 @@ "node": ">=12.0.0" } }, + "../../packages/core/node_modules/expand-template": { + "version": "2.0.3", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "../../packages/core/node_modules/fetch-cookie": { "version": "2.1.0", "license": "Unlicense", @@ -25310,6 +25455,11 @@ "tough-cookie": "^4.0.0" } }, + "../../packages/core/node_modules/file-uri-to-path": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, "../../packages/core/node_modules/form-data": { "version": "4.0.0", "dev": true, @@ -25323,6 +25473,11 @@ "node": ">= 6" } }, + "../../packages/core/node_modules/fs-constants": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, "../../packages/core/node_modules/fs.realpath": { "version": "1.0.0", "dev": true, @@ -25336,6 +25491,11 @@ "node": "*" } }, + "../../packages/core/node_modules/github-from-package": { + "version": "0.0.0", + "dev": true, + "license": "MIT" + }, "../../packages/core/node_modules/glob": { "version": "7.2.3", "dev": true, @@ -25479,6 +25639,25 @@ "node": ">=0.10.0" } }, + "../../packages/core/node_modules/ieee754": { + "version": "1.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "../../packages/core/node_modules/inflight": { "version": "1.0.6", "dev": true, @@ -25493,6 +25672,11 @@ "dev": true, "license": "ISC" }, + "../../packages/core/node_modules/ini": { + "version": "1.3.8", + "dev": true, + "license": "ISC" + }, "../../packages/core/node_modules/is-alphabetical": { "version": "2.0.1", "dev": true, @@ -25698,6 +25882,13 @@ "node": ">= 12" } }, + "../../packages/core/node_modules/kysely": { + "version": "0.26.3", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "../../packages/core/node_modules/leac": { "version": "0.6.0", "license": "MIT", @@ -25804,6 +25995,17 @@ "node": ">= 0.6" } }, + "../../packages/core/node_modules/mimic-response": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "../../packages/core/node_modules/minimatch": { "version": "3.1.2", "dev": true, @@ -25815,6 +26017,19 @@ "node": "*" } }, + "../../packages/core/node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../packages/core/node_modules/mkdirp-classic": { + "version": "0.5.3", + "dev": true, + "license": "MIT" + }, "../../packages/core/node_modules/mlly": { "version": "1.4.0", "dev": true, @@ -25841,7 +26056,7 @@ "license": "MIT" }, "../../packages/core/node_modules/nanoid": { - "version": "3.3.6", + "version": "5.0.1", "dev": true, "funding": [ { @@ -25851,12 +26066,17 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, + "../../packages/core/node_modules/napi-build-utils": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, "../../packages/core/node_modules/nearley": { "version": "2.20.1", "license": "MIT", @@ -25881,6 +26101,17 @@ "version": "2.20.3", "license": "MIT" }, + "../../packages/core/node_modules/node-abi": { + "version": "3.47.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "../../packages/core/node_modules/node-fetch": { "version": "2.6.7", "license": "MIT", @@ -26067,6 +26298,48 @@ "node": "^10 || ^12 || >=14" } }, + "../../packages/core/node_modules/postcss/node_modules/nanoid": { + "version": "3.3.6", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "../../packages/core/node_modules/prebuild-install": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "../../packages/core/node_modules/prismjs": { "version": "1.29.0", "license": "MIT", @@ -26087,6 +26360,15 @@ "version": "1.9.0", "license": "MIT" }, + "../../packages/core/node_modules/pump": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "../../packages/core/node_modules/punycode": { "version": "2.3.1", "license": "MIT", @@ -26117,11 +26399,38 @@ "node": ">=0.12" } }, + "../../packages/core/node_modules/rc": { + "version": "1.2.8", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "../../packages/core/node_modules/react-is": { "version": "18.2.0", "dev": true, "license": "MIT" }, + "../../packages/core/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "../../packages/core/node_modules/refractor": { "version": "4.8.1", "dev": true, @@ -26172,6 +26481,25 @@ "dev": true, "license": "MIT" }, + "../../packages/core/node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "../../packages/core/node_modules/safer-buffer": { "version": "2.1.2", "dev": true, @@ -26240,6 +26568,49 @@ "dev": true, "license": "ISC" }, + "../../packages/core/node_modules/simple-concat": { + "version": "1.0.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "../../packages/core/node_modules/simple-get": { + "version": "4.0.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "../../packages/core/node_modules/source-map": { "version": "0.6.1", "dev": true, @@ -26279,6 +26650,22 @@ "dev": true, "license": "MIT" }, + "../../packages/core/node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "../../packages/core/node_modules/strip-json-comments": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "../../packages/core/node_modules/strip-literal": { "version": "1.3.0", "dev": true, @@ -26306,6 +26693,32 @@ "dev": true, "license": "MIT" }, + "../../packages/core/node_modules/tar-fs": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "../../packages/core/node_modules/tar-stream": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "../../packages/core/node_modules/test-exclude": { "version": "6.0.0", "dev": true, @@ -26379,6 +26792,17 @@ "version": "2.4.1", "license": "0BSD" }, + "../../packages/core/node_modules/tunnel-agent": { + "version": "0.6.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "../../packages/core/node_modules/type-detect": { "version": "4.0.8", "dev": true, @@ -26411,6 +26835,11 @@ "requires-port": "^1.0.0" } }, + "../../packages/core/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, "../../packages/core/node_modules/v8-to-istanbul": { "version": "9.1.0", "dev": true, @@ -44025,6 +44454,14 @@ "node": ">=6" } }, + "node_modules/kysely": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.26.3.tgz", + "integrity": "sha512-yWSgGi9bY13b/W06DD2OCDDHQmq1kwTGYlQ4wpZkMOJqMGCstVCFIvxCCVG4KfY1/3G0MhDAcZsip/Lw8/vJWw==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/leven": { "version": "3.1.0", "dev": true, diff --git a/apps/web/package.json b/apps/web/package.json index f8a889c39..989f543e2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -52,6 +52,7 @@ "hotkeys-js": "^3.8.3", "immer": "^9.0.6", "katex": "0.16.2", + "kysely": "^0.26.3", "mac-scrollbar": "^0.10.3", "marked": "^4.1.0", "pdfjs-dist": "3.6.172", diff --git a/apps/web/src/app-effects.tsx b/apps/web/src/app-effects.tsx index 68bdf6616..7eac8fd0c 100644 --- a/apps/web/src/app-effects.tsx +++ b/apps/web/src/app-effects.tsx @@ -83,10 +83,10 @@ export default function AppEffects({ setShow }: AppEffectsProps) { initStore(); initAttachments(); - refreshNavItems(); initEditorStore(); (async function () { + await refreshNavItems(); await updateLastSynced(); if (await initUser()) { showUpgradeReminderDialogs(); diff --git a/apps/web/src/common/db.ts b/apps/web/src/common/db.ts index a6b16dd21..1ecf6d8c9 100644 --- a/apps/web/src/common/db.ts +++ b/apps/web/src/common/db.ts @@ -22,6 +22,12 @@ import { DatabasePersistence, NNStorage } from "../interfaces/storage"; import { logger } from "../utils/logger"; import type Database from "@notesnook/core/dist/api"; import { showMigrationDialog } from "./dialog-controller"; +// import { SQLocalKysely } from "sqlocal/kysely"; +import { WaSqliteWorkerDriver } from "./sqlite/sqlite.kysely"; +import { SqliteAdapter, SqliteQueryCompiler, SqliteIntrospector } from "kysely"; +// import SQLiteESMFactory from "./sqlite/wa-sqlite-async"; +// import * as SQLite from "./sqlite/sqlite-api"; +// import { IDBBatchAtomicVFS } from "./sqlite/IDBBatchAtomicVFS"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -34,6 +40,13 @@ async function initializeDatabase(persistence: DatabasePersistence) { const { Compressor } = await import("../utils/compressor"); db = database; + // // const ss = wrap(new Worker()); + // // await ss.init("test.db", false, uri); + + // const res = await ss.run("query", `.table`); + // console.log(res); + // await ss.close(); + db.host({ API_HOST: "https://api.notesnook.com", AUTH_HOST: "https://auth.streetwriters.co", @@ -43,10 +56,25 @@ async function initializeDatabase(persistence: DatabasePersistence) { }); database.setup({ + sqliteOptions: { + dialect: { + createDriver: () => + new WaSqliteWorkerDriver({ async: true, dbName: "test.db" }), + createAdapter: () => new SqliteAdapter(), + createIntrospector: (db) => new SqliteIntrospector(db), + createQueryCompiler: () => new SqliteQueryCompiler() + }, + journalMode: "MEMORY", + synchronous: "normal", + pageSize: 8192, + cacheSize: -16000, + lockingMode: "exclusive" + }, storage: await NNStorage.createInstance("Notesnook", persistence), eventsource: EventSource, fs: FileStorage, - compressor: new Compressor() + compressor: new Compressor(), + batchSize: 500 }); // if (IS_TESTING) { diff --git a/apps/web/src/common/export.ts b/apps/web/src/common/export.ts index b0b2ee793..b3b31160c 100644 --- a/apps/web/src/common/export.ts +++ b/apps/web/src/common/export.ts @@ -159,7 +159,8 @@ export async function exportNote( const exported = await db.notes .export(note.id, { format: format === "pdf" ? "html" : format, - contentItem: content + contentItem: content, + disableTemplate }) .catch((e: Error) => { console.error(note, e); diff --git a/apps/web/src/common/multi-select.ts b/apps/web/src/common/multi-select.ts index 6393d55ca..358abb09a 100644 --- a/apps/web/src/common/multi-select.ts +++ b/apps/web/src/common/multi-select.ts @@ -34,14 +34,14 @@ type Item = { metadata?: Record; }; -async function moveNotesToTrash(notes: Item[], confirm = true) { - if (confirm && !(await showMultiDeleteConfirmation(notes.length))) return; - if (notes.some((n) => n.locked) && !(await Vault.unlockVault())) return; +async function moveNotesToTrash(ids: string[], confirm = true) { + if (confirm && !(await showMultiDeleteConfirmation(ids.length))) return; - const items = notes.map((item) => { - if (db.monographs?.isPublished(item.id)) return 0; - return item.id; - }); + const lockedIds = await db.notes.locked.ids(); + if (ids.some((id) => lockedIds.includes(id)) && !(await Vault.unlockVault())) + return; + + const items = ids.filter((id) => !db.monographs.isPublished(id)); await TaskManager.startTask({ type: "status", @@ -57,10 +57,10 @@ async function moveNotesToTrash(notes: Item[], confirm = true) { showToast("success", `${pluralize(items.length, "note")} moved to trash`); } -async function moveNotebooksToTrash(notebooks: Item[]) { - const isMultiselect = notebooks.length > 1; +async function moveNotebooksToTrash(ids: string[]) { + const isMultiselect = ids.length > 1; if (isMultiselect) { - if (!(await showMultiDeleteConfirmation(notebooks.length))) return; + if (!(await showMultiDeleteConfirmation(ids.length))) return; } await TaskManager.startTask({ @@ -68,16 +68,13 @@ async function moveNotebooksToTrash(notebooks: Item[]) { id: "deleteNotebooks", action: async (report) => { report({ - text: `Deleting ${pluralize(notebooks.length, "notebook")}...` + text: `Deleting ${pluralize(ids.length, "notebook")}...` }); - await notebookStore.delete(...notebooks.map((i) => i.id)); + await notebookStore.delete(...ids); } }); - showToast( - "success", - `${pluralize(notebooks.length, "notebook")} moved to trash` - ); + showToast("success", `${pluralize(ids.length, "notebook")} moved to trash`); } async function deleteTopics(notebookId: string, topics: Item[]) { diff --git a/apps/web/src/common/sqlite/AccessHandlePoolVFS.js b/apps/web/src/common/sqlite/AccessHandlePoolVFS.js new file mode 100644 index 000000000..dc1543fad --- /dev/null +++ b/apps/web/src/common/sqlite/AccessHandlePoolVFS.js @@ -0,0 +1,462 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +// Copyright 2023 Roy T. Hashimoto. All Rights Reserved. +import * as VFS from "./VFS.js"; + +const SECTOR_SIZE = 4096; + +// Each OPFS file begins with a fixed-size header with metadata. The +// contents of the file follow immediately after the header. +const HEADER_MAX_PATH_SIZE = 512; +const HEADER_FLAGS_SIZE = 4; +const HEADER_DIGEST_SIZE = 8; +const HEADER_CORPUS_SIZE = HEADER_MAX_PATH_SIZE + HEADER_FLAGS_SIZE; +const HEADER_OFFSET_FLAGS = HEADER_MAX_PATH_SIZE; +const HEADER_OFFSET_DIGEST = HEADER_CORPUS_SIZE; +const HEADER_OFFSET_DATA = SECTOR_SIZE; + +// These file types are expected to persist in the file system outside +// a session. Other files will be removed on VFS start. +const PERSISTENT_FILE_TYPES = + VFS.SQLITE_OPEN_MAIN_DB | + VFS.SQLITE_OPEN_MAIN_JOURNAL | + VFS.SQLITE_OPEN_SUPER_JOURNAL | + VFS.SQLITE_OPEN_WAL; + +const DEFAULT_CAPACITY = 6; + +function log(...args) { + // console.debug(...args); +} + +/** + * This VFS uses the updated Access Handle API with all synchronous methods + * on FileSystemSyncAccessHandle (instead of just read and write). It will + * work with the regular SQLite WebAssembly build, i.e. the one without + * Asyncify. + */ +export class AccessHandlePoolVFS extends VFS.Base { + // All the OPFS files the VFS uses are contained in one flat directory + // specified in the constructor. No other files should be written here. + #directoryPath; + #directoryHandle; + + // The OPFS files all have randomly-generated names that do not match + // the SQLite files whose data they contain. This map links those names + // with their respective OPFS access handles. + #mapAccessHandleToName = new Map(); + + // When a SQLite file is associated with an OPFS file, that association + // is kept in #mapPathToAccessHandle. Each access handle is in exactly + // one of #mapPathToAccessHandle or #availableAccessHandles. + #mapPathToAccessHandle = new Map(); + #availableAccessHandles = new Set(); + + #mapIdToFile = new Map(); + + constructor(directoryPath) { + super(); + this.#directoryPath = directoryPath; + this.isReady = this.reset().then(async () => { + if (this.getCapacity() === 0) { + await this.addCapacity(DEFAULT_CAPACITY); + } + }); + } + + get name() { + return "AccessHandlePool"; + } + + xOpen(name, fileId, flags, pOutFlags) { + log(`xOpen ${name} ${fileId} 0x${flags.toString(16)}`); + try { + // First try to open a path that already exists in the file system. + const path = name ? this.#getPath(name) : Math.random().toString(36); + let accessHandle = this.#mapPathToAccessHandle.get(path); + if (!accessHandle && flags & VFS.SQLITE_OPEN_CREATE) { + // File not found so try to create it. + if (this.getSize() < this.getCapacity()) { + // Choose an unassociated OPFS file from the pool. + [accessHandle] = this.#availableAccessHandles.keys(); + this.#setAssociatedPath(accessHandle, path, flags); + } else { + // Out of unassociated files. This can be fixed by calling + // addCapacity() from the application. + throw new Error("cannot create file"); + } + } + if (!accessHandle) { + throw new Error("file not found"); + } + // Subsequent methods are only passed the fileId, so make sure we have + // a way to get the file resources. + const file = { path, flags, accessHandle }; + this.#mapIdToFile.set(fileId, file); + + pOutFlags.setInt32(0, flags, true); + return VFS.SQLITE_OK; + } catch (e) { + console.error(e.message); + return VFS.SQLITE_CANTOPEN; + } + } + + xClose(fileId) { + const file = this.#mapIdToFile.get(fileId); + if (file) { + log(`xClose ${file.path}`); + + file.accessHandle.flush(); + this.#mapIdToFile.delete(fileId); + if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) { + this.#deletePath(file.path); + } + } + return VFS.SQLITE_OK; + } + + xRead(fileId, pData, iOffset) { + const file = this.#mapIdToFile.get(fileId); + log(`xRead ${file.path} ${pData.byteLength} ${iOffset}`); + + const nBytes = file.accessHandle.read(pData, { + at: HEADER_OFFSET_DATA + iOffset + }); + if (nBytes < pData.byteLength) { + pData.fill(0, nBytes, pData.byteLength); + return VFS.SQLITE_IOERR_SHORT_READ; + } + return VFS.SQLITE_OK; + } + + xWrite(fileId, pData, iOffset) { + const file = this.#mapIdToFile.get(fileId); + log(`xWrite ${file.path} ${pData.byteLength} ${iOffset}`); + + const nBytes = file.accessHandle.write(pData, { + at: HEADER_OFFSET_DATA + iOffset + }); + return nBytes === pData.byteLength ? VFS.SQLITE_OK : VFS.SQLITE_IOERR; + } + + xTruncate(fileId, iSize) { + const file = this.#mapIdToFile.get(fileId); + log(`xTruncate ${file.path} ${iSize}`); + + file.accessHandle.truncate(HEADER_OFFSET_DATA + iSize); + return VFS.SQLITE_OK; + } + + xSync(fileId, flags) { + const file = this.#mapIdToFile.get(fileId); + log(`xSync ${file.path} ${flags}`); + + file.accessHandle.flush(); + return VFS.SQLITE_OK; + } + + xFileSize(fileId, pSize64) { + const file = this.#mapIdToFile.get(fileId); + const size = file.accessHandle.getSize() - HEADER_OFFSET_DATA; + log(`xFileSize ${file.path} ${size}`); + pSize64.setBigInt64(0, BigInt(size), true); + return VFS.SQLITE_OK; + } + + xSectorSize(fileId) { + log("xSectorSize", SECTOR_SIZE); + return SECTOR_SIZE; + } + + xDeviceCharacteristics(fileId) { + log("xDeviceCharacteristics"); + return VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN; + } + + xAccess(name, flags, pResOut) { + log(`xAccess ${name} ${flags}`); + const path = this.#getPath(name); + pResOut.setInt32(0, this.#mapPathToAccessHandle.has(path) ? 1 : 0, true); + return VFS.SQLITE_OK; + } + + xDelete(name, syncDir) { + log(`xDelete ${name} ${syncDir}`); + const path = this.#getPath(name); + this.#deletePath(path); + return VFS.SQLITE_OK; + } + + async close() { + await this.#releaseAccessHandles(); + } + + /** + * Release and reacquire all OPFS access handles. This must be called + * and awaited before any SQLite call that uses the VFS and also before + * any capacity changes. + */ + async reset() { + await this.isReady; + + // All files are stored in a single directory. + let handle = await navigator.storage.getDirectory(); + for (const d of this.#directoryPath.split("/")) { + if (d) { + handle = await handle.getDirectoryHandle(d, { create: true }); + } + } + this.#directoryHandle = handle; + + await this.#releaseAccessHandles(); + await this.#acquireAccessHandles(); + } + + /** + * Returns the number of SQLite files in the file system. + * @returns {number} + */ + getSize() { + return this.#mapPathToAccessHandle.size; + } + + /** + * Returns the maximum number of SQLite files the file system can hold. + * @returns {number} + */ + getCapacity() { + return this.#mapAccessHandleToName.size; + } + + /** + * Increase the capacity of the file system by n. + * @param {number} n + * @returns {Promise} + */ + async addCapacity(n) { + for (let i = 0; i < n; ++i) { + const name = Math.random().toString(36).replace("0.", ""); + const handle = await this.#directoryHandle.getFileHandle(name, { + create: true + }); + const accessHandle = await handle.createSyncAccessHandle(); + this.#mapAccessHandleToName.set(accessHandle, name); + + this.#setAssociatedPath(accessHandle, "", 0); + } + return n; + } + + /** + * Decrease the capacity of the file system by n. The capacity cannot be + * decreased to fewer than the current number of SQLite files in the + * file system. + * @param {number} n + * @returns {Promise} + */ + async removeCapacity(n) { + let nRemoved = 0; + for (const accessHandle of Array.from(this.#availableAccessHandles)) { + if (nRemoved == n || this.getSize() === this.getCapacity()) + return nRemoved; + + const name = this.#mapAccessHandleToName.get(accessHandle); + await accessHandle.close(); + await this.#directoryHandle.removeEntry(name); + this.#mapAccessHandleToName.delete(accessHandle); + this.#availableAccessHandles.delete(accessHandle); + ++nRemoved; + } + return nRemoved; + } + + async #acquireAccessHandles() { + // Enumerate all the files in the directory. + const files = []; + for await (const [name, handle] of this.#directoryHandle) { + if (handle.kind === "file") { + files.push([name, handle]); + } + } + + // Open access handles in parallel, separating associated and unassociated. + await Promise.all( + files.map(async ([name, handle]) => { + const accessHandle = await handle.createSyncAccessHandle(); + this.#mapAccessHandleToName.set(accessHandle, name); + const path = this.#getAssociatedPath(accessHandle); + if (path) { + this.#mapPathToAccessHandle.set(path, accessHandle); + } else { + this.#availableAccessHandles.add(accessHandle); + } + }) + ); + } + + #releaseAccessHandles() { + for (const accessHandle of this.#mapAccessHandleToName.keys()) { + accessHandle.close(); + } + this.#mapAccessHandleToName.clear(); + this.#mapPathToAccessHandle.clear(); + this.#availableAccessHandles.clear(); + } + + /** + * Read and return the associated path from an OPFS file header. + * Empty string is returned for an unassociated OPFS file. + * @param accessHandle FileSystemSyncAccessHandle + * @returns {string} path or empty string + */ + #getAssociatedPath(accessHandle) { + // Read the path and digest of the path from the file. + const corpus = new Uint8Array(HEADER_CORPUS_SIZE); + accessHandle.read(corpus, { at: 0 }); + + // Delete files not expected to be present. + const dataView = new DataView(corpus.buffer, corpus.byteOffset); + const flags = dataView.getUint32(HEADER_OFFSET_FLAGS); + if ( + corpus[0] && + (flags & VFS.SQLITE_OPEN_DELETEONCLOSE || + (flags & PERSISTENT_FILE_TYPES) === 0) + ) { + console.warn(`Remove file with unexpected flags ${flags.toString(16)}`); + this.#setAssociatedPath(accessHandle, "", 0); + return ""; + } + + const fileDigest = new Uint32Array(HEADER_DIGEST_SIZE / 4); + accessHandle.read(fileDigest, { at: HEADER_OFFSET_DIGEST }); + + // Verify the digest. + const computedDigest = this.#computeDigest(corpus); + if (fileDigest.every((value, i) => value === computedDigest[i])) { + // Good digest. Decode the null-terminated path string. + const pathBytes = corpus.findIndex((value) => value === 0); + if (pathBytes === 0) { + // Ensure that unassociated files are empty. Unassociated files are + // truncated in #setAssociatedPath after the header is written. If + // an interruption occurs right before the truncation then garbage + // may remain in the file. + accessHandle.truncate(HEADER_OFFSET_DATA); + } + return new TextDecoder().decode(corpus.subarray(0, pathBytes)); + } else { + // Bad digest. Repair this header. + console.warn("Disassociating file with bad digest."); + this.#setAssociatedPath(accessHandle, "", 0); + return ""; + } + } + + /** + * Set the path on an OPFS file header. + * @param accessHandle FileSystemSyncAccessHandle + * @param {string} path + * @param {number} flags + */ + #setAssociatedPath(accessHandle, path, flags) { + // Convert the path string to UTF-8. + const corpus = new Uint8Array(HEADER_CORPUS_SIZE); + const encodedResult = new TextEncoder().encodeInto(path, corpus); + if (encodedResult.written >= HEADER_MAX_PATH_SIZE) { + throw new Error("path too long"); + } + + // Add the creation flags. + const dataView = new DataView(corpus.buffer, corpus.byteOffset); + dataView.setUint32(HEADER_OFFSET_FLAGS, flags); + + // Write the OPFS file header, including the digest. + const digest = this.#computeDigest(corpus); + accessHandle.write(corpus, { at: 0 }); + accessHandle.write(digest, { at: HEADER_OFFSET_DIGEST }); + accessHandle.flush(); + + if (path) { + this.#mapPathToAccessHandle.set(path, accessHandle); + this.#availableAccessHandles.delete(accessHandle); + } else { + // This OPFS file doesn't represent any SQLite file so it doesn't + // need to keep any data. + accessHandle.truncate(HEADER_OFFSET_DATA); + this.#availableAccessHandles.add(accessHandle); + } + } + + /** + * We need a synchronous digest function so can't use WebCrypto. + * Adapted from https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js + * @param {Uint8Array} corpus + * @returns {ArrayBuffer} 64-bit digest + */ + #computeDigest(corpus) { + if (!corpus[0]) { + // Optimization for deleted file. + return new Uint32Array([0xfecc5f80, 0xaccec037]); + } + + let h1 = 0xdeadbeef; + let h2 = 0x41c6ce57; + + for (const value of corpus) { + h1 = Math.imul(h1 ^ value, 2654435761); + h2 = Math.imul(h2 ^ value, 1597334677); + } + + h1 = + Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ + Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = + Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ + Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + return new Uint32Array([h1 >>> 0, h2 >>> 0]); + } + + /** + * Convert a bare filename, path, or URL to a UNIX-style path. + * @param {string|URL} nameOrURL + * @returns {string} path + */ + #getPath(nameOrURL) { + const url = + typeof nameOrURL === "string" + ? new URL(nameOrURL, "file://localhost/") + : nameOrURL; + return url.pathname; + } + + /** + * Remove the association between a path and an OPFS file. + * @param {string} path + */ + #deletePath(path) { + const accessHandle = this.#mapPathToAccessHandle.get(path); + if (accessHandle) { + // Un-associate the SQLite path from the OPFS file. + this.#mapPathToAccessHandle.delete(path); + this.#setAssociatedPath(accessHandle, "", 0); + } + } +} diff --git a/apps/web/src/common/sqlite/IDBBatchAtomicVFS.js b/apps/web/src/common/sqlite/IDBBatchAtomicVFS.js new file mode 100644 index 000000000..945f3635a --- /dev/null +++ b/apps/web/src/common/sqlite/IDBBatchAtomicVFS.js @@ -0,0 +1,855 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +// Copyright 2022 Roy T. Hashimoto. All Rights Reserved. +import * as VFS from "./VFS.js"; +import { WebLocksExclusive as WebLocks } from "./WebLocks.js"; +import { IDBContext } from "./IDBContext.js"; + +const SECTOR_SIZE = 512; +const MAX_TASK_MILLIS = 3000; + +/** + * @typedef VFSOptions + * @property {"default"|"strict"|"relaxed"} [durability] + * @property {"deferred"|"manual"} [purge] + * @property {number} [purgeAtLeast] + */ + +/** @type {VFSOptions} */ +const DEFAULT_OPTIONS = { + durability: "default", + purge: "deferred", + purgeAtLeast: 16 +}; + +function log(...args) { + // console.log(...args); +} + +/** + * @typedef FileBlock IndexedDB object with key [path, offset, version] + * @property {string} path + * @property {number} offset negative of position in file + * @property {number} version + * @property {Uint8Array} data + * + * @property {number} [fileSize] Only present on block 0 + */ + +/** + * @typedef OpenedFileEntry + * @property {string} path + * @property {number} flags + * @property {FileBlock} block0 + * @property {WebLocks} locks + * + * @property {Set} [changedPages] + * @property {boolean} [overwrite] + */ + +// This sample VFS stores optionally versioned writes to IndexedDB, which +// it uses with the SQLite xFileControl() batch atomic write feature. +export class IDBBatchAtomicVFS extends VFS.Base { + #options; + /** @type {Map} */ #mapIdToFile = new Map(); + + /** @type {IDBContext} */ #idb; + /** @type {Set} */ #pendingPurges = new Set(); + + #taskTimestamp = performance.now(); + #pendingAsync = new Set(); + + constructor(idbDatabaseName = "wa-sqlite", options = DEFAULT_OPTIONS) { + super(); + this.name = idbDatabaseName; + this.#options = Object.assign({}, DEFAULT_OPTIONS, options); + this.#idb = new IDBContext(openDatabase(idbDatabaseName), { + durability: this.#options.durability + }); + } + + async close() { + for (const fileId of this.#mapIdToFile.keys()) { + await this.xClose(fileId); + } + + await this.#idb?.close(); + this.#idb = null; + } + + /** + * @param {string?} name + * @param {number} fileId + * @param {number} flags + * @param {DataView} pOutFlags + * @returns {number} + */ + xOpen(name, fileId, flags, pOutFlags) { + return this.handleAsync(async () => { + if (name === null) name = `null_${fileId}`; + log(`xOpen ${name} 0x${fileId.toString(16)} 0x${flags.toString(16)}`); + + try { + // Filenames can be URLs, possibly with query parameters. + const url = new URL(name, "http://localhost/"); + /** @type {OpenedFileEntry} */ const file = { + path: url.pathname, + flags, + block0: null, + locks: new WebLocks(url.pathname) + }; + this.#mapIdToFile.set(fileId, file); + + // Read the first block, which also contains the file metadata. + await this.#idb.run("readwrite", async ({ blocks }) => { + file.block0 = await blocks.get(this.#bound(file, 0)); + if (!file.block0) { + if (flags & VFS.SQLITE_OPEN_CREATE) { + file.block0 = { + path: file.path, + offset: 0, + version: 0, + data: new Uint8Array(0), + fileSize: 0 + }; + blocks.put(file.block0); + } else { + throw new Error(`file not found: ${file.path}`); + } + } + }); + pOutFlags.setInt32(0, flags & VFS.SQLITE_OPEN_READONLY, true); + return VFS.SQLITE_OK; + } catch (e) { + console.error(e); + return VFS.SQLITE_CANTOPEN; + } + }); + } + + /** + * @param {number} fileId + * @returns {number} + */ + xClose(fileId) { + return this.handleAsync(async () => { + try { + const file = this.#mapIdToFile.get(fileId); + if (file) { + log(`xClose ${file.path}`); + + this.#mapIdToFile.delete(fileId); + if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) { + this.#idb.run("readwrite", ({ blocks }) => { + blocks.delete(IDBKeyRange.bound([file.path], [file.path, []])); + }); + } + } + return VFS.SQLITE_OK; + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + }); + } + + /** + * @param {number} fileId + * @param {Uint8Array} pData + * @param {number} iOffset + * @returns {number} + */ + xRead(fileId, pData, iOffset) { + return this.handleAsync(async () => { + const file = this.#mapIdToFile.get(fileId); + log(`xRead ${file.path} ${pData.byteLength} ${iOffset}`); + + try { + // Read as many blocks as necessary to satisfy the read request. + // Usually a read fits within a single write but there is at least + // one case - rollback after journal spill - where reads cross + // write boundaries so we have to allow for that. + const result = await this.#idb.run("readonly", async ({ blocks }) => { + let pDataOffset = 0; + while (pDataOffset < pData.byteLength) { + // Fetch the IndexedDB block for this file location. + const fileOffset = iOffset + pDataOffset; + /** @type {FileBlock} */ + const block = + fileOffset < file.block0.data.byteLength + ? file.block0 + : await blocks.get(this.#bound(file, -fileOffset)); + + if (!block || block.data.byteLength - block.offset <= fileOffset) { + pData.fill(0, pDataOffset); + return VFS.SQLITE_IOERR_SHORT_READ; + } + + const buffer = pData.subarray(pDataOffset); + const blockOffset = fileOffset + block.offset; + const nBytesToCopy = Math.min( + Math.max(block.data.byteLength - blockOffset, 0), // source bytes + buffer.byteLength + ); // destination bytes + buffer.set( + block.data.subarray(blockOffset, blockOffset + nBytesToCopy) + ); + pDataOffset += nBytesToCopy; + } + return VFS.SQLITE_OK; + }); + return result; + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + }); + } + + /** + * @param {number} fileId + * @param {Uint8Array} pData + * @param {number} iOffset + * @returns {number} + */ + xWrite(fileId, pData, iOffset) { + // Handle asynchronously every MAX_TASK_MILLIS milliseconds. This is + // tricky because Asyncify calls asynchronous methods twice: once + // to initiate the call and unwinds the stack, then rewinds the + // stack and calls again to retrieve the completed result. + const rewound = this.#pendingAsync.has(fileId); + if (rewound || performance.now() - this.#taskTimestamp > MAX_TASK_MILLIS) { + const result = this.handleAsync(async () => { + if (this.handleAsync !== super.handleAsync) { + this.#pendingAsync.add(fileId); + } + await new Promise((resolve) => setTimeout(resolve)); + + const result = this.#xWriteHelper(fileId, pData, iOffset); + this.#taskTimestamp = performance.now(); + return result; + }); + + if (rewound) this.#pendingAsync.delete(fileId); + return result; + } + return this.#xWriteHelper(fileId, pData, iOffset); + } + + /** + * @param {number} fileId + * @param {Uint8Array} pData + * @param {number} iOffset + * @returns {number} + */ + #xWriteHelper(fileId, pData, iOffset) { + const file = this.#mapIdToFile.get(fileId); + log(`xWrite ${file.path} ${pData.byteLength} ${iOffset}`); + try { + // Convert the write directly into an IndexedDB object. Our assumption + // is that SQLite will only overwrite data with an xWrite of the same + // offset and size unless the database page size changes, except when + // changing database page size which is handled by #reblockIfNeeded(). + const prevFileSize = file.block0.fileSize; + file.block0.fileSize = Math.max( + file.block0.fileSize, + iOffset + pData.byteLength + ); + const block = + iOffset === 0 + ? file.block0 + : { + path: file.path, + offset: -iOffset, + version: file.block0.version, + data: null + }; + block.data = pData.slice(); + + if (file.changedPages) { + // This write is part of a batch atomic write. All writes in the + // batch have a new version, so update the changed list to allow + // old versions to be eventually deleted. + if (prevFileSize === file.block0.fileSize) { + file.changedPages.add(-iOffset); + } + + // Defer writing block 0 to IndexedDB until batch commit. + if (iOffset !== 0) { + this.#idb.run("readwrite", ({ blocks }) => blocks.put(block)); + } + } else { + // Not a batch atomic write so write through. + this.#idb.run("readwrite", ({ blocks }) => blocks.put(block)); + } + return VFS.SQLITE_OK; + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + } + + /** + * @param {number} fileId + * @param {number} iSize + * @returns {number} + */ + xTruncate(fileId, iSize) { + const file = this.#mapIdToFile.get(fileId); + log(`xTruncate ${file.path} ${iSize}`); + + try { + Object.assign(file.block0, { + fileSize: iSize, + data: file.block0.data.slice(0, iSize) + }); + + // Delete all blocks beyond the file size and update metadata. + // This is never called within a transaction. + const block0 = Object.assign({}, file.block0); + this.#idb.run("readwrite", ({ blocks }) => { + blocks.delete(this.#bound(file, -Infinity, -iSize)); + blocks.put(block0); + }); + return VFS.SQLITE_OK; + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + } + + /** + * @param {number} fileId + * @param {number} flags + * @returns {number} + */ + xSync(fileId, flags) { + // Skip IndexedDB sync if durability is relaxed and the last + // sync was recent enough. + const rewound = this.#pendingAsync.has(fileId); + if ( + rewound || + this.#options.durability !== "relaxed" || + performance.now() - this.#taskTimestamp > MAX_TASK_MILLIS + ) { + const result = this.handleAsync(async () => { + if (this.handleAsync !== super.handleAsync) { + this.#pendingAsync.add(fileId); + } + + const result = await this.#xSyncHelper(fileId, flags); + this.#taskTimestamp = performance.now(); + return result; + }); + + if (rewound) this.#pendingAsync.delete(fileId); + return result; + } + + const file = this.#mapIdToFile.get(fileId); + log(`xSync ${file.path} ${flags}`); + return VFS.SQLITE_OK; + } + + /** + * @param {number} fileId + * @param {number} flags + * @returns {Promise} + */ + async #xSyncHelper(fileId, flags) { + const file = this.#mapIdToFile.get(fileId); + log(`xSync ${file.path} ${flags}`); + try { + await this.#idb.sync(); + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + return VFS.SQLITE_OK; + } + + /** + * @param {number} fileId + * @param {DataView} pSize64 + * @returns {number} + */ + xFileSize(fileId, pSize64) { + const file = this.#mapIdToFile.get(fileId); + log(`xFileSize ${file.path}`); + + pSize64.setBigInt64(0, BigInt(file.block0.fileSize), true); + return VFS.SQLITE_OK; + } + + /** + * @param {number} fileId + * @param {number} flags + * @returns {number} + */ + xLock(fileId, flags) { + return this.handleAsync(async () => { + const file = this.#mapIdToFile.get(fileId); + log(`xLock ${file.path} ${flags}`); + try { + // Acquire the lock. + const result = await file.locks.lock(flags); + if ( + result === VFS.SQLITE_OK && + file.locks.state === VFS.SQLITE_LOCK_SHARED + ) { + // Update block 0 in case another connection changed it. + file.block0 = await this.#idb.run("readonly", ({ blocks }) => { + return blocks.get(this.#bound(file, 0)); + }); + } + return result; + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + }); + } + + /** + * @param {number} fileId + * @param {number} flags + * @returns {number} + */ + xUnlock(fileId, flags) { + return this.handleAsync(async () => { + const file = this.#mapIdToFile.get(fileId); + log(`xUnlock ${file.path} ${flags}`); + try { + return file.locks.unlock(flags); + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + }); + } + + /** + * @param {number} fileId + * @param {DataView} pResOut + * @returns {number} + */ + xCheckReservedLock(fileId, pResOut) { + return this.handleAsync(async () => { + const file = this.#mapIdToFile.get(fileId); + log(`xCheckReservedLock ${file.path}`); + + const isReserved = await file.locks.isSomewhereReserved(); + pResOut.setInt32(0, isReserved ? 1 : 0, true); + return VFS.SQLITE_OK; + }); + } + + /** + * @param {number} fileId + * @returns {number} + */ + xSectorSize(fileId) { + log("xSectorSize"); + return SECTOR_SIZE; + } + + /** + * @param {number} fileId + * @returns {number} + */ + xDeviceCharacteristics(fileId) { + log("xDeviceCharacteristics"); + return ( + VFS.SQLITE_IOCAP_BATCH_ATOMIC | + VFS.SQLITE_IOCAP_SAFE_APPEND | + VFS.SQLITE_IOCAP_SEQUENTIAL | + VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN + ); + } + + /** + * @param {number} fileId + * @param {number} op + * @param {DataView} pArg + * @returns {number} + */ + xFileControl(fileId, op, pArg) { + const file = this.#mapIdToFile.get(fileId); + log(`xFileControl ${file.path} ${op}`); + + switch (op) { + case 11: //SQLITE_FCNTL_OVERWRITE + // This called on VACUUM. Set a flag so we know whether to check + // later if the page size changed. + file.overwrite = true; + return VFS.SQLITE_OK; + + case 21: // SQLITE_FCNTL_SYNC + // This is called at the end of each database transaction, whether + // it is batch atomic or not. Handle page size changes here. + if (file.overwrite) { + // As an optimization we only check for and handle a page file + // changes if we know a VACUUM has been done because handleAsync() + // has to unwind and rewind the stack. We must be sure to follow + // the same conditional path in both calls. + try { + return this.handleAsync(async () => { + await this.#reblockIfNeeded(file); + return VFS.SQLITE_OK; + }); + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + } + return VFS.SQLITE_OK; + + case 22: // SQLITE_FCNTL_COMMIT_PHASETWO + // This is called after a commit is completed. + file.overwrite = false; + return VFS.SQLITE_OK; + + case 31: // SQLITE_FCNTL_BEGIN_ATOMIC_WRITE + return this.handleAsync(async () => { + try { + // Prepare a new version for IndexedDB blocks. + file.block0.version--; + file.changedPages = new Set(); + + // Clear blocks from abandoned transactions that would conflict + // with the new transaction. + this.#idb.run("readwrite", async ({ blocks }) => { + const keys = await blocks + .index("version") + .getAllKeys( + IDBKeyRange.bound( + [file.path], + [file.path, file.block0.version] + ) + ); + for (const key of keys) { + blocks.delete(key); + } + }); + return VFS.SQLITE_OK; + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + }); + + case 32: // SQLITE_FCNTL_COMMIT_ATOMIC_WRITE + try { + const block0 = Object.assign({}, file.block0); + block0.data = block0.data.slice(); + const changedPages = file.changedPages; + file.changedPages = null; + this.#idb.run("readwrite", async ({ blocks }) => { + // Write block 0 to commit the new version. + blocks.put(block0); + + // Blocks to purge are saved in a special IndexedDB object with + // an "index" of "purge". Add pages changed by this transaction. + const purgeBlock = (await blocks.get([file.path, "purge", 0])) ?? { + path: file.path, + offset: "purge", + version: 0, + data: new Map(), + count: 0 + }; + + purgeBlock.count += changedPages.size; + for (const pageIndex of changedPages) { + purgeBlock.data.set(pageIndex, block0.version); + } + + blocks.put(purgeBlock); + this.#maybePurge(file.path, purgeBlock.count); + }); + return VFS.SQLITE_OK; + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + + case 33: // SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE + return this.handleAsync(async () => { + try { + // Restore original state. Objects for the abandoned version will + // be left in IndexedDB to be removed by the next atomic write + // transaction. + file.changedPages = null; + file.block0 = await this.#idb.run("readonly", ({ blocks }) => { + return blocks.get([file.path, 0, file.block0.version + 1]); + }); + return VFS.SQLITE_OK; + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + }); + + default: + return VFS.SQLITE_NOTFOUND; + } + } + + /** + * @param {string} name + * @param {number} flags + * @param {DataView} pResOut + * @returns {number} + */ + xAccess(name, flags, pResOut) { + return this.handleAsync(async () => { + try { + if (name.includes("-journal") || name.includes("-wal")) { + pResOut.setInt32(0, 0, true); + return VFS.SQLITE_OK; + } + + const path = new URL(name, "file://localhost/").pathname; + log(`xAccess ${path} ${flags}`); + + // Check if block 0 exists. + const key = await this.#idb.run("readonly", ({ blocks }) => { + return blocks.getKey(this.#bound({ path }, 0)); + }); + pResOut.setInt32(0, key ? 1 : 0, true); + return VFS.SQLITE_OK; + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + }); + } + + /** + * @param {string} name + * @param {number} syncDir + * @returns {number} + */ + xDelete(name, syncDir) { + return this.handleAsync(async () => { + const path = new URL(name, "file://localhost/").pathname; + log(`xDelete ${path} ${syncDir}`); + + try { + this.#idb.run("readwrite", ({ blocks }) => { + return blocks.delete(IDBKeyRange.bound([path], [path, []])); + }); + if (syncDir) { + await this.#idb.sync(); + } + return VFS.SQLITE_OK; + } catch (e) { + console.error(e); + return VFS.SQLITE_IOERR; + } + }); + } + + /** + * Purge obsolete blocks from a database file. + * @param {string} path + */ + async purge(path) { + const start = Date.now(); + await this.#idb.run("readwrite", async ({ blocks }) => { + const purgeBlock = await blocks.get([path, "purge", 0]); + if (purgeBlock) { + for (const [pageOffset, version] of purgeBlock.data) { + blocks.delete( + IDBKeyRange.bound( + [path, pageOffset, version], + [path, pageOffset, Infinity], + true, + false + ) + ); + } + await blocks.delete([path, "purge", 0]); + } + log( + `purge ${path} ${purgeBlock?.data.size ?? 0} pages in ${ + Date.now() - start + } ms` + ); + }); + } + + /** + * Conditionally schedule a purge task. + * @param {string} path + * @param {number} nPages + */ + #maybePurge(path, nPages) { + if ( + this.#options.purge === "manual" || + this.#pendingPurges.has(path) || + nPages < this.#options.purgeAtLeast + ) { + // No purge needed. + return; + } + + if (globalThis.requestIdleCallback) { + globalThis.requestIdleCallback(() => { + this.purge(path); + this.#pendingPurges.delete(path); + }); + } else { + setTimeout(() => { + this.purge(path); + this.#pendingPurges.delete(path); + }); + } + this.#pendingPurges.add(path); + } + + #bound(file, begin, end = 0) { + // Fetch newest block 0. For other blocks, use block 0 version. + const version = + !begin || -begin < file.block0.data.length + ? -Infinity + : file.block0.version; + return IDBKeyRange.bound( + [file.path, begin, version], + [file.path, end, Infinity] + ); + } + + // The database page size can be changed with PRAGMA page_size and VACUUM. + // The updated file will be overwritten with a regular transaction using + // the old page size. After that it will be read and written using the + // new page size, so the IndexedDB objects must be combined or split + // appropriately. + async #reblockIfNeeded(file) { + const oldPageSize = file.block0.data.length; + if (oldPageSize < 18) return; // no page size defined + + const view = new DataView( + file.block0.data.buffer, + file.block0.data.byteOffset + ); + let newPageSize = view.getUint16(16); + if (newPageSize === 1) newPageSize = 65536; + if (newPageSize === oldPageSize) return; // no page size change + + const maxPageSize = Math.max(oldPageSize, newPageSize); + const nOldPages = maxPageSize / oldPageSize; + const nNewPages = maxPageSize / newPageSize; + + const newPageCount = view.getUint32(28); + const fileSize = newPageCount * newPageSize; + + const version = file.block0.version; + await this.#idb.run("readwrite", async ({ blocks }) => { + // When the block size changes, the entire file is rewritten. Delete + // all blocks older than block 0 to leave a single version at every + // offset. + const keys = await blocks + .index("version") + .getAllKeys( + IDBKeyRange.bound([file.path, version + 1], [file.path, Infinity]) + ); + for (const key of keys) { + blocks.delete(key); + } + blocks.delete([file.path, "purge", 0]); + + // Do the conversion in chunks of the larger of the page sizes. + for (let iOffset = 0; iOffset < fileSize; iOffset += maxPageSize) { + // Fetch nOldPages. They can be fetched in one request because + // there is now a single version in the file. + const oldPages = await blocks.getAll( + IDBKeyRange.lowerBound([ + file.path, + -(iOffset + maxPageSize), + Infinity + ]), + nOldPages + ); + for (const oldPage of oldPages) { + blocks.delete([oldPage.path, oldPage.offset, oldPage.version]); + } + + // Convert to new pages. + if (nNewPages === 1) { + // Combine nOldPages old pages into a new page. + const buffer = new Uint8Array(newPageSize); + for (const oldPage of oldPages) { + buffer.set(oldPage.data, -(iOffset + oldPage.offset)); + } + const newPage = { + path: file.path, + offset: -iOffset, + version, + data: buffer + }; + if (newPage.offset === 0) { + newPage.fileSize = fileSize; + file.block0 = newPage; + } + blocks.put(newPage); + } else { + // Split an old page into nNewPages new pages. + const oldPage = oldPages[0]; + for (let i = 0; i < nNewPages; ++i) { + const offset = -(iOffset + i * newPageSize); + if (-offset >= fileSize) break; + const newPage = { + path: oldPage.path, + offset, + version, + data: oldPage.data.subarray( + i * newPageSize, + (i + 1) * newPageSize + ) + }; + if (newPage.offset === 0) { + newPage.fileSize = fileSize; + file.block0 = newPage; + } + blocks.put(newPage); + } + } + } + }); + } +} + +function openDatabase(idbDatabaseName) { + return new Promise((resolve, reject) => { + const request = globalThis.indexedDB.open(idbDatabaseName, 5); + request.addEventListener("upgradeneeded", function () { + const blocks = request.result.createObjectStore("blocks", { + keyPath: ["path", "offset", "version"] + }); + blocks.createIndex("version", ["path", "version"]); + }); + request.addEventListener("success", () => { + resolve(request.result); + }); + request.addEventListener("error", () => { + reject(request.error); + }); + }); +} diff --git a/apps/web/src/common/sqlite/IDBContext.js b/apps/web/src/common/sqlite/IDBContext.js new file mode 100644 index 000000000..a5ca60ace --- /dev/null +++ b/apps/web/src/common/sqlite/IDBContext.js @@ -0,0 +1,281 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +// Copyright 2022 Roy T. Hashimoto. All Rights Reserved. + +// IndexedDB transactions older than this will be replaced. +const MAX_TRANSACTION_LIFETIME_MILLIS = 5_000; + +// For debugging. +let nextTxId = 0; +const mapTxToId = new WeakMap(); +function log(...args) { + // console.log(...args); +} + +// This class manages IDBTransaction and IDBRequest instances. It tries +// to reuse transactions to minimize transaction overhead. +export class IDBContext { + /** @type {IDBDatabase} */ #db; + /** @type {Promise} */ #dbReady; + #txOptions; + + /** @type {IDBTransaction} */ #tx = null; + #txTimestamp = 0; + #runChain = Promise.resolve(); + #putChain = Promise.resolve(); + + /** + * @param {IDBDatabase|Promise} idbDatabase + */ + constructor(idbDatabase, txOptions = { durability: "default" }) { + this.#dbReady = Promise.resolve(idbDatabase).then((db) => (this.#db = db)); + this.#txOptions = txOptions; + } + + async close() { + const db = this.#db ?? (await this.#dbReady); + await this.#runChain; + await this.sync(); + db.close(); + } + + /** + * Run a function with the provided object stores. The function + * should be idempotent in case it is passed an expired transaction. + * @param {IDBTransactionMode} mode + * @param {(stores: Object.) => any} f + */ + async run(mode, f) { + // Ensure that functions run sequentially. + const result = this.#runChain.then(() => this.#run(mode, f)); + this.#runChain = result.catch(() => {}); + return result; + } + + /** + * @param {IDBTransactionMode} mode + * @param {(stores: Object.) => any} f + * @returns + */ + async #run(mode, f) { + const db = this.#db ?? (await this.#dbReady); + if (mode === "readwrite" && this.#tx?.mode === "readonly") { + // Mode requires a new transaction. + this.#tx = null; + } else if ( + performance.now() - this.#txTimestamp > + MAX_TRANSACTION_LIFETIME_MILLIS + ) { + // Chrome times out transactions after 60 seconds so refresh preemptively. + try { + this.#tx?.commit(); + } catch (e) { + // Explicit commit can fail but this can be ignored if it will + // auto-commit anyway. + if (e.name !== "InvalidStateError") throw e; + } + + // Skip to the next task to allow processing. + await new Promise((resolve) => setTimeout(resolve)); + this.#tx = null; + } + + // Run the user function with a retry in case the transaction is invalid. + for (let i = 0; i < 2; ++i) { + if (!this.#tx) { + // @ts-ignore + this.#tx = db.transaction(db.objectStoreNames, mode, this.#txOptions); + const timestamp = (this.#txTimestamp = performance.now()); + + // Chain the result of every transaction. If any transaction is + // aborted then the next sync() call will throw. + this.#putChain = this.#putChain.then(() => { + return new Promise((resolve, reject) => { + this.#tx.addEventListener("complete", (event) => { + resolve(); + if (this.#tx === event.target) { + this.#tx = null; + } + log(`transaction ${mapTxToId.get(event.target)} complete`); + }); + this.#tx.addEventListener("abort", (event) => { + console.warn("tx abort", (performance.now() - timestamp) / 1000); + // @ts-ignore + const e = event.target.error; + reject(e); + if (this.#tx === event.target) { + this.#tx = null; + } + log(`transaction ${mapTxToId.get(event.target)} aborted`, e); + }); + }); + }); + + log(`new transaction ${nextTxId} ${mode}`); + mapTxToId.set(this.#tx, nextTxId++); + } + + try { + const stores = Object.fromEntries( + Array.from(db.objectStoreNames, (name) => { + return [name, new ObjectStore(this.#tx.objectStore(name))]; + }) + ); + return await f(stores); + } catch (e) { + this.#tx = null; + if (i) throw e; + // console.warn('retrying with new transaction'); + } + } + } + + async sync() { + // Wait until all transactions since the previous sync have committed. + // Throw if any transaction failed. + await this.#putChain; + this.#putChain = Promise.resolve(); + } +} + +/** + * Helper to convert IDBRequest to Promise. + * @param {IDBRequest} request + * @returns {Promise} + */ +function wrapRequest(request) { + return new Promise((resolve, reject) => { + request.addEventListener("success", () => resolve(request.result)); + request.addEventListener("error", () => reject(request.error)); + }); +} + +// IDBObjectStore wrapper passed to IDBContext run functions. +class ObjectStore { + #objectStore; + + /** + * @param {IDBObjectStore} objectStore + */ + constructor(objectStore) { + this.#objectStore = objectStore; + } + + /** + * @param {IDBValidKey|IDBKeyRange} query + * @returns {Promise} + */ + get(query) { + log(`get ${this.#objectStore.name}`, query); + const request = this.#objectStore.get(query); + return wrapRequest(request); + } + + /** + * @param {IDBValidKey|IDBKeyRange} query + * @param {number} [count] + * @returns {Promise} + */ + getAll(query, count) { + log(`getAll ${this.#objectStore.name}`, query, count); + const request = this.#objectStore.getAll(query, count); + return wrapRequest(request); + } + + /** + * @param {IDBValidKey|IDBKeyRange} query + * @returns {Promise} + */ + getKey(query) { + log(`getKey ${this.#objectStore.name}`, query); + const request = this.#objectStore.getKey(query); + return wrapRequest(request); + } + + /** + * @param {IDBValidKey|IDBKeyRange} query + * @param {number} [count] + * @returns {Promise} + */ + getAllKeys(query, count) { + log(`getAllKeys ${this.#objectStore.name}`, query, count); + const request = this.#objectStore.getAllKeys(query, count); + return wrapRequest(request); + } + + /** + * @param {any} value + * @param {IDBValidKey} [key] + * @returns {Promise} + */ + put(value, key) { + log(`put ${this.#objectStore.name}`, value, key); + const request = this.#objectStore.put(value, key); + return wrapRequest(request); + } + + /** + * @param {IDBValidKey|IDBKeyRange} query + * @returns {Promise} + */ + delete(query) { + log(`delete ${this.#objectStore.name}`, query); + const request = this.#objectStore.delete(query); + return wrapRequest(request); + } + + clear() { + log(`clear ${this.#objectStore.name}`); + const request = this.#objectStore.clear(); + return wrapRequest(request); + } + + index(name) { + return new Index(this.#objectStore.index(name)); + } +} + +class Index { + /** @type {IDBIndex} */ #index; + + /** + * @param {IDBIndex} index + */ + constructor(index) { + this.#index = index; + } + + /** + * @param {IDBValidKey|IDBKeyRange} query + * @param {number} [count] + * @returns {Promise} + */ + getAllKeys(query, count) { + log( + `IDBIndex.getAllKeys ${this.#index.objectStore.name}<${ + this.#index.name + }>`, + query, + count + ); + const request = this.#index.getAllKeys(query, count); + return wrapRequest(request); + } +} diff --git a/apps/web/src/common/sqlite/VFS.js b/apps/web/src/common/sqlite/VFS.js new file mode 100644 index 000000000..0e83f4c73 --- /dev/null +++ b/apps/web/src/common/sqlite/VFS.js @@ -0,0 +1,191 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +// Copyright 2022 Roy T. Hashimoto. All Rights Reserved. +import * as VFS from "./sqlite-constants.js"; +export * from "./sqlite-constants.js"; + +// Base class for a VFS. +export class Base { + mxPathName = 64; + + /** + * @param {number} fileId + * @returns {number} + */ + xClose(fileId) { + return VFS.SQLITE_IOERR; + } + + /** + * @param {number} fileId + * @param {Uint8Array} pData + * @param {number} iOffset + * @returns {number} + */ + xRead(fileId, pData, iOffset) { + return VFS.SQLITE_IOERR; + } + + /** + * @param {number} fileId + * @param {Uint8Array} pData + * @param {number} iOffset + * @returns {number} + */ + xWrite(fileId, pData, iOffset) { + return VFS.SQLITE_IOERR; + } + + /** + * @param {number} fileId + * @param {number} iSize + * @returns {number} + */ + xTruncate(fileId, iSize) { + return VFS.SQLITE_IOERR; + } + + /** + * @param {number} fileId + * @param {*} flags + * @returns {number} + */ + xSync(fileId, flags) { + return VFS.SQLITE_OK; + } + + /** + * @param {number} fileId + * @param {DataView} pSize64 + * @returns {number} + */ + xFileSize(fileId, pSize64) { + return VFS.SQLITE_IOERR; + } + + /** + * @param {number} fileId + * @param {number} flags + * @returns {number} + */ + xLock(fileId, flags) { + return VFS.SQLITE_OK; + } + + /** + * @param {number} fileId + * @param {number} flags + * @returns {number} + */ + xUnlock(fileId, flags) { + return VFS.SQLITE_OK; + } + + /** + * @param {number} fileId + * @param {DataView} pResOut + * @returns {number} + */ + xCheckReservedLock(fileId, pResOut) { + pResOut.setInt32(0, 0, true); + return VFS.SQLITE_OK; + } + + /** + * @param {number} fileId + * @param {number} op + * @param {DataView} pArg + * @returns {number} + */ + xFileControl(fileId, op, pArg) { + return VFS.SQLITE_NOTFOUND; + } + + /** + * @param {number} fileId + * @returns {number} + */ + xSectorSize(fileId) { + return 512; + } + + /** + * @param {number} fileId + * @returns {number} + */ + xDeviceCharacteristics(fileId) { + return 0; + } + + /** + * @param {string?} name + * @param {number} fileId + * @param {number} flags + * @param {DataView} pOutFlags + * @returns {number} + */ + xOpen(name, fileId, flags, pOutFlags) { + return VFS.SQLITE_CANTOPEN; + } + + /** + * @param {string} name + * @param {number} syncDir + * @returns {number} + */ + xDelete(name, syncDir) { + return VFS.SQLITE_IOERR; + } + + /** + * @param {string} name + * @param {number} flags + * @param {DataView} pResOut + * @returns {number} + */ + xAccess(name, flags, pResOut) { + return VFS.SQLITE_IOERR; + } + + /** + * Handle asynchronous operation. This implementation will be overriden on + * registration by an Asyncify build. + * @param {function(): Promise} f + * @returns {number} + */ + handleAsync(f) { + // This default implementation deliberately does not match the + // declared signature. It will be used in testing VFS classes + // separately from SQLite. This will work acceptably for methods + // that simply return the handleAsync() result without using it. + // @ts-ignore + return f(); + } +} + +export const FILE_TYPE_MASK = [ + VFS.SQLITE_OPEN_MAIN_DB, + VFS.SQLITE_OPEN_MAIN_JOURNAL, + VFS.SQLITE_OPEN_TEMP_DB, + VFS.SQLITE_OPEN_TEMP_JOURNAL, + VFS.SQLITE_OPEN_TRANSIENT_DB, + VFS.SQLITE_OPEN_SUBJOURNAL, + VFS.SQLITE_OPEN_SUPER_JOURNAL +].reduce((mask, element) => mask | element); diff --git a/apps/web/src/common/sqlite/WebLocks.js b/apps/web/src/common/sqlite/WebLocks.js new file mode 100644 index 000000000..832440b51 --- /dev/null +++ b/apps/web/src/common/sqlite/WebLocks.js @@ -0,0 +1,364 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +// Copyright 2022 Roy T. Hashimoto. All Rights Reserved. +import * as VFS from "./VFS.js"; + +const LOCK_TYPE_MASK = + VFS.SQLITE_LOCK_NONE | + VFS.SQLITE_LOCK_SHARED | + VFS.SQLITE_LOCK_RESERVED | + VFS.SQLITE_LOCK_PENDING | + VFS.SQLITE_LOCK_EXCLUSIVE; + +export class WebLocksBase { + get state() { + return this.#state; + } + #state = VFS.SQLITE_LOCK_NONE; + + timeoutMillis = 0; + + /** @type {Map void>} */ #releasers = new Map(); + /** @type {Promise<0|5|3850>} */ #pending = Promise.resolve(0); + + /** + * @param {number} flags + * @returns {Promise<0|5|3850>} SQLITE_OK, SQLITE_BUSY, SQLITE_IOERR_LOCK + */ + async lock(flags) { + return this.#apply(this.#lock, flags); + } + + /** + * @param {number} flags + * @returns {Promise<0|5|3850>} SQLITE_OK, SQLITE_IOERR_LOCK + */ + async unlock(flags) { + return this.#apply(this.#unlock, flags); + } + + /** + * @returns {Promise} + */ + async isSomewhereReserved() { + throw new Error("unimplemented"); + } + + /** + * + * @param {(targetState: number) => void} method + * @param {number} flags + */ + async #apply(method, flags) { + const targetState = flags & LOCK_TYPE_MASK; + try { + // Force locks and unlocks to run sequentially. This allows not + // waiting for unlocks to complete. + const call = () => method.call(this, targetState); + await (this.#pending = this.#pending.then(call, call)); + this.#state = targetState; + return VFS.SQLITE_OK; + } catch (e) { + if (e.name === "AbortError") { + return VFS.SQLITE_BUSY; + } + console.error(e); + return VFS.SQLITE_IOERR_LOCK; + } + } + + async #lock(targetState) { + if (targetState === this.#state) return VFS.SQLITE_OK; + switch (this.#state) { + case VFS.SQLITE_LOCK_NONE: + switch (targetState) { + case VFS.SQLITE_LOCK_SHARED: + return this._NONEtoSHARED(); + default: + throw new Error( + `unexpected transition ${this.#state} -> ${targetState}` + ); + } + + case VFS.SQLITE_LOCK_SHARED: + switch (targetState) { + case VFS.SQLITE_LOCK_RESERVED: + return this._SHAREDtoRESERVED(); + case VFS.SQLITE_LOCK_EXCLUSIVE: + return this._SHAREDtoEXCLUSIVE(); + default: + throw new Error( + `unexpected transition ${this.#state} -> ${targetState}` + ); + } + + case VFS.SQLITE_LOCK_RESERVED: + switch (targetState) { + case VFS.SQLITE_LOCK_EXCLUSIVE: + return this._RESERVEDtoEXCLUSIVE(); + default: + throw new Error( + `unexpected transition ${this.#state} -> ${targetState}` + ); + } + + default: + throw new Error( + `unexpected transition ${this.#state} -> ${targetState}` + ); + } + } + + async #unlock(targetState) { + if (targetState === this.#state) return VFS.SQLITE_OK; + switch (this.#state) { + case VFS.SQLITE_LOCK_EXCLUSIVE: + switch (targetState) { + case VFS.SQLITE_LOCK_SHARED: + return this._EXCLUSIVEtoSHARED(); + case VFS.SQLITE_LOCK_NONE: + return this._EXCLUSIVEtoNONE(); + default: + throw new Error( + `unexpected transition ${this.#state} -> ${targetState}` + ); + } + + case VFS.SQLITE_LOCK_RESERVED: + switch (targetState) { + case VFS.SQLITE_LOCK_SHARED: + return this._RESERVEDtoSHARED(); + case VFS.SQLITE_LOCK_NONE: + return this._RESERVEDtoNONE(); + default: + throw new Error( + `unexpected transition ${this.#state} -> ${targetState}` + ); + } + + case VFS.SQLITE_LOCK_SHARED: + switch (targetState) { + case VFS.SQLITE_LOCK_NONE: + return this._SHAREDtoNONE(); + default: + throw new Error( + `unexpected transition ${this.#state} -> ${targetState}` + ); + } + + default: + throw new Error( + `unexpected transition ${this.#state} -> ${targetState}` + ); + } + } + + async _NONEtoSHARED() {} + + async _SHAREDtoEXCLUSIVE() { + await this._SHAREDtoRESERVED(); + await this._RESERVEDtoEXCLUSIVE(); + } + + async _SHAREDtoRESERVED() {} + + async _RESERVEDtoEXCLUSIVE() {} + + async _EXCLUSIVEtoRESERVED() {} + + async _EXCLUSIVEtoSHARED() { + await this._EXCLUSIVEtoRESERVED(); + await this._RESERVEDtoSHARED(); + } + + async _EXCLUSIVEtoNONE() { + await this._EXCLUSIVEtoRESERVED(); + await this._RESERVEDtoSHARED(); + await this._SHAREDtoNONE(); + } + + async _RESERVEDtoSHARED() {} + + async _RESERVEDtoNONE() { + await this._RESERVEDtoSHARED(); + await this._SHAREDtoNONE(); + } + + async _SHAREDtoNONE() {} + + /** + * @param {string} lockName + * @param {LockOptions} options + * @returns {Promise} + */ + _acquireWebLock(lockName, options) { + return new Promise(async (resolve, reject) => { + try { + await navigator.locks.request(lockName, options, (lock) => { + resolve(lock); + if (lock) { + return new Promise((release) => + this.#releasers.set(lockName, release) + ); + } + }); + } catch (e) { + reject(e); + } + }); + } + + /** + * @param {string} lockName + */ + _releaseWebLock(lockName) { + this.#releasers.get(lockName)?.(); + this.#releasers.delete(lockName); + } + + /** + * @param {string} lockName + */ + async _pollWebLock(lockName) { + const query = await navigator.locks.query(); + return query.held.find(({ name }) => name === lockName)?.mode; + } + + /** + * @returns {?AbortSignal} + */ + _getTimeoutSignal() { + if (this.timeoutMillis) { + const abortController = new AbortController(); + setTimeout(() => abortController.abort(), this.timeoutMillis); + return abortController.signal; + } + return undefined; + } +} + +export class WebLocksExclusive extends WebLocksBase { + /** + * @param {string} name + */ + constructor(name) { + super(); + this._lockName = name + "-outer"; + this._reservedName = name + "-reserved"; + } + + async isSomewhereReserved() { + const mode = await this._pollWebLock(this._reservedName); + return mode === "exclusive"; + } + + async _NONEtoSHARED() { + await this._acquireWebLock(this._lockName, { + mode: "exclusive", + signal: this._getTimeoutSignal() + }); + } + + async _SHAREDtoRESERVED() { + await this._acquireWebLock(this._reservedName, { + mode: "exclusive", + signal: this._getTimeoutSignal() + }); + } + + async _RESERVEDtoSHARED() { + this._releaseWebLock(this._reservedName); + } + + async _SHAREDtoNONE() { + this._releaseWebLock(this._lockName); + } +} + +export class WebLocksShared extends WebLocksBase { + maxRetryMillis = 1000; + + /** + * @param {string} name + */ + constructor(name) { + super(); + this._outerName = name + "-outer"; + this._innerName = name + "-inner"; + } + + async isSomewhereReserved() { + const mode = await this._pollWebLock(this._outerName); + return mode === "exclusive"; + } + + async _NONEtoSHARED() { + await this._acquireWebLock(this._outerName, { + mode: "shared", + signal: this._getTimeoutSignal() + }); + await this._acquireWebLock(this._innerName, { + mode: "shared", + signal: this._getTimeoutSignal() + }); + this._releaseWebLock(this._outerName); + } + + async _SHAREDtoRESERVED() { + let timeoutMillis = 1; + while (true) { + // Attempt to get the outer lock without blocking. + const isLocked = await this._acquireWebLock(this._outerName, { + mode: "exclusive", + ifAvailable: true + }); + if (isLocked) break; + + if (await this.isSomewhereReserved()) { + // Someone else has a reserved lock so retry cannot succeed. + throw new DOMException("", "AbortError"); + } + + await new Promise((resolve) => setTimeout(resolve, timeoutMillis)); + timeoutMillis = Math.min(2 * timeoutMillis, this.maxRetryMillis); + } + this._releaseWebLock(this._innerName); + } + + async _RESERVEDtoEXCLUSIVE() { + await this._acquireWebLock(this._innerName, { + mode: "exclusive", + signal: this._getTimeoutSignal() + }); + } + + async _EXCLUSIVEtoRESERVED() { + this._releaseWebLock(this._innerName); + } + + async _RESERVEDtoSHARED() { + await this._acquireWebLock(this._innerName, { mode: "shared" }); + this._releaseWebLock(this._outerName); + } + + async _SHAREDtoNONE() { + this._releaseWebLock(this._innerName); + } +} diff --git a/apps/web/src/common/sqlite/globals.d.ts b/apps/web/src/common/sqlite/globals.d.ts new file mode 100644 index 000000000..a7e2a3455 --- /dev/null +++ b/apps/web/src/common/sqlite/globals.d.ts @@ -0,0 +1,85 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +/* eslint-disable no-var */ + +declare namespace Asyncify { + function handleAsync(f: () => Promise); +} + +declare function UTF8ToString(ptr: number): string; +declare function lengthBytesUTF8(s: string): number; +declare function stringToUTF8(s: string, p: number, n: number); +declare function ccall( + name: string, + returns: string, + args: Array, + options?: object +): any; +declare function getValue(ptr: number, type: string): number; +declare function setValue(ptr: number, value: number, type: string): number; +declare function mergeInto(library: object, methods: object): void; + +declare var HEAPU8: Uint8Array; +declare var HEAPU32: Uint32Array; +declare var LibraryManager; +declare var Module; +declare var _vfsAccess; +declare var _vfsCheckReservedLock; +declare var _vfsClose; +declare var _vfsDelete; +declare var _vfsDeviceCharacteristics; +declare var _vfsFileControl; +declare var _vfsFileSize; +declare var _vfsLock; +declare var _vfsOpen; +declare var _vfsRead; +declare var _vfsSectorSize; +declare var _vfsSync; +declare var _vfsTruncate; +declare var _vfsUnlock; +declare var _vfsWrite; + +declare var _jsFunc; +declare var _jsStep; +declare var _jsFinal; + +declare var _modStruct; +declare var _modCreate; +declare var _modConnect; +declare var _modBestIndex; +declare var _modDisconnect; +declare var _modDestroy; +declare var _modOpen; +declare var _modClose; +declare var _modFilter; +declare var _modNext; +declare var _modEof; +declare var _modColumn; +declare var _modRowid; +declare var _modUpdate; +declare var _modBegin; +declare var _modSync; +declare var _modCommit; +declare var _modRollback; +declare var _modFindFunction; +declare var _modRename; + +declare var _jsAuth; + +declare var _jsProgress; diff --git a/apps/web/src/common/sqlite/index.d.ts b/apps/web/src/common/sqlite/index.d.ts new file mode 100644 index 000000000..b5772961c --- /dev/null +++ b/apps/web/src/common/sqlite/index.d.ts @@ -0,0 +1,1726 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +/** + * This is a WebAssembly build of SQLite with experimental support for + * writing SQLite virtual file systems and modules (for virtual tables) + * in Javascript. Also see the + * [GitHub repository](https://github.com/rhashimoto/wa-sqlite) and the + * [online demo](https://rhashimoto.github.io/wa-sqlite/demo/). + * @module + */ + +/** + * Javascript types that SQLite can use + * + * C integer and floating-point types both map to/from Javascript `number`. + * Blob data can be provided to SQLite as `Uint8Array` or `number[]` (with + * each element converted to a byte); SQLite always returns blob data as + * `Uint8Array` + */ +type SQLiteCompatibleType = + | number + | string + | Uint8Array + | Array + | bigint + | null; + +/** + * SQLite Virtual File System object + * + * Objects with this interface can be passed to {@link SQLiteAPI.vfs_register} + * to define a new filesystem. + * + * There are examples of a synchronous + * [MemoryVFS.js](https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/MemoryVFS.js), + * and asynchronous + * [MemoryAsyncVFS.js](https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/MemoryAsyncVFS.js) + * and + * [IndexedDbVFS.js](https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/IndexedDbVFS.js). + * + * @see https://sqlite.org/vfs.html + * @see https://sqlite.org/c3ref/io_methods.html + */ +declare interface SQLiteVFS { + /** Maximum length of a file path in UTF-8 bytes (default 64) */ + mxPathName?: number; + + /** @see https://sqlite.org/c3ref/io_methods.html */ + xClose(fileId: number): number; + + /** @see https://sqlite.org/c3ref/io_methods.html */ + xRead(fileId: number, pData: Uint8Array, iOffset: number): number; + + /** @see https://sqlite.org/c3ref/io_methods.html */ + xWrite(fileId: number, pData: Uint8Array, iOffset: number): number; + + /** @see https://sqlite.org/c3ref/io_methods.html */ + xTruncate(fileId: number, iSize: number): number; + + /** @see https://sqlite.org/c3ref/io_methods.html */ + xSync(fileId: number, flags: number): number; + + /** @see https://sqlite.org/c3ref/io_methods.html */ + xFileSize(fileId: number, pSize64: DataView): number; + + /** @see https://sqlite.org/c3ref/io_methods.html */ + xLock(fileId: number, flags: number): number; + + /** @see https://sqlite.org/c3ref/io_methods.html */ + xUnlock(fileId: number, flags: number): number; + + /** @see https://sqlite.org/c3ref/io_methods.html */ + xCheckReservedLock(fileId: number, pResOut: DataView): number; + + /** @see https://sqlite.org/c3ref/io_methods.html */ + xFileControl(fileId: number, flags: number, pOut: DataView): number; + + /** @see https://sqlite.org/c3ref/io_methods.html */ + xDeviceCharacteristics(fileId: number): number; + + /** @see https://sqlite.org/c3ref/vfs.html */ + xOpen( + name: string | null, + fileId: number, + flags: number, + pOutFlags: DataView + ): number; + + /** @see https://sqlite.org/c3ref/vfs.html */ + xDelete(name: string, syncDir: number): number; + + /** @see https://sqlite.org/c3ref/vfs.html */ + xAccess(name: string, flags: number, pResOut: DataView): number; +} + +/** + * This object is passed by SQLite to implementations of + * {@link SQLiteModule.xBestIndex} + * @see https://sqlite.org/c3ref/index_info.html + */ +declare interface SQLiteModuleIndexInfo { + nConstraint: number; + aConstraint: Array<{ + iColumn: number; + op: number; + usable: boolean; + }>; + nOrderBy: number; + aOrderBy: Array<{ + iColumn: number; + desc: boolean; + }>; + aConstraintUsage: Array<{ + argvIndex: number; + omit: boolean; + }>; + idxNum: number; + idxStr: string | null; + orderByConsumed: boolean; + estimatedCost: number; + estimatedRows: number; + idxFlags: number; + colUsed: number; +} + +/** + * SQLite Module object + * + * Objects with this interface can be passed to {@link SQLiteAPI.create_module} + * to define a module for virtual tables. + * + * There is an example + * [ArrayModule.js](https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/ArrayModule.js) + * that allows a virtual table to reference a Javascript array. + * + * @see https://sqlite.org/vtab.html + */ +declare interface SQLiteModule { + /** + * @see https://sqlite.org/vtab.html#the_xcreate_method + */ + xCreate?( + db: number, + appData, + argv: string[], + pVTab: number, + pzErr: DataView + ): number; + + /** + * @see https://sqlite.org/vtab.html#the_xconnect_method + */ + xConnect( + db: number, + appData, + argv: string[], + pVTab: number, + pzErr: DataView + ): number; + + /** + * @see https://sqlite.org/vtab.html#the_xbestindex_method + */ + xBestIndex(pVTab: number, indexInfo: SQLiteModuleIndexInfo): number; + + /** + * @see https://sqlite.org/vtab.html#the_xdisconnect_method + */ + xDisconnect(pVTab: number): number; + + /** + * @see https://sqlite.org/vtab.html#the_xdestroy_method + */ + xDestroy(pVTab: number): number; + + /** + * @see https://sqlite.org/vtab.html#the_xopen_method + */ + xOpen(pVTab: number, pCursor: number): number; + + /** + * @see https://sqlite.org/vtab.html#the_xclose_method + */ + xClose(pCursor: number): number; + + /** + * @see https://sqlite.org/vtab.html#the_xfilter_method + */ + xFilter( + pCursor: number, + idxNum: number, + idxString: string | null, + values: number[] + ): number; + + /** + * @see https://sqlite.org/vtab.html#the_xnext_method + */ + xNext(pCursor: number): number; + + /** + * @see https://sqlite.org/vtab.html#the_xeof_method + */ + xEof(pCursor: number): number; + + /** + * @see https://sqlite.org/vtab.html#the_xcolumn_method + */ + xColumn(pCursor: number, pContext: number, iCol: number): number; + + /** + * @see https://sqlite.org/vtab.html#the_xrowid_method + */ + xRowid(pCursor: number, pRowid: DataView): number; + + /** + * @see https://sqlite.org/vtab.html#the_xupdate_method + */ + xUpdate?(pVTab: number, values: number[], pRowId: DataView): number; + + /** + * @see https://sqlite.org/vtab.html#the_xbegin_method + */ + xBegin?(pVTab: number): number; + + /** + * @see https://sqlite.org/vtab.html#the_xsync_method + */ + xSync?(pVTab: number): number; + + /** + * @see https://sqlite.org/vtab.html#the_xcommit_method + */ + xCommit?(pVTab: number): number; + + /** + * @see https://sqlite.org/vtab.html#the_xrollback_method + */ + xRollback?(pVTab: number): number; + + /** + * @see https://sqlite.org/vtab.html#the_xrename_method + */ + xRename?(pVTab: number, zNew: string): number; +} + +/** + * Javascript wrappers for the SQLite C API (plus a few convenience functions) + * + * Function signatures have been slightly modified to be more + * Javascript-friendly. For the C functions that return an error code, + * the corresponding Javascript wrapper will throw an exception with a + * `code` property on an error. + * + * Note that a few functions return a Promise in order to accomodate + * either a synchronous or asynchronous SQLite build, generally those + * involved with opening/closing a database or executing a statement. + * + * To create an instance of the API, follow these steps: + * + * ```javascript + * // Import an ES6 module factory function from one of the + * // package builds, either 'wa-sqlite.mjs' (synchronous) or + * // 'wa-sqlite-async.mjs' (asynchronous). You should only + * // use the asynchronous build if you plan to use an + * // asynchronous VFS or module. + * import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite.mjs'; + * + * // Import the Javascript API wrappers. + * import * as SQLite from 'wa-sqlite'; + * + * // Use an async function to simplify Promise handling. + * (async function() { + * // Invoke the ES6 module factory to create the SQLite + * // Emscripten module. This will fetch and compile the + * // .wasm file. + * const module = await SQLiteESMFactory(); + * + * // Use the module to build the API instance. + * const sqlite3 = SQLite.Factory(module); + * + * // Use the API to open and access a database. + * const db = await sqlite3.open_v2('myDB'); + * ... + * })(); + * ``` + * + * @see https://sqlite.org/c3ref/funclist.html + */ +declare interface SQLiteAPI { + /** + * Bind a collection of values to a statement + * + * This convenience function binds values from either an array or object + * to a prepared statement with placeholder parameters. + * + * Array example using numbered parameters (numbering is implicit in + * this example): + * ``` + * const str = sqlite3.str_new(db, ` + * INSERT INTO tbl VALUES (?, ?, ?); + * `); + * const prepared = await sqlite3.prepare_v2(db, sqlite3.str_value(str)); + * sqlite3.bind_collection(prepared.stmt, [42, 'hello', null]); + * ... + * ``` + * + * Object example using named parameters (':', '@', or '$' prefixes + * are allowed): + * ``` + * const str = sqlite3.str_new(db, ` + * INSERT INTO tbl VALUES (@foo, @bar, @baz); + * `); + * const prepared = await sqlite3.prepare_v2(db, sqlite3.str_value(str)); + * sqlite3.bind_collection(prepared.stmt, { + * '@foo': 42, + * '@bar': 'hello', + * '@baz': null, + * }); + * ... + * ``` + * + * Note that SQLite bindings are indexed beginning with 1, but when + * binding values from an array `a` the values begin with `a[0]`. + * @param stmt prepared statement pointer + * @param bindings + * @returns `SQLITE_OK` (throws exception on error) + */ + bind_collection( + stmt: number, + bindings: + | { [index: string]: SQLiteCompatibleType | null } + | Array + ): number; + + /** + * Bind value to prepared statement + * + * This convenience function calls the appropriate `bind_*` function + * based on the type of `value`. Note that binding indices begin with 1. + * @param stmt prepared statement pointer + * @param i binding index + * @param value + * @returns `SQLITE_OK` (throws exception on error) + */ + bind(stmt: number, i: number, value: SQLiteCompatibleType | null): number; + + /** + * Bind blob to prepared statement parameter + * + * Note that binding indices begin with 1. + * @see https://www.sqlite.org/c3ref/bind_blob.html + * @param stmt prepared statement pointer + * @param i binding index + * @param value + * @returns `SQLITE_OK` (throws exception on error) + */ + bind_blob(stmt: number, i: number, value: Uint8Array | Array): number; + + /** + * Bind number to prepared statement parameter + * + * Note that binding indices begin with 1. + * @see https://www.sqlite.org/c3ref/bind_blob.html + * @param stmt prepared statement pointer + * @param i binding index + * @param value + * @returns `SQLITE_OK` (throws exception on error) + */ + bind_double(stmt: number, i: number, value: number): number; + + /** + * Bind number to prepared statement parameter + * + * Note that binding indices begin with 1. + * @see https://www.sqlite.org/c3ref/bind_blob.html + * @param stmt prepared statement pointer + * @param i binding index + * @param value + * @returns `SQLITE_OK` (throws exception on error) + */ + bind_int(stmt: number, i: number, value: number): number; + + /** + * Bind number to prepared statement parameter + * + * Note that binding indices begin with 1. + * @see https://www.sqlite.org/c3ref/bind_blob.html + * @param stmt prepared statement pointer + * @param i binding index + * @param value + * @returns `SQLITE_OK` (throws exception on error) + */ + bind_int64(stmt: number, i: number, value: bigint): number; + + /** + * Bind null to prepared statement + * + * Note that binding indices begin with 1. + * @see https://www.sqlite.org/c3ref/bind_blob.html + * @param stmt prepared statement pointer + * @param value + * @returns `SQLITE_OK` (throws exception on error) + */ + bind_null(stmt: number, i: number): number; + + /** + * Get number of bound parameters + * @see https://www.sqlite.org/c3ref/bind_parameter_count.html + * @param stmt prepared statement pointer + * @returns number of statement binding locations + */ + bind_parameter_count(stmt: number): number; + + /** + * Reset all bindings on a prepared statement + * @see https://www.sqlite.org/c3ref/clear_bindings.html + * @param stmt prepared statement pointer + * @returns `SQLITE_OK` (throws exception on error) + */ + clear_bindings(stmt: number): number; + + /** + * Get name of bound parameter + * + * Note that binding indices begin with 1. + * @see https://www.sqlite.org/c3ref/bind_parameter_name.html + * @param stmt prepared statement pointer + * @param i binding index + * @returns binding name + */ + bind_parameter_name(stmt: number, i: number): string; + + /** + * Bind string to prepared statement + * + * Note that binding indices begin with 1. + * @see https://www.sqlite.org/c3ref/bind_blob.html + * @param stmt prepared statement pointer + * @param i binding index + * @param value + * @returns `SQLITE_OK` (throws exception on error) + */ + bind_text(stmt: number, i: number, value: string): number; + + /** + * Get count of rows modified by last insert/update + * @see https://www.sqlite.org/c3ref/changes.html + * @param db database pointer + * @returns number of rows modified + */ + changes(db): number; + + /** + * Close database connection + * @see https://www.sqlite.org/c3ref/close.html + * @param db database pointer + * @returns `SQLITE_OK` (throws exception on error) + */ + close(db): Promise; + + /** + * Call the appropriate `column_*` function based on the column type + * + * The type is determined by calling {@link column_type}, which may + * not match the type declared in `CREATE TABLE`. Note that if the column + * value is a blob then as with `column_blob` the result may be invalid + * after the next SQLite call; copy if it needs to be retained. + * + * Integer values are returned as Number if within the min/max safe + * integer bounds, otherwise they are returned as BigInt. + * @param stmt prepared statement pointer + * @param i column index + * @returns column value + */ + column(stmt: number, i: number): SQLiteCompatibleType; + + /** + * Extract a column value from a row after a prepared statment {@link step} + * + * The contents of the returned buffer may be invalid after the + * next SQLite call. Make a copy of the data (e.g. with `.slice()`) + * if longer retention is required. + * @see https://www.sqlite.org/c3ref/column_blob.html + * @param stmt prepared statement pointer + * @param i column index + * @returns column value + */ + column_blob(stmt: number, i: number): Uint8Array; + + /** + * Get storage size for column text or blob + * @see https://www.sqlite.org/c3ref/column_blob.html + * @param stmt prepared statement pointer + * @param i column index + * @returns number of bytes in column text or blob + */ + column_bytes(stmt: number, i: number): number; + + /** + * Get number of columns for a prepared statement + * @see https://www.sqlite.org/c3ref/column_blob.html + * @param stmt prepared statement pointer + * @returns number of columns + */ + column_count(stmt: number): number; + + /** + * Extract a column value from a row after a prepared statment {@link step} + * @see https://www.sqlite.org/c3ref/column_blob.html + * @param stmt prepared statement pointer + * @param i column index + * @returns column value + */ + column_double(stmt: number, i: number): number; + + /** + * Extract a column value from a row after a prepared statment {@link step} + * @see https://www.sqlite.org/c3ref/column_blob.html + * @param stmt prepared statement pointer + * @param i column index + * @returns column value + */ + column_int(stmt: number, i: number): number; + + /** + * Extract a column value from a row after a prepared statment {@link step} + * @see https://www.sqlite.org/c3ref/column_blob.html + * @param stmt prepared statement pointer + * @param i column index + * @returns column value + */ + column_int64(stmt: number, i: number): bigint; + + /** + * Get a column name for a prepared statement + * @see https://www.sqlite.org/c3ref/column_blob.html + * @param stmt prepared statement pointer + * @param i column index + * @returns column name + */ + column_name(stmt: number, i: number): string; + + /** + * Get names for all columns of a prepared statement + * + * This is a convenience function that calls {@link column_count} and + * {@link column_name}. + * @param stmt + * @returns array of column names + */ + column_names(stmt: number): Array; + + /** + * Extract a column value from a row after a prepared statment {@link step} + * @see https://www.sqlite.org/c3ref/column_blob.html + * @param stmt prepared statement pointer + * @param i column index + * @returns column value + */ + column_text(stmt: number, i: number): string; + + /** + * Get column type for a prepared statement + * + * Note that this type may not match the type declared in `CREATE TABLE`. + * @see https://www.sqlite.org/c3ref/column_blob.html + * @param stmt prepared statement pointer + * @param i column index + * @returns enumeration value for type + */ + column_type(stmt: number, i: number): number; + + /** + * Create or redefine SQL functions + * @see https://sqlite.org/c3ref/create_function.html + * @param db database pointer + * @param zFunctionName + * @param nArg number of function arguments + * @param eTextRep text encoding (and other flags) + * @param pApp application data + * @param xFunc + * @param xStep + * @param xFinal + * @returns `SQLITE_OK` (throws exception on error) + */ + create_function( + db: number, + zFunctionName: string, + nArg: number, + eTextRep: number, + pApp: number, + xFunc?: (context: number, values: Uint32Array) => void, + xStep?: (context: number, values: Uint32Array) => void, + xFinal?: (context: number) => void + ): number; + + /** + * Create a SQLite module for virtual tables + * @see https://www.sqlite.org/c3ref/create_module.html + * @param db database pointer + * @param zName + * @param module + * @param appData + * @returns `SQLITE_OK` (throws exception on error) + */ + create_module( + db: number, + zName: string, + module: SQLiteModule, + appData? + ): number; + + /** + * Get number of columns in current row of a prepared statement + * @see https://www.sqlite.org/c3ref/data_count.html + * @param stmt prepared statement pointer + * @returns number of columns + */ + data_count(stmt: number): number; + + /** + * Declare the schema of a virtual table in module + * {@link SQLiteModule.xCreate} or {@link SQLiteModule.xConnect} + * methods + * @see https://www.sqlite.org/c3ref/declare_vtab.html + * @param db database pointer + * @param zSQL schema declaration + * @returns `SQLITE_OK` (throws exception on error) + */ + declare_vtab(db: number, zSQL: string): number; + + /** + * One-step query execution interface + * + * The implementation of this function uses {@link row}, which makes a + * copy of blobs and returns BigInt for integers outside the safe integer + * bounds for Number. + * @see https://www.sqlite.org/c3ref/exec.html + * @param db database pointer + * @param zSQL queries + * @param callback called for each output row + * @returns Promise resolving to `SQLITE_OK` (rejects on error) + */ + exec( + db: number, + zSQL: string, + callback?: ( + row: Array, + columns: string[] + ) => void + ): Promise; + + /** + * Destroy a prepared statement object compiled with {@link prepare_v2} + * + * This function does *not* throw on error. + * @see https://www.sqlite.org/c3ref/finalize.html + * @param stmt prepared statement pointer + * @returns Promise resolving to `SQLITE_OK` or error status + */ + finalize(stmt: number): Promise; + + /** + * Test for autocommit mode + * @see https://sqlite.org/c3ref/get_autocommit.html + * @param db database pointer + * @returns Non-zero if autocommit mode is on, zero otherwise + */ + get_autocommit(db: number): number; + + /** + * Get SQLite library version + * @see https://www.sqlite.org/c3ref/libversion.html + * @returns version string, e.g. '3.35.5' + */ + libversion(): string; + + /** + * Get SQLite library version + * @see https://www.sqlite.org/c3ref/libversion.html + * @returns version number, e.g. 3035005 + */ + libversion_number(): number; + + /** + * Set a usage limit on a connection. + * @see https://www.sqlite.org/c3ref/limit.html + * @param db database pointer + * @param id limit category + * @param newVal + * @returns previous setting + */ + limit(db: number, id: number, newVal: number): number; + + /** + * Opening a new database connection. + * + * Note that this function differs from the C API in that it + * returns the Promise-wrapped database pointer (instead of a + * result code). + * @see https://sqlite.org/c3ref/open.html + * @param zFilename + * @param iFlags `SQLite.SQLITE_OPEN_CREATE | SQLite.SQLITE_OPEN_READWRITE` (0x6) if omitted + * @param zVfs VFS name + * @returns Promise-wrapped database pointer. + */ + open_v2(zFilename: string, iFlags?: number, zVfs?: string): Promise; + + /** + * Compile an SQL statement + * + * SQL is provided as a pointer in WASM memory, so the utility functions + * {@link str_new} and {@link str_value} should be used. The returned + * Promise-wrapped object provides both the prepared statement and a + * pointer to the still uncompiled SQL that can be used with the next + * call to this function. A Promise containing `null` is returned + * when no statement remains. + * + * Each prepared statement should be destroyed with {@link finalize} + * after its usage is complete. + * + * Code using {@link prepare_v2} generally looks like this: + * ```javascript + * const str = sqlite3.str_new(db, sql); + * try { + * // Traverse and prepare the SQL, statement by statement. + * let prepared = { stmt: null, sql: sqlite3.str_value(str) }; + * while ((prepared = await sqlite3.prepare_v2(db, prepared.sql))) { + * try { + * // Step through the rows produced by the statement. + * while (await sqlite3.step(prepared.stmt) === SQLite.SQLITE_ROW) { + * // Do something with the row data... + * } + * } finally { + * sqlite3.finalize(prepared.stmt); + * } + * } + * } finally { + * sqlite3.str_finish(str); + * } + * ``` + * + * The {@link statements} convenience function can be used to + * avoid the boilerplate of calling {@link prepare_v2} directly. + * @see https://www.sqlite.org/c3ref/prepare.html + * @param db database pointer + * @param sql SQL pointer + * @returns Promise-wrapped object containing the prepared statement + * pointer and next SQL pointer, or a Promise containing `null` when + * no statement remains + */ + prepare_v2( + db: number, + sql: number + ): Promise<{ stmt: number; sql: number } | null>; + + /** + * Specify callback to be invoked between long-running queries + * @param db database pointer + * @param nProgressOps target number of database operations between handler invocations + * @param handler + * @param userData + */ + progress_handler( + db: number, + nProgressOps: number, + handler: (userData: any) => number, + userData + ); + + /** + * Reset a prepared statement object + * @see https://www.sqlite.org/c3ref/reset.html + * @param stmt prepared statement pointer + * @returns Promise-wrapped `SQLITE_OK` (rejects on error) + */ + reset(stmt: number): Promise; + + /** + * Convenience function to call `result_*` based of the type of `value` + * @param context context pointer + * @param value + */ + result( + context: number, + value: (SQLiteCompatibleType | number[]) | null + ): void; + + /** + * Set the result of a function or vtable column + * @see https://sqlite.org/c3ref/result_blob.html + * @param context context pointer + * @param value + */ + result_blob(context: number, value: Uint8Array | number[]): void; + + /** + * Set the result of a function or vtable column + * @see https://sqlite.org/c3ref/result_blob.html + * @param context context pointer + * @param value + */ + result_double(context: number, value: number): void; + + /** + * Set the result of a function or vtable column + * @see https://sqlite.org/c3ref/result_blob.html + * @param context context pointer + * @param value + */ + result_int(context: number, value: number): void; + + /** + * Set the result of a function or vtable column + * @see https://sqlite.org/c3ref/result_blob.html + * @param context context pointer + * @param value + */ + result_int64(context: number, value: bigint): void; + + /** + * Set the result of a function or vtable column + * @see https://sqlite.org/c3ref/result_blob.html + * @param context context pointer + */ + result_null(context: number): void; + + /** + * Set the result of a function or vtable column + * @see https://sqlite.org/c3ref/result_blob.html + * @param context context pointer + * @param value + */ + result_text(context: number, value: string): void; + + /** + * Get all column data for a row from a prepared statement step + * + * This convenience function will return a copy of any blob, unlike + * {@link column_blob} which returns a value referencing volatile WASM + * memory with short validity. Like {@link column}, it will return a + * BigInt for integers outside the safe integer bounds for Number. + * @param stmt prepared statement pointer + * @returns row data + */ + row(stmt: number): Array; + + /** + * Register a callback function that is invoked to authorize certain SQL statement actions. + * @see https://www.sqlite.org/c3ref/set_authorizer.html + * @param db database pointer + * @param authFunction + * @param userData + */ + set_authorizer( + db: number, + authFunction: ( + userData: any, + iActionCode: number, + param3: string | null, + param4: string | null, + param5: string | null, + param6: string | null + ) => number, + userData: any + ): number; + + /** + * Get statement SQL + * @see https://www.sqlite.org/c3ref/expanded_sql.html + * @param stmt prepared statement pointer + * @returns SQL + */ + sql(stmt: number): string; + + /** + * SQL statement iterator + * + * This is a convenience function that manages statement compilation, + * replacing boilerplate code associated with calling {@link prepare_v2} + * directly. It is typically used with a `for await` loop (in an + * async function), like this: + * ```javascript + * // Compile one statement on each iteration of this loop. + * for await (const stmt of sqlite3.statements(db, sql)) { + * // Bind parameters here if using SQLite placeholders. + * + * // Execute the statement with this loop. + * while (await sqlite3.step(stmt) === SQLite.SQLITE_ROW) { + * // Collect row data here. + * } + * + * // Change bindings, reset, and execute again if desired. + * } + * ``` + * + * {@link finalize} should *not* be called on a statement provided + * by the iterator; the statement resources will be released + * automatically at the end of each iteration. This also means + * that the statement is only valid within the scope of the loop - + * use {@link prepare_v2} directly to compile a statement with an + * application-specified lifetime. + * + * If using the iterator manually, i.e. by calling its `next` + * method, be sure to call the `return` method if iteration + * is abandoned before completion (`for await` and other implicit + * traversals provided by Javascript do this automatically) + * to ensure that all allocated resources are released. + * @param db database pointer + * @param sql + */ + statements(db: number, sql: string): AsyncIterable; + + /** + * Evaluate an SQL statement + * @see https://www.sqlite.org/c3ref/step.html + * @param stmt prepared statement pointer + * @returns Promise resolving to `SQLITE_ROW` or `SQLITE_DONE` + * (rejects on error) + */ + step(stmt: number): Promise; + + /** + * Create a new `sqlite3_str` dynamic string instance + * + * The purpose for `sqlite3_str` is to transfer a SQL string in + * Javascript to WebAssembly memory for use with {@link prepare_v2}. + * + * An optional initialization argument has been added for convenience + * which is functionally equivalent to (but slightly more efficient): + * ```javascript + * const str = sqlite3.str_new(db); + * sqlite3.str_appendall(str, s); + * ``` + * + * A `sqlite3_str` instance should always be destroyed with + * {@link str_finish} after use to avoid a resource leak. + * + * @see https://www.sqlite.org/c3ref/str_append.html + * @param db database pointer + * @param s optional initialization string + * @returns `sqlite3_str` pointer + */ + str_new(db: number, s?: string): number; + + /** + * Add content to a `sqlite3_str` dynamic string + * + * Not recommended for building strings incrementally; prefer using + * Javascript and {@link str_new} with initialization. + * @see https://www.sqlite.org/c3ref/str_append.html + * @param str `sqlite3_str` pointer + * @param s string to append + */ + str_appendall(str: number, s: string): void; + + /** + * Get pointer to `sqlite3_str` dynamic string data + * + * The returned pointer points to the UTF-8 encoded string in + * WebAssembly memory. Use as input with {@link prepare_v2}. + * @see https://www.sqlite.org/c3ref/str_errcode.html + * @param str `sqlite3_str` pointer + * @returns pointer to string data + */ + str_value(str: number): number; + + /** + * Finalize a `sqlite3_str` dynamic string created with {@link str_new} + * @see https://www.sqlite.org/c3ref/str_append.html + * @param str `sqlite3_str` pointer + */ + str_finish(str: number): void; + + /** + * Get application data in custom function implementation + * @see https://sqlite.org/c3ref/user_data.html + * @param context context pointer + * @returns application data + */ + user_data(context: number): any; + + /** + * Extract a value from `sqlite3_value` + * + * This is a convenience function that calls the appropriate `value_*` + * function based on its type. Note that if the value is a blob then as + * with `value_blob` the result may be invalid after the next SQLite call. + * + * Integer values are returned as Number if within the min/max safe + * integer bounds, otherwise they are returned as BigInt. + * @param pValue `sqlite3_value` pointer + * @returns value + */ + value(pValue: number): SQLiteCompatibleType; + + /** + * Extract a value from `sqlite3_value` + * + * The contents of the returned buffer may be invalid after the + * next SQLite call. Make a copy of the data (e.g. with `.slice()`) + * if longer retention is required. + * @see https://sqlite.org/c3ref/value_blob.html + * @param pValue `sqlite3_value` pointer + * @returns value + */ + value_blob(pValue: number): Uint8Array; + + /** + * Get blob or text size for value + * @see https://sqlite.org/c3ref/value_blob.html + * @param pValue `sqlite3_value` pointer + * @returns size + */ + value_bytes(pValue: number): number; + + /** + * Extract a value from `sqlite3_value` + * @see https://sqlite.org/c3ref/value_blob.html + * @param pValue `sqlite3_value` pointer + * @returns value + */ + value_double(pValue: number): number; + + /** + * Extract a value from `sqlite3_value` + * @see https://sqlite.org/c3ref/value_blob.html + * @param pValue `sqlite3_value` pointer + * @returns value + */ + value_int(pValue: number): number; + + /** + * Extract a value from `sqlite3_value` + * @see https://sqlite.org/c3ref/value_blob.html + * @param pValue `sqlite3_value` pointer + * @returns value + */ + value_int64(pValue: number): bigint; + + /** + * Extract a value from `sqlite3_value` + * @see https://sqlite.org/c3ref/value_blob.html + * @param pValue `sqlite3_value` pointer + * @returns value + */ + value_text(pValue: number): string; + + /** + * Get type of `sqlite3_value` + * @see https://sqlite.org/c3ref/value_blob.html + * @param pValue `sqlite3_value` pointer + * @returns enumeration value for type + */ + value_type(pValue: number): number; + + /** + * Register a new Virtual File System. + * + * @see https://www.sqlite.org/c3ref/vfs_find.html + * @param vfs VFS object + * @param makeDefault + * @returns `SQLITE_OK` (throws exception on error) + */ + vfs_register(vfs: SQLiteVFS, makeDefault?: boolean): number; +} + +/** @ignore */ +declare module "wa-sqlite/src/sqlite-constants.js" { + export const SQLITE_OK: 0; + export const SQLITE_ERROR: 1; + export const SQLITE_INTERNAL: 2; + export const SQLITE_PERM: 3; + export const SQLITE_ABORT: 4; + export const SQLITE_BUSY: 5; + export const SQLITE_LOCKED: 6; + export const SQLITE_NOMEM: 7; + export const SQLITE_READONLY: 8; + export const SQLITE_INTERRUPT: 9; + export const SQLITE_IOERR: 10; + export const SQLITE_CORRUPT: 11; + export const SQLITE_NOTFOUND: 12; + export const SQLITE_FULL: 13; + export const SQLITE_CANTOPEN: 14; + export const SQLITE_PROTOCOL: 15; + export const SQLITE_EMPTY: 16; + export const SQLITE_SCHEMA: 17; + export const SQLITE_TOOBIG: 18; + export const SQLITE_CONSTRAINT: 19; + export const SQLITE_MISMATCH: 20; + export const SQLITE_MISUSE: 21; + export const SQLITE_NOLFS: 22; + export const SQLITE_AUTH: 23; + export const SQLITE_FORMAT: 24; + export const SQLITE_RANGE: 25; + export const SQLITE_NOTADB: 26; + export const SQLITE_NOTICE: 27; + export const SQLITE_WARNING: 28; + export const SQLITE_ROW: 100; + export const SQLITE_DONE: 101; + export const SQLITE_IOERR_ACCESS: 3338; + export const SQLITE_IOERR_CHECKRESERVEDLOCK: 3594; + export const SQLITE_IOERR_CLOSE: 4106; + export const SQLITE_IOERR_DATA: 8202; + export const SQLITE_IOERR_DELETE: 2570; + export const SQLITE_IOERR_DELETE_NOENT: 5898; + export const SQLITE_IOERR_DIR_FSYNC: 1290; + export const SQLITE_IOERR_FSTAT: 1802; + export const SQLITE_IOERR_FSYNC: 1034; + export const SQLITE_IOERR_GETTEMPPATH: 6410; + export const SQLITE_IOERR_LOCK: 3850; + export const SQLITE_IOERR_NOMEM: 3082; + export const SQLITE_IOERR_READ: 266; + export const SQLITE_IOERR_RDLOCK: 2314; + export const SQLITE_IOERR_SEEK: 5642; + export const SQLITE_IOERR_SHORT_READ: 522; + export const SQLITE_IOERR_TRUNCATE: 1546; + export const SQLITE_IOERR_UNLOCK: 2058; + export const SQLITE_IOERR_VNODE: 6922; + export const SQLITE_IOERR_WRITE: 778; + export const SQLITE_IOERR_BEGIN_ATOMIC: 7434; + export const SQLITE_IOERR_COMMIT_ATOMIC: 7690; + export const SQLITE_IOERR_ROLLBACK_ATOMIC: 7946; + export const SQLITE_CONSTRAINT_CHECK: 275; + export const SQLITE_CONSTRAINT_COMMITHOOK: 531; + export const SQLITE_CONSTRAINT_FOREIGNKEY: 787; + export const SQLITE_CONSTRAINT_FUNCTION: 1043; + export const SQLITE_CONSTRAINT_NOTNULL: 1299; + export const SQLITE_CONSTRAINT_PINNED: 2835; + export const SQLITE_CONSTRAINT_PRIMARYKEY: 1555; + export const SQLITE_CONSTRAINT_ROWID: 2579; + export const SQLITE_CONSTRAINT_TRIGGER: 1811; + export const SQLITE_CONSTRAINT_UNIQUE: 2067; + export const SQLITE_CONSTRAINT_VTAB: 2323; + export const SQLITE_OPEN_READONLY: 1; + export const SQLITE_OPEN_READWRITE: 2; + export const SQLITE_OPEN_CREATE: 4; + export const SQLITE_OPEN_DELETEONCLOSE: 8; + export const SQLITE_OPEN_EXCLUSIVE: 16; + export const SQLITE_OPEN_AUTOPROXY: 32; + export const SQLITE_OPEN_URI: 64; + export const SQLITE_OPEN_MEMORY: 128; + export const SQLITE_OPEN_MAIN_DB: 256; + export const SQLITE_OPEN_TEMP_DB: 512; + export const SQLITE_OPEN_TRANSIENT_DB: 1024; + export const SQLITE_OPEN_MAIN_JOURNAL: 2048; + export const SQLITE_OPEN_TEMP_JOURNAL: 4096; + export const SQLITE_OPEN_SUBJOURNAL: 8192; + export const SQLITE_OPEN_SUPER_JOURNAL: 16384; + export const SQLITE_OPEN_NOMUTEX: 32768; + export const SQLITE_OPEN_FULLMUTEX: 65536; + export const SQLITE_OPEN_SHAREDCACHE: 131072; + export const SQLITE_OPEN_PRIVATECACHE: 262144; + export const SQLITE_OPEN_WAL: 524288; + export const SQLITE_OPEN_NOFOLLOW: 16777216; + export const SQLITE_LOCK_NONE: 0; + export const SQLITE_LOCK_SHARED: 1; + export const SQLITE_LOCK_RESERVED: 2; + export const SQLITE_LOCK_PENDING: 3; + export const SQLITE_LOCK_EXCLUSIVE: 4; + export const SQLITE_IOCAP_ATOMIC: 1; + export const SQLITE_IOCAP_ATOMIC512: 2; + export const SQLITE_IOCAP_ATOMIC1K: 4; + export const SQLITE_IOCAP_ATOMIC2K: 8; + export const SQLITE_IOCAP_ATOMIC4K: 16; + export const SQLITE_IOCAP_ATOMIC8K: 32; + export const SQLITE_IOCAP_ATOMIC16K: 64; + export const SQLITE_IOCAP_ATOMIC32K: 128; + export const SQLITE_IOCAP_ATOMIC64K: 256; + export const SQLITE_IOCAP_SAFE_APPEND: 512; + export const SQLITE_IOCAP_SEQUENTIAL: 1024; + export const SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN: 2048; + export const SQLITE_IOCAP_POWERSAFE_OVERWRITE: 4096; + export const SQLITE_IOCAP_IMMUTABLE: 8192; + export const SQLITE_IOCAP_BATCH_ATOMIC: 16384; + export const SQLITE_ACCESS_EXISTS: 0; + export const SQLITE_ACCESS_READWRITE: 1; + export const SQLITE_ACCESS_READ: 2; + export const SQLITE_FCNTL_LOCKSTATE: 1; + export const SQLITE_FCNTL_GET_LOCKPROXYFILE: 2; + export const SQLITE_FCNTL_SET_LOCKPROXYFILE: 3; + export const SQLITE_FCNTL_LAST_ERRNO: 4; + export const SQLITE_FCNTL_SIZE_HINT: 5; + export const SQLITE_FCNTL_CHUNK_SIZE: 6; + export const SQLITE_FCNTL_FILE_POINTER: 7; + export const SQLITE_FCNTL_SYNC_OMITTED: 8; + export const SQLITE_FCNTL_WIN32_AV_RETRY: 9; + export const SQLITE_FCNTL_PERSIST_WAL: 10; + export const SQLITE_FCNTL_OVERWRITE: 11; + export const SQLITE_FCNTL_VFSNAME: 12; + export const SQLITE_FCNTL_POWERSAFE_OVERWRITE: 13; + export const SQLITE_FCNTL_PRAGMA: 14; + export const SQLITE_FCNTL_BUSYHANDLER: 15; + export const SQLITE_FCNTL_TEMPFILENAME: 16; + export const SQLITE_FCNTL_MMAP_SIZE: 18; + export const SQLITE_FCNTL_TRACE: 19; + export const SQLITE_FCNTL_HAS_MOVED: 20; + export const SQLITE_FCNTL_SYNC: 21; + export const SQLITE_FCNTL_COMMIT_PHASETWO: 22; + export const SQLITE_FCNTL_WIN32_SET_HANDLE: 23; + export const SQLITE_FCNTL_WAL_BLOCK: 24; + export const SQLITE_FCNTL_ZIPVFS: 25; + export const SQLITE_FCNTL_RBU: 26; + export const SQLITE_FCNTL_VFS_POINTER: 27; + export const SQLITE_FCNTL_JOURNAL_POINTER: 28; + export const SQLITE_FCNTL_WIN32_GET_HANDLE: 29; + export const SQLITE_FCNTL_PDB: 30; + export const SQLITE_FCNTL_BEGIN_ATOMIC_WRITE: 31; + export const SQLITE_FCNTL_COMMIT_ATOMIC_WRITE: 32; + export const SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE: 33; + export const SQLITE_FCNTL_LOCK_TIMEOUT: 34; + export const SQLITE_FCNTL_DATA_VERSION: 35; + export const SQLITE_FCNTL_SIZE_LIMIT: 36; + export const SQLITE_FCNTL_CKPT_DONE: 37; + export const SQLITE_FCNTL_RESERVE_BYTES: 38; + export const SQLITE_FCNTL_CKPT_START: 39; + export const SQLITE_INTEGER: 1; + export const SQLITE_FLOAT: 2; + export const SQLITE_TEXT: 3; + export const SQLITE_BLOB: 4; + export const SQLITE_NULL: 5; + export const SQLITE_STATIC: 0; + export const SQLITE_TRANSIENT: -1; + export const SQLITE_UTF8: 1; + export const SQLITE_UTF16LE: 2; + export const SQLITE_UTF16BE: 3; + export const SQLITE_UTF16: 4; + export const SQLITE_INDEX_CONSTRAINT_EQ: 2; + export const SQLITE_INDEX_CONSTRAINT_GT: 4; + export const SQLITE_INDEX_CONSTRAINT_LE: 8; + export const SQLITE_INDEX_CONSTRAINT_LT: 16; + export const SQLITE_INDEX_CONSTRAINT_GE: 32; + export const SQLITE_INDEX_CONSTRAINT_MATCH: 64; + export const SQLITE_INDEX_CONSTRAINT_LIKE: 65; + export const SQLITE_INDEX_CONSTRAINT_GLOB: 66; + export const SQLITE_INDEX_CONSTRAINT_REGEXP: 67; + export const SQLITE_INDEX_CONSTRAINT_NE: 68; + export const SQLITE_INDEX_CONSTRAINT_ISNOT: 69; + export const SQLITE_INDEX_CONSTRAINT_ISNOTNULL: 70; + export const SQLITE_INDEX_CONSTRAINT_ISNULL: 71; + export const SQLITE_INDEX_CONSTRAINT_IS: 72; + export const SQLITE_INDEX_CONSTRAINT_FUNCTION: 150; + export const SQLITE_INDEX_SCAN_UNIQUE: 1; + export const SQLITE_DETERMINISTIC: 0x000000800; + export const SQLITE_DIRECTONLY: 0x000080000; + export const SQLITE_SUBTYPE: 0x000100000; + export const SQLITE_INNOCUOUS: 0x000200000; + export const SQLITE_SYNC_NORMAL: 0x00002; + export const SQLITE_SYNC_FULL: 0x00003; + export const SQLITE_SYNC_DATAONLY: 0x00010; + export const SQLITE_CREATE_INDEX: 1; + export const SQLITE_CREATE_TABLE: 2; + export const SQLITE_CREATE_TEMP_INDEX: 3; + export const SQLITE_CREATE_TEMP_TABLE: 4; + export const SQLITE_CREATE_TEMP_TRIGGER: 5; + export const SQLITE_CREATE_TEMP_VIEW: 6; + export const SQLITE_CREATE_TRIGGER: 7; + export const SQLITE_CREATE_VIEW: 8; + export const SQLITE_DELETE: 9; + export const SQLITE_DROP_INDEX: 10; + export const SQLITE_DROP_TABLE: 11; + export const SQLITE_DROP_TEMP_INDEX: 12; + export const SQLITE_DROP_TEMP_TABLE: 13; + export const SQLITE_DROP_TEMP_TRIGGER: 14; + export const SQLITE_DROP_TEMP_VIEW: 15; + export const SQLITE_DROP_TRIGGER: 16; + export const SQLITE_DROP_VIEW: 17; + export const SQLITE_INSERT: 18; + export const SQLITE_PRAGMA: 19; + export const SQLITE_READ: 20; + export const SQLITE_SELECT: 21; + export const SQLITE_TRANSACTION: 22; + export const SQLITE_UPDATE: 23; + export const SQLITE_ATTACH: 24; + export const SQLITE_DETACH: 25; + export const SQLITE_ALTER_TABLE: 26; + export const SQLITE_REINDEX: 27; + export const SQLITE_ANALYZE: 28; + export const SQLITE_CREATE_VTABLE: 29; + export const SQLITE_DROP_VTABLE: 30; + export const SQLITE_FUNCTION: 31; + export const SQLITE_SAVEPOINT: 32; + export const SQLITE_COPY: 0; + export const SQLITE_RECURSIVE: 33; + export const SQLITE_DENY: 1; + export const SQLITE_IGNORE: 2; + export const SQLITE_LIMIT_LENGTH: 0; + export const SQLITE_LIMIT_SQL_LENGTH: 1; + export const SQLITE_LIMIT_COLUMN: 2; + export const SQLITE_LIMIT_EXPR_DEPTH: 3; + export const SQLITE_LIMIT_COMPOUND_SELECT: 4; + export const SQLITE_LIMIT_VDBE_OP: 5; + export const SQLITE_LIMIT_FUNCTION_ARG: 6; + export const SQLITE_LIMIT_ATTACHED: 7; + export const SQLITE_LIMIT_LIKE_PATTERN_LENGTH: 8; + export const SQLITE_LIMIT_VARIABLE_NUMBER: 9; + export const SQLITE_LIMIT_TRIGGER_DEPTH: 10; + export const SQLITE_LIMIT_WORKER_THREADS: 11; +} + +/** @ignore */ +declare module "wa-sqlite" { + export * from "wa-sqlite/src/sqlite-constants.js"; + + /** + * Builds a Javascript API from the Emscripten module. This API is still + * low-level and closely corresponds to the C API exported by the module, + * but differs in some specifics like throwing exceptions on errors. + * @param {*} Module SQLite module + * @returns {SQLiteAPI} + */ + export function Factory(Module: any): SQLiteAPI; + + export class SQLiteError extends Error { + constructor(message: any, code: any); + code: any; + } +} + +/** @ignore */ +declare module "wa-sqlite/dist/wa-sqlite.mjs" { + function ModuleFactory(config?: object): Promise; + export = ModuleFactory; +} + +/** @ignore */ +declare module "wa-sqlite/dist/wa-sqlite-async.mjs" { + function ModuleFactory(config?: object): Promise; + export = ModuleFactory; +} + +/** @ignore */ +declare module "wa-sqlite/src/VFS.js" { + export * from "wa-sqlite/src/sqlite-constants.js"; + + export class Base { + mxPathName: number; + /** + * @param {number} fileId + * @returns {number|Promise} + */ + xClose(fileId: number): number; + /** + * @param {number} fileId + * @param {Uint8Array} pData + * @param {number} iOffset + * @returns {number} + */ + xRead( + fileId: number, + pData: { + size: number; + value: Uint8Array; + }, + iOffset: number + ): number; + /** + * @param {number} fileId + * @param {Uint8Array} pData + * @param {number} iOffset + * @returns {number} + */ + xWrite( + fileId: number, + pData: { + size: number; + value: Uint8Array; + }, + iOffset: number + ): number; + /** + * @param {number} fileId + * @param {number} iSize + * @returns {number} + */ + xTruncate(fileId: number, iSize: number): number; + /** + * @param {number} fileId + * @param {*} flags + * @returns {number} + */ + xSync(fileId: number, flags: any): number; + /** + * @param {number} fileId + * @param {DataView} pSize64 + * @returns {number|Promise} + */ + xFileSize(fileId: number, pSize64: DataView): number; + /** + * @param {number} fileId + * @param {number} flags + * @returns {number} + */ + xLock(fileId: number, flags: number): number; + /** + * @param {number} fileId + * @param {number} flags + * @returns {number} + */ + xUnlock(fileId: number, flags: number): number; + /** + * @param {number} fileId + * @param {DataView} pResOut + * @returns {number} + */ + xCheckReservedLock(fileId: number, pResOut: DataView): number; + /** + * @param {number} fileId + * @param {number} flags + * @param {DataView} pArg + * @returns {number} + */ + xFileControl(fileId: number, flags: number, pArg: DataView): number; + /** + * @param {number} fileId + * @returns {number} + */ + xSectorSize(fileId: number): number; + /** + * @param {number} fileId + * @returns {number} + */ + xDeviceCharacteristics(fileId: number): number; + /** + * @param {string?} name + * @param {number} fileId + * @param {number} flags + * @param {DataView} pOutFlags + * @returns {number} + */ + xOpen( + name: string | null, + fileId: number, + flags: number, + pOutFlags: DataView + ): number; + /** + * + * @param {string} name + * @param {number} syncDir + * @returns {number} + */ + xDelete(name: string, syncDir: number): number; + /** + * @param {string} name + * @param {number} flags + * @param {DataView} pResOut + * @returns {number} + */ + xAccess(name: string, flags: number, pResOut: DataView): number; + /** + * Handle asynchronous operation. This implementation will be overriden on + * registration by an Asyncify build. + * @param {function(): Promise} f + * @returns {number} + */ + handleAsync(f: () => Promise): number; + } +} + +/** @ignore */ +declare module "wa-sqlite/src/examples/ArrayModule.js" { + export class ArrayModule { + /** + * @param {SQLiteAPI} sqlite3 + * @param {number} db + * @param {Array} rows Table data. + * @param {Array} columns Column names. + */ + constructor( + sqlite3: any, + db: number, + rows: Array, + columns: Array + ); + mapCursorToState: Map; + sqlite3: any; + db: number; + rows: any[][]; + columns: string[]; + /** + * @param {number} db + * @param {*} appData Application data passed to `SQLiteAPI.create_module`. + * @param {Array} argv + * @param {number} pVTab + * @param {{ set: function(string): void}} pzErr + * @returns {number|Promise} + */ + xCreate( + db: number, + appData: any, + argv: Array, + pVTab: number, + pzErr: { + set: (arg0: string) => void; + } + ): number | Promise; + /** + * @param {number} db + * @param {*} appData Application data passed to `SQLiteAPI.create_module`. + * @param {Array} argv + * @param {number} pVTab + * @param {{ set: function(string): void}} pzErr + * @returns {number|Promise} + */ + xConnect( + db: number, + appData: any, + argv: Array, + pVTab: number, + pzErr: { + set: (arg0: string) => void; + } + ): number | Promise; + /** + * @param {number} pVTab + * @param {SQLiteModuleIndexInfo} indexInfo + * @returns {number|Promise} + */ + xBestIndex(pVTab: number, indexInfo: any): number | Promise; + /** + * @param {number} pVTab + * @returns {number|Promise} + */ + xDisconnect(pVTab: number): number | Promise; + /** + * @param {number} pVTab + * @returns {number|Promise} + */ + xDestroy(pVTab: number): number | Promise; + /** + * @param {number} pVTab + * @param {number} pCursor + * @returns {number|Promise} + */ + xOpen(pVTab: number, pCursor: number): number | Promise; + /** + * @param {number} pCursor + * @returns {number|Promise} + */ + xClose(pCursor: number): number | Promise; + /** + * @param {number} pCursor + * @param {number} idxNum + * @param {string?} idxStr + * @param {Array} values + * @returns {number|Promise} + */ + xFilter( + pCursor: number, + idxNum: number, + idxStr: string | null, + values: Array + ): number | Promise; + /** + * @param {number} pCursor + * @returns {number|Promise} + */ + xNext(pCursor: number): number | Promise; + /** + * @param {number} pCursor + * @returns {number|Promise} + */ + xEof(pCursor: number): number | Promise; + /** + * @param {number} pCursor + * @param {number} pContext + * @param {number} iCol + * @returns {number|Promise} + */ + xColumn( + pCursor: number, + pContext: number, + iCol: number + ): number | Promise; + /** + * @param {number} pCursor + * @param {{ set: function(number): void}} pRowid + * @returns {number|Promise} + */ + xRowid( + pCursor: number, + pRowid: { + set: (arg0: number) => void; + } + ): number | Promise; + /** + * @param {number} pVTab + * @param {Array} values sqlite3_value pointers + * @param {{ set: function(number): void}} pRowid + * @returns {number|Promise} + */ + xUpdate( + pVTab: number, + values: Array, + pRowid: { + set: (arg0: number) => void; + } + ): number | Promise; + } +} + +/** @ignore */ +declare module "wa-sqlite/src/examples/ArrayAsyncModule.js" { + import { ArrayModule } from "wa-sqlite/src/examples/ArrayModule.js"; + export class ArrayAsyncModule extends ArrayModule { + /** + * @param {function} f + * @returns {Promise} + */ + handleAsync(f: Function): Promise; + } +} + +/** @ignore */ +declare module "wa-sqlite/src/examples/IndexedDbVFS.js" { + import * as VFS from "wa-sqlite/src/VFS.js"; + export class IndexedDbVFS extends VFS.Base { + /** + * @param {string} idbName Name of IndexedDB database. + */ + constructor(idbName?: string); + name: string; + mapIdToFile: Map; + cacheSize: number; + db: any; + close(): Promise; + /** + * Delete a file from IndexedDB. + * @param {string} name + */ + deleteFile(name: string): Promise; + /** + * Forcibly clear an orphaned file lock. + * @param {string} name + */ + forceClearLock(name: string): Promise; + _getStore(mode?: string): any; + /** + * Returns the key for file metadata. + * @param {string} name + * @returns + */ + _metaKey(name: string): string; + /** + * Returns the key for file block data. + * @param {string} name + * @param {number} index + * @returns + */ + _blockKey(name: string, index: number): string; + _getBlock(store: any, file: any, index: any): Promise; + _putBlock(store: any, file: any, index: any, blockData: any): void; + _purgeCache(store: any, file: any, size?: number): void; + _flushCache(store: any, file: any): Promise; + _sync(file: any): Promise; + /** + * Helper function that deletes all keys greater or equal to `key` + * provided they start with `prefix`. + * @param {string} key + * @param {string} [prefix] + * @returns + */ + _delete(key: string, prefix?: string): Promise; + } +} + +/** @ignore */ +declare module "wa-sqlite/src/examples/MemoryVFS.js" { + import * as VFS from "wa-sqlite/src/VFS.js"; + export class MemoryVFS extends VFS.Base { + name: string; + mapNameToFile: Map; + mapIdToFile: Map; + } +} + +/** @ignore */ +declare module "wa-sqlite/src/examples/MemoryAsyncVFS.js" { + import { MemoryVFS } from "wa-sqlite/src/examples/MemoryVFS.js"; + export class MemoryAsyncVFS extends MemoryVFS {} +} + +/** @ignore */ +declare module "wa-sqlite/src/examples/tag.js" { + /** + * Template tag builder. This function creates a tag with an API and + * database from the same module, then the tag can be used like this: + * ``` + * const sql = tag(sqlite3, db); + * const results = await sql` + * SELECT 1 + 1; + * SELECT 6 * 7; + * `; + * ``` + * The returned Promise value contains an array of results for each + * SQL statement that produces output. Each result is an object with + * properties `columns` (array of names) and `rows` (array of array + * of values). + * @param {SQLiteAPI} sqlite3 + * @param {number} db + * @returns {function(TemplateStringsArray, ...any): Promise} + */ + export function tag( + sqlite3: any, + db: number + ): (arg0: TemplateStringsArray, ...args: any[]) => Promise; +} diff --git a/apps/web/src/common/sqlite/sqlite-api.js b/apps/web/src/common/sqlite/sqlite-api.js new file mode 100644 index 000000000..3940cc117 --- /dev/null +++ b/apps/web/src/common/sqlite/sqlite-api.js @@ -0,0 +1,958 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import * as SQLite from "./sqlite-constants.js"; +export * from "./sqlite-constants.js"; + +const MAX_INT64 = 0x7fffffffffffffffn; +const MIN_INT64 = -0x8000000000000000n; + +export class SQLiteError extends Error { + constructor(message, code) { + super(message); + this.code = code; + } +} + +const async = true; + +/** + * Builds a Javascript API from the Emscripten module. This API is still + * low-level and closely corresponds to the C API exported by the module, + * but differs in some specifics like throwing exceptions on errors. + * @param {*} Module SQLite Emscripten module + */ +export function Factory(Module) { + /** @type {SQLiteAPI} */ + const sqlite3 = {}; + + const sqliteFreeAddress = Module._getSqliteFree(); + + // Allocate some space for 32-bit returned values. + const tmp = Module._malloc(8); + const tmpPtr = [tmp, tmp + 4]; + + // Convert a JS string to a C string. sqlite3_malloc is used to allocate + // memory (use sqlite3_free to deallocate). + function createUTF8(s) { + if (typeof s !== "string") return 0; + const n = Module.lengthBytesUTF8(s); + const zts = Module._sqlite3_malloc(n + 1); + Module.stringToUTF8(s, zts, n + 1); + return zts; + } + + /** + * Concatenate 32-bit numbers into a 64-bit (signed) BigInt. + * @param {number} lo32 + * @param {number} hi32 + * @returns {bigint} + */ + function cvt32x2ToBigInt(lo32, hi32) { + return (BigInt(hi32) << 32n) | (BigInt(lo32) & 0xffffffffn); + } + + /** + * Concatenate 32-bit numbers and return as number or BigInt, depending + * on the value. + * @param {number} lo32 + * @param {number} hi32 + * @returns {number|bigint} + */ + const cvt32x2AsSafe = (function () { + const hiMax = BigInt(Number.MAX_SAFE_INTEGER) >> 32n; + const hiMin = BigInt(Number.MIN_SAFE_INTEGER) >> 32n; + + return function (lo32, hi32) { + if (hi32 > hiMax || hi32 < hiMin) { + // Can't be expressed as a Number so use BigInt. + return cvt32x2ToBigInt(lo32, hi32); + } else { + // Combine the upper and lower 32-bit numbers. The complication is + // that lo32 is a signed integer which makes manipulating its bits + // a little tricky - the sign bit gets handled separately. + return hi32 * 0x100000000 + (lo32 & 0x7fffffff) - (lo32 & 0x80000000); + } + }; + })(); + + const databases = new Set(); + function verifyDatabase(db) { + if (!databases.has(db)) { + throw new SQLiteError("not a database", SQLite.SQLITE_MISUSE); + } + } + + const mapStmtToDB = new Map(); + function verifyStatement(stmt) { + if (!mapStmtToDB.has(stmt)) { + throw new SQLiteError("not a statement", SQLite.SQLITE_MISUSE); + } + } + + sqlite3.bind_collection = function (stmt, bindings) { + verifyStatement(stmt); + const isArray = Array.isArray(bindings); + const nBindings = sqlite3.bind_parameter_count(stmt); + for (let i = 1; i <= nBindings; ++i) { + const key = isArray ? i - 1 : sqlite3.bind_parameter_name(stmt, i); + const value = bindings[key]; + if (value !== undefined) { + sqlite3.bind(stmt, i, value); + } + } + return SQLite.SQLITE_OK; + }; + + sqlite3.bind = function (stmt, i, value) { + verifyStatement(stmt); + switch (typeof value) { + case "number": + if (value === (value | 0)) { + return sqlite3.bind_int(stmt, i, value); + } else { + return sqlite3.bind_double(stmt, i, value); + } + case "string": + return sqlite3.bind_text(stmt, i, value); + default: + if (value instanceof Uint8Array || Array.isArray(value)) { + return sqlite3.bind_blob(stmt, i, value); + } else if (value === null) { + return sqlite3.bind_null(stmt, i); + } else if (typeof value === "bigint") { + return sqlite3.bind_int64(stmt, i, value); + } else if (value === undefined) { + // Existing binding (or NULL) will be used. + return SQLite.SQLITE_NOTICE; + } else { + console.warn("unknown binding converted to null", value); + return sqlite3.bind_null(stmt, i); + } + } + }; + + sqlite3.bind_blob = (function () { + const fname = "sqlite3_bind_blob"; + const f = Module.cwrap(fname, ...decl("nnnnn:n")); + return function (stmt, i, value) { + verifyStatement(stmt); + // @ts-ignore + const byteLength = value.byteLength ?? value.length; + const ptr = Module._sqlite3_malloc(byteLength); + Module.HEAPU8.subarray(ptr).set(value); + const result = f(stmt, i, ptr, byteLength, sqliteFreeAddress); + // trace(fname, result); + return check(fname, result, mapStmtToDB.get(stmt)); + }; + })(); + + sqlite3.bind_parameter_count = (function () { + const fname = "sqlite3_bind_parameter_count"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (stmt) { + verifyStatement(stmt); + const result = f(stmt); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.clear_bindings = (function () { + const fname = "sqlite3_clear_bindings"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (stmt) { + verifyStatement(stmt); + const result = f(stmt); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.bind_double = (function () { + const fname = "sqlite3_bind_double"; + const f = Module.cwrap(fname, ...decl("nnn:n")); + return function (stmt, i, value) { + verifyStatement(stmt); + const result = f(stmt, i, value); + // trace(fname, result); + return check(fname, result, mapStmtToDB.get(stmt)); + }; + })(); + + sqlite3.bind_int = (function () { + const fname = "sqlite3_bind_int"; + const f = Module.cwrap(fname, ...decl("nnn:n")); + return function (stmt, i, value) { + verifyStatement(stmt); + if (value > 0x7fffffff || value < -0x80000000) return SQLite.SQLITE_RANGE; + + const result = f(stmt, i, value); + // trace(fname, result); + return check(fname, result, mapStmtToDB.get(stmt)); + }; + })(); + + sqlite3.bind_int64 = (function () { + const fname = "sqlite3_bind_int64"; + const f = Module.cwrap(fname, ...decl("nnnn:n")); + return function (stmt, i, value) { + verifyStatement(stmt); + if (value > MAX_INT64 || value < MIN_INT64) return SQLite.SQLITE_RANGE; + + const lo32 = value & 0xffffffffn; + const hi32 = value >> 32n; + const result = f(stmt, i, Number(lo32), Number(hi32)); + // trace(fname, result); + return check(fname, result, mapStmtToDB.get(stmt)); + }; + })(); + + sqlite3.bind_null = (function () { + const fname = "sqlite3_bind_null"; + const f = Module.cwrap(fname, ...decl("nn:n")); + return function (stmt, i) { + verifyStatement(stmt); + const result = f(stmt, i); + // trace(fname, result); + return check(fname, result, mapStmtToDB.get(stmt)); + }; + })(); + + sqlite3.bind_parameter_name = (function () { + const fname = "sqlite3_bind_parameter_name"; + const f = Module.cwrap(fname, ...decl("n:s")); + return function (stmt, i) { + verifyStatement(stmt); + const result = f(stmt, i); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.bind_text = (function () { + const fname = "sqlite3_bind_text"; + const f = Module.cwrap(fname, ...decl("nnnnn:n")); + return function (stmt, i, value) { + verifyStatement(stmt); + const ptr = createUTF8(value); + const result = f(stmt, i, ptr, -1, sqliteFreeAddress); + // trace(fname, result); + return check(fname, result, mapStmtToDB.get(stmt)); + }; + })(); + + sqlite3.changes = (function () { + const fname = "sqlite3_changes"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (db) { + verifyDatabase(db); + const result = f(db); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.close = (function () { + const fname = "sqlite3_close"; + const f = Module.cwrap(fname, ...decl("n:n"), { async }); + return async function (db) { + verifyDatabase(db); + const result = await f(db); + databases.delete(db); + return check(fname, result, db); + }; + })(); + + sqlite3.column = function (stmt, iCol) { + verifyStatement(stmt); + const type = sqlite3.column_type(stmt, iCol); + switch (type) { + case SQLite.SQLITE_BLOB: + return sqlite3.column_blob(stmt, iCol); + case SQLite.SQLITE_FLOAT: + return sqlite3.column_double(stmt, iCol); + case SQLite.SQLITE_INTEGER: + const lo32 = sqlite3.column_int(stmt, iCol); + const hi32 = Module.getTempRet0(); + return cvt32x2AsSafe(lo32, hi32); + case SQLite.SQLITE_NULL: + return null; + case SQLite.SQLITE_TEXT: + return sqlite3.column_text(stmt, iCol); + default: + throw new SQLiteError("unknown type", type); + } + }; + + sqlite3.column_blob = (function () { + const fname = "sqlite3_column_blob"; + const f = Module.cwrap(fname, ...decl("nn:n")); + return function (stmt, iCol) { + verifyStatement(stmt); + const nBytes = sqlite3.column_bytes(stmt, iCol); + const address = f(stmt, iCol); + const result = Module.HEAPU8.subarray(address, address + nBytes); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.column_bytes = (function () { + const fname = "sqlite3_column_bytes"; + const f = Module.cwrap(fname, ...decl("nn:n")); + return function (stmt, iCol) { + verifyStatement(stmt); + const result = f(stmt, iCol); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.column_count = (function () { + const fname = "sqlite3_column_count"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (stmt) { + verifyStatement(stmt); + const result = f(stmt); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.column_double = (function () { + const fname = "sqlite3_column_double"; + const f = Module.cwrap(fname, ...decl("nn:n")); + return function (stmt, iCol) { + verifyStatement(stmt); + const result = f(stmt, iCol); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.column_int = (function () { + // Retrieve int64 but use only the lower 32 bits. The upper 32-bits are + // accessible with Module.getTempRet0(). + const fname = "sqlite3_column_int64"; + const f = Module.cwrap(fname, ...decl("nn:n")); + return function (stmt, iCol) { + verifyStatement(stmt); + const result = f(stmt, iCol); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.column_int64 = (function () { + const fname = "sqlite3_column_int64"; + const f = Module.cwrap(fname, ...decl("nn:n")); + return function (stmt, iCol) { + verifyStatement(stmt); + const lo32 = f(stmt, iCol); + const hi32 = Module.getTempRet0(); + const result = cvt32x2ToBigInt(lo32, hi32); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.column_name = (function () { + const fname = "sqlite3_column_name"; + const f = Module.cwrap(fname, ...decl("nn:s")); + return function (stmt, iCol) { + verifyStatement(stmt); + const result = f(stmt, iCol); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.column_names = function (stmt) { + const columns = []; + const nColumns = sqlite3.column_count(stmt); + for (let i = 0; i < nColumns; ++i) { + columns.push(sqlite3.column_name(stmt, i)); + } + return columns; + }; + + sqlite3.column_text = (function () { + const fname = "sqlite3_column_text"; + const f = Module.cwrap(fname, ...decl("nn:s")); + return function (stmt, iCol) { + verifyStatement(stmt); + const result = f(stmt, iCol); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.column_type = (function () { + const fname = "sqlite3_column_type"; + const f = Module.cwrap(fname, ...decl("nn:n")); + return function (stmt, iCol) { + verifyStatement(stmt); + const result = f(stmt, iCol); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.create_function = function ( + db, + zFunctionName, + nArg, + eTextRep, + pApp, + xFunc, + xStep, + xFinal + ) { + verifyDatabase(db); + if (xFunc && !xStep && !xFinal) { + const result = Module.createFunction( + db, + zFunctionName, + nArg, + eTextRep, + pApp, + xFunc + ); + return check("sqlite3_create_function", result, db); + } + + if (!xFunc && xStep && xFinal) { + const result = Module.createAggregate( + db, + zFunctionName, + nArg, + eTextRep, + pApp, + xStep, + xFinal + ); + return check("sqlite3_create_function", result, db); + } + + throw new SQLiteError("invalid function combination", SQLite.SQLITE_MISUSE); + }; + + sqlite3.create_module = function (db, zName, module, appData) { + verifyDatabase(db); + const result = Module.createModule(db, zName, module, appData); + return check("sqlite3_create_module", result, db); + }; + + sqlite3.data_count = (function () { + const fname = "sqlite3_data_count"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (stmt) { + verifyStatement(stmt); + const result = f(stmt); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.declare_vtab = (function () { + const fname = "sqlite3_declare_vtab"; + const f = Module.cwrap(fname, ...decl("ns:n")); + return function (pVTab, zSQL) { + const result = f(pVTab, zSQL); + return check("sqlite3_declare_vtab", result); + }; + })(); + + sqlite3.exec = async function (db, sql, callback) { + for await (const stmt of sqlite3.statements(db, sql)) { + let columns; + while ((await sqlite3.step(stmt)) === SQLite.SQLITE_ROW) { + if (callback) { + columns = columns ?? sqlite3.column_names(stmt); + const row = sqlite3.row(stmt); + await callback(row, columns); + } + } + } + return SQLite.SQLITE_OK; + }; + + sqlite3.finalize = (function () { + const fname = "sqlite3_finalize"; + const f = Module.cwrap(fname, ...decl("n:n"), { async }); + return async function (stmt) { + if (!mapStmtToDB.has(stmt)) { + return SQLite.SQLITE_MISUSE; + } + const result = await f(stmt); + + const db = mapStmtToDB.get(stmt); + mapStmtToDB.delete(stmt); + + // Don't throw on error here. Typically the error has already been + // thrown and finalize() is part of the cleanup. + return result; + }; + })(); + + sqlite3.get_autocommit = (function () { + const fname = "sqlite3_get_autocommit"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (db) { + const result = f(db); + return result; + }; + })(); + + sqlite3.libversion = (function () { + const fname = "sqlite3_libversion"; + const f = Module.cwrap(fname, ...decl(":s")); + return function () { + const result = f(); + return result; + }; + })(); + + sqlite3.libversion_number = (function () { + const fname = "sqlite3_libversion_number"; + const f = Module.cwrap(fname, ...decl(":n")); + return function () { + const result = f(); + return result; + }; + })(); + + sqlite3.limit = (function () { + const fname = "sqlite3_limit"; + const f = Module.cwrap(fname, ...decl("nnn:n")); + return function (db, id, newVal) { + const result = f(db, id, newVal); + return result; + }; + })(); + + sqlite3.open_v2 = (function () { + const fname = "sqlite3_open_v2"; + const f = Module.cwrap(fname, ...decl("snnn:n"), { async }); + return async function (zFilename, flags, zVfs) { + flags = flags || SQLite.SQLITE_OPEN_CREATE | SQLite.SQLITE_OPEN_READWRITE; + zVfs = createUTF8(zVfs); + const result = await f(zFilename, tmpPtr[0], flags, zVfs); + + const db = Module.getValue(tmpPtr[0], "*"); + databases.add(db); + Module._sqlite3_free(zVfs); + + Module.ccall("RegisterExtensionFunctions", "void", ["number"], [db]); + check(fname, result); + return db; + }; + })(); + + sqlite3.prepare_v2 = (function () { + const fname = "sqlite3_prepare_v2"; + const f = Module.cwrap(fname, ...decl("nnnnn:n"), { async }); + return async function (db, sql) { + const result = await f(db, sql, -1, tmpPtr[0], tmpPtr[1]); + check(fname, result, db); + + const stmt = Module.getValue(tmpPtr[0], "*"); + if (stmt) { + mapStmtToDB.set(stmt, db); + return { stmt, sql: Module.getValue(tmpPtr[1], "*") }; + } + return null; + }; + })(); + + sqlite3.progress_handler = function (db, nProgressOps, handler, userData) { + verifyDatabase(db); + Module.progressHandler(db, nProgressOps, handler, userData); + }; + + sqlite3.reset = (function () { + const fname = "sqlite3_reset"; + const f = Module.cwrap(fname, ...decl("n:n"), { async }); + return async function (stmt) { + verifyStatement(stmt); + const result = await f(stmt); + return check(fname, result, mapStmtToDB.get(stmt)); + }; + })(); + + sqlite3.result = function (context, value) { + switch (typeof value) { + case "number": + if (value === (value | 0)) { + sqlite3.result_int(context, value); + } else { + sqlite3.result_double(context, value); + } + break; + case "string": + sqlite3.result_text(context, value); + break; + default: + if (value instanceof Uint8Array || Array.isArray(value)) { + sqlite3.result_blob(context, value); + } else if (value === null) { + sqlite3.result_null(context); + } else if (typeof value === "bigint") { + return sqlite3.result_int64(context, value); + } else { + console.warn("unknown result converted to null", value); + sqlite3.result_null(context); + } + break; + } + }; + + sqlite3.result_blob = (function () { + const fname = "sqlite3_result_blob"; + const f = Module.cwrap(fname, ...decl("nnnn:n")); + return function (context, value) { + // @ts-ignore + const byteLength = value.byteLength ?? value.length; + const ptr = Module._sqlite3_malloc(byteLength); + Module.HEAPU8.subarray(ptr).set(value); + f(context, ptr, byteLength, sqliteFreeAddress); // void return + }; + })(); + + sqlite3.result_double = (function () { + const fname = "sqlite3_result_double"; + const f = Module.cwrap(fname, ...decl("nn:n")); + return function (context, value) { + f(context, value); // void return + }; + })(); + + sqlite3.result_int = (function () { + const fname = "sqlite3_result_int"; + const f = Module.cwrap(fname, ...decl("nn:n")); + return function (context, value) { + f(context, value); // void return + }; + })(); + + sqlite3.result_int64 = (function () { + const fname = "sqlite3_result_int64"; + const f = Module.cwrap(fname, ...decl("nnn:n")); + return function (context, value) { + if (value > MAX_INT64 || value < MIN_INT64) return SQLite.SQLITE_RANGE; + + const lo32 = value & 0xffffffffn; + const hi32 = value >> 32n; + f(context, Number(lo32), Number(hi32)); // void return + }; + })(); + + sqlite3.result_null = (function () { + const fname = "sqlite3_result_null"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (context) { + f(context); // void return + }; + })(); + + sqlite3.result_text = (function () { + const fname = "sqlite3_result_text"; + const f = Module.cwrap(fname, ...decl("nnnn:n")); + return function (context, value) { + const ptr = createUTF8(value); + f(context, ptr, -1, sqliteFreeAddress); // void return + }; + })(); + + sqlite3.row = function (stmt) { + const row = []; + const nColumns = sqlite3.data_count(stmt); + for (let i = 0; i < nColumns; ++i) { + const value = sqlite3.column(stmt, i); + + // Copy blob if aliasing volatile WebAssembly memory. This avoids an + // unnecessary copy if users monkey patch column_blob to copy. + // @ts-ignore + row.push(value?.buffer === Module.HEAPU8.buffer ? value.slice() : value); + } + return row; + }; + + sqlite3.set_authorizer = function (db, authFunction, userData) { + verifyDatabase(db); + const result = Module.setAuthorizer(db, authFunction, userData); + return check("sqlite3_set_authorizer", result, db); + }; + + sqlite3.sql = (function () { + const fname = "sqlite3_sql"; + const f = Module.cwrap(fname, ...decl("n:s")); + return function (stmt) { + verifyStatement(stmt); + const result = f(stmt); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.statements = function (db, sql) { + return (async function* () { + const str = sqlite3.str_new(db, sql); + let prepared = { stmt: null, sql: sqlite3.str_value(str) }; + try { + while ((prepared = await sqlite3.prepare_v2(db, prepared.sql))) { + // console.log(sqlite3.sql(prepared.stmt)); + yield prepared.stmt; + sqlite3.finalize(prepared.stmt); + prepared.stmt = null; + } + } finally { + if (prepared?.stmt) { + sqlite3.finalize(prepared.stmt); + } + sqlite3.str_finish(str); + } + })(); + }; + + sqlite3.step = (function () { + const fname = "sqlite3_step"; + const f = Module.cwrap(fname, ...decl("n:n"), { async }); + return async function (stmt) { + verifyStatement(stmt); + const result = await f(stmt); + return check(fname, result, mapStmtToDB.get(stmt), [ + SQLite.SQLITE_ROW, + SQLite.SQLITE_DONE + ]); + }; + })(); + + // Duplicate some of the SQLite dynamic string API but without + // calling SQLite (except for memory allocation). We need some way + // to transfer Javascript strings and might as well use an API + // that mimics the SQLite API. + let stringId = 0; + const strings = new Map(); + + sqlite3.str_new = function (db, s = "") { + const sBytes = Module.lengthBytesUTF8(s); + const str = stringId++ & 0xffffffff; + const data = { + offset: Module._sqlite3_malloc(sBytes + 1), + bytes: sBytes + }; + strings.set(str, data); + Module.stringToUTF8(s, data.offset, data.bytes + 1); + return str; + }; + + sqlite3.str_appendall = function (str, s) { + if (!strings.has(str)) { + throw new SQLiteError("not a string", SQLite.SQLITE_MISUSE); + } + const data = strings.get(str); + + const sBytes = Module.lengthBytesUTF8(s); + const newBytes = data.bytes + sBytes; + const newOffset = Module._sqlite3_malloc(newBytes + 1); + const newArray = Module.HEAPU8.subarray( + newOffset, + newOffset + newBytes + 1 + ); + newArray.set(Module.HEAPU8.subarray(data.offset, data.offset + data.bytes)); + Module.stringToUTF8(s, newOffset + data.bytes, sBytes + 1); + + Module._sqlite3_free(data.offset); + data.offset = newOffset; + data.bytes = newBytes; + strings.set(str, data); + }; + + sqlite3.str_finish = function (str) { + if (!strings.has(str)) { + throw new SQLiteError("not a string", SQLite.SQLITE_MISUSE); + } + const data = strings.get(str); + strings.delete(str); + Module._sqlite3_free(data.offset); + }; + + sqlite3.str_value = function (str) { + if (!strings.has(str)) { + throw new SQLiteError("not a string", SQLite.SQLITE_MISUSE); + } + return strings.get(str).offset; + }; + + sqlite3.user_data = function (context) { + return Module.getFunctionUserData(context); + }; + + sqlite3.value = function (pValue) { + const type = sqlite3.value_type(pValue); + switch (type) { + case SQLite.SQLITE_BLOB: + return sqlite3.value_blob(pValue); + case SQLite.SQLITE_FLOAT: + return sqlite3.value_double(pValue); + case SQLite.SQLITE_INTEGER: + const lo32 = sqlite3.value_int(pValue); + const hi32 = Module.getTempRet0(); + return cvt32x2AsSafe(lo32, hi32); + case SQLite.SQLITE_NULL: + return null; + case SQLite.SQLITE_TEXT: + return sqlite3.value_text(pValue); + default: + throw new SQLiteError("unknown type", type); + } + }; + + sqlite3.value_blob = (function () { + const fname = "sqlite3_value_blob"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (pValue) { + const nBytes = sqlite3.value_bytes(pValue); + const address = f(pValue); + const result = Module.HEAPU8.subarray(address, address + nBytes); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.value_bytes = (function () { + const fname = "sqlite3_value_bytes"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (pValue) { + const result = f(pValue); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.value_double = (function () { + const fname = "sqlite3_value_double"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (pValue) { + const result = f(pValue); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.value_int = (function () { + const fname = "sqlite3_value_int64"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (pValue) { + const result = f(pValue); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.value_int64 = (function () { + const fname = "sqlite3_value_int64"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (pValue) { + const lo32 = f(pValue); + const hi32 = Module.getTempRet0(); + const result = cvt32x2ToBigInt(lo32, hi32); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.value_text = (function () { + const fname = "sqlite3_value_text"; + const f = Module.cwrap(fname, ...decl("n:s")); + return function (pValue) { + const result = f(pValue); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.value_type = (function () { + const fname = "sqlite3_value_type"; + const f = Module.cwrap(fname, ...decl("n:n")); + return function (pValue) { + const result = f(pValue); + // trace(fname, result); + return result; + }; + })(); + + sqlite3.vfs_register = function (vfs, makeDefault) { + const result = Module.registerVFS(vfs, makeDefault); + return check("sqlite3_vfs_register", result); + }; + + function check(fname, result, db = null, allowed = [SQLite.SQLITE_OK]) { + // trace(fname, result); + if (allowed.includes(result)) return result; + const message = db + ? Module.ccall("sqlite3_errmsg", "string", ["number"], [db]) + : fname; + throw new SQLiteError(message, result); + } + + return sqlite3; +} + +function trace(...args) { + // const date = new Date(); + // const t = date.getHours().toString().padStart(2, '0') + ':' + + // date.getMinutes().toString().padStart(2, '0') + ':' + + // date.getSeconds().toString().padStart(2, '0') + '.' + + // date.getMilliseconds().toString().padStart(3, '0'); + // console.debug(t, ...args); +} + +// Helper function to use a more compact signature specification. +function decl(s) { + const result = []; + const m = s.match(/([ns@]*):([nsv@])/); + switch (m[2]) { + case "n": + result.push("number"); + break; + case "s": + result.push("string"); + break; + case "v": + result.push(null); + break; + } + + const args = []; + for (let c of m[1]) { + switch (c) { + case "n": + args.push("number"); + break; + case "s": + args.push("string"); + break; + } + } + result.push(args); + return result; +} diff --git a/apps/web/src/common/sqlite/sqlite-constants.js b/apps/web/src/common/sqlite/sqlite-constants.js new file mode 100644 index 000000000..d1474e9e5 --- /dev/null +++ b/apps/web/src/common/sqlite/sqlite-constants.js @@ -0,0 +1,288 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +export const SQLITE_OK = 0; +export const SQLITE_ERROR = 1; +export const SQLITE_INTERNAL = 2; +export const SQLITE_PERM = 3; +export const SQLITE_ABORT = 4; +export const SQLITE_BUSY = 5; +export const SQLITE_LOCKED = 6; +export const SQLITE_NOMEM = 7; +export const SQLITE_READONLY = 8; +export const SQLITE_INTERRUPT = 9; +export const SQLITE_IOERR = 10; +export const SQLITE_CORRUPT = 11; +export const SQLITE_NOTFOUND = 12; +export const SQLITE_FULL = 13; +export const SQLITE_CANTOPEN = 14; +export const SQLITE_PROTOCOL = 15; +export const SQLITE_EMPTY = 16; +export const SQLITE_SCHEMA = 17; +export const SQLITE_TOOBIG = 18; +export const SQLITE_CONSTRAINT = 19; +export const SQLITE_MISMATCH = 20; +export const SQLITE_MISUSE = 21; +export const SQLITE_NOLFS = 22; +export const SQLITE_AUTH = 23; +export const SQLITE_FORMAT = 24; +export const SQLITE_RANGE = 25; +export const SQLITE_NOTADB = 26; +export const SQLITE_NOTICE = 27; +export const SQLITE_WARNING = 28; +export const SQLITE_ROW = 100; +export const SQLITE_DONE = 101; + +// Extended error codes. +export const SQLITE_IOERR_ACCESS = 3338; +export const SQLITE_IOERR_CHECKRESERVEDLOCK = 3594; +export const SQLITE_IOERR_CLOSE = 4106; +export const SQLITE_IOERR_DATA = 8202; +export const SQLITE_IOERR_DELETE = 2570; +export const SQLITE_IOERR_DELETE_NOENT = 5898; +export const SQLITE_IOERR_DIR_FSYNC = 1290; +export const SQLITE_IOERR_FSTAT = 1802; +export const SQLITE_IOERR_FSYNC = 1034; +export const SQLITE_IOERR_GETTEMPPATH = 6410; +export const SQLITE_IOERR_LOCK = 3850; +export const SQLITE_IOERR_NOMEM = 3082; +export const SQLITE_IOERR_READ = 266; +export const SQLITE_IOERR_RDLOCK = 2314; +export const SQLITE_IOERR_SEEK = 5642; +export const SQLITE_IOERR_SHORT_READ = 522; +export const SQLITE_IOERR_TRUNCATE = 1546; +export const SQLITE_IOERR_UNLOCK = 2058; +export const SQLITE_IOERR_VNODE = 6922; +export const SQLITE_IOERR_WRITE = 778; +export const SQLITE_IOERR_BEGIN_ATOMIC = 7434; +export const SQLITE_IOERR_COMMIT_ATOMIC = 7690; +export const SQLITE_IOERR_ROLLBACK_ATOMIC = 7946; + +// Other extended result codes. +export const SQLITE_CONSTRAINT_CHECK = 275; +export const SQLITE_CONSTRAINT_COMMITHOOK = 531; +export const SQLITE_CONSTRAINT_FOREIGNKEY = 787; +export const SQLITE_CONSTRAINT_FUNCTION = 1043; +export const SQLITE_CONSTRAINT_NOTNULL = 1299; +export const SQLITE_CONSTRAINT_PINNED = 2835; +export const SQLITE_CONSTRAINT_PRIMARYKEY = 1555; +export const SQLITE_CONSTRAINT_ROWID = 2579; +export const SQLITE_CONSTRAINT_TRIGGER = 1811; +export const SQLITE_CONSTRAINT_UNIQUE = 2067; +export const SQLITE_CONSTRAINT_VTAB = 2323; + +// Open flags. +// https://www.sqlite.org/c3ref/c_open_autoproxy.html +export const SQLITE_OPEN_READONLY = 0x00000001; +export const SQLITE_OPEN_READWRITE = 0x00000002; +export const SQLITE_OPEN_CREATE = 0x00000004; +export const SQLITE_OPEN_DELETEONCLOSE = 0x00000008; +export const SQLITE_OPEN_EXCLUSIVE = 0x00000010; +export const SQLITE_OPEN_AUTOPROXY = 0x00000020; +export const SQLITE_OPEN_URI = 0x00000040; +export const SQLITE_OPEN_MEMORY = 0x00000080; +export const SQLITE_OPEN_MAIN_DB = 0x00000100; +export const SQLITE_OPEN_TEMP_DB = 0x00000200; +export const SQLITE_OPEN_TRANSIENT_DB = 0x00000400; +export const SQLITE_OPEN_MAIN_JOURNAL = 0x00000800; +export const SQLITE_OPEN_TEMP_JOURNAL = 0x00001000; +export const SQLITE_OPEN_SUBJOURNAL = 0x00002000; +export const SQLITE_OPEN_SUPER_JOURNAL = 0x00004000; +export const SQLITE_OPEN_NOMUTEX = 0x00008000; +export const SQLITE_OPEN_FULLMUTEX = 0x00010000; +export const SQLITE_OPEN_SHAREDCACHE = 0x00020000; +export const SQLITE_OPEN_PRIVATECACHE = 0x00040000; +export const SQLITE_OPEN_WAL = 0x00080000; +export const SQLITE_OPEN_NOFOLLOW = 0x01000000; + +// Locking levels. +// https://www.sqlite.org/c3ref/c_lock_exclusive.html +export const SQLITE_LOCK_NONE = 0; +export const SQLITE_LOCK_SHARED = 1; +export const SQLITE_LOCK_RESERVED = 2; +export const SQLITE_LOCK_PENDING = 3; +export const SQLITE_LOCK_EXCLUSIVE = 4; + +// Device characteristics. +// https://www.sqlite.org/c3ref/c_iocap_atomic.html +export const SQLITE_IOCAP_ATOMIC = 0x00000001; +export const SQLITE_IOCAP_ATOMIC512 = 0x00000002; +export const SQLITE_IOCAP_ATOMIC1K = 0x00000004; +export const SQLITE_IOCAP_ATOMIC2K = 0x00000008; +export const SQLITE_IOCAP_ATOMIC4K = 0x00000010; +export const SQLITE_IOCAP_ATOMIC8K = 0x00000020; +export const SQLITE_IOCAP_ATOMIC16K = 0x00000040; +export const SQLITE_IOCAP_ATOMIC32K = 0x00000080; +export const SQLITE_IOCAP_ATOMIC64K = 0x00000100; +export const SQLITE_IOCAP_SAFE_APPEND = 0x00000200; +export const SQLITE_IOCAP_SEQUENTIAL = 0x00000400; +export const SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN = 0x00000800; +export const SQLITE_IOCAP_POWERSAFE_OVERWRITE = 0x00001000; +export const SQLITE_IOCAP_IMMUTABLE = 0x00002000; +export const SQLITE_IOCAP_BATCH_ATOMIC = 0x00004000; + +// xAccess flags. +// https://www.sqlite.org/c3ref/c_access_exists.html +export const SQLITE_ACCESS_EXISTS = 0; +export const SQLITE_ACCESS_READWRITE = 1; +export const SQLITE_ACCESS_READ = 2; + +// File control opcodes +// https://www.sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlbeginatomicwrite +export const SQLITE_FCNTL_LOCKSTATE = 1; +export const SQLITE_FCNTL_GET_LOCKPROXYFILE = 2; +export const SQLITE_FCNTL_SET_LOCKPROXYFILE = 3; +export const SQLITE_FCNTL_LAST_ERRNO = 4; +export const SQLITE_FCNTL_SIZE_HINT = 5; +export const SQLITE_FCNTL_CHUNK_SIZE = 6; +export const SQLITE_FCNTL_FILE_POINTER = 7; +export const SQLITE_FCNTL_SYNC_OMITTED = 8; +export const SQLITE_FCNTL_WIN32_AV_RETRY = 9; +export const SQLITE_FCNTL_PERSIST_WAL = 10; +export const SQLITE_FCNTL_OVERWRITE = 11; +export const SQLITE_FCNTL_VFSNAME = 12; +export const SQLITE_FCNTL_POWERSAFE_OVERWRITE = 13; +export const SQLITE_FCNTL_PRAGMA = 14; +export const SQLITE_FCNTL_BUSYHANDLER = 15; +export const SQLITE_FCNTL_TEMPFILENAME = 16; +export const SQLITE_FCNTL_MMAP_SIZE = 18; +export const SQLITE_FCNTL_TRACE = 19; +export const SQLITE_FCNTL_HAS_MOVED = 20; +export const SQLITE_FCNTL_SYNC = 21; +export const SQLITE_FCNTL_COMMIT_PHASETWO = 22; +export const SQLITE_FCNTL_WIN32_SET_HANDLE = 23; +export const SQLITE_FCNTL_WAL_BLOCK = 24; +export const SQLITE_FCNTL_ZIPVFS = 25; +export const SQLITE_FCNTL_RBU = 26; +export const SQLITE_FCNTL_VFS_POINTER = 27; +export const SQLITE_FCNTL_JOURNAL_POINTER = 28; +export const SQLITE_FCNTL_WIN32_GET_HANDLE = 29; +export const SQLITE_FCNTL_PDB = 30; +export const SQLITE_FCNTL_BEGIN_ATOMIC_WRITE = 31; +export const SQLITE_FCNTL_COMMIT_ATOMIC_WRITE = 32; +export const SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE = 33; +export const SQLITE_FCNTL_LOCK_TIMEOUT = 34; +export const SQLITE_FCNTL_DATA_VERSION = 35; +export const SQLITE_FCNTL_SIZE_LIMIT = 36; +export const SQLITE_FCNTL_CKPT_DONE = 37; +export const SQLITE_FCNTL_RESERVE_BYTES = 38; +export const SQLITE_FCNTL_CKPT_START = 39; + +// Fundamental datatypes. +// https://www.sqlite.org/c3ref/c_blob.html +export const SQLITE_INTEGER = 1; +export const SQLITE_FLOAT = 2; +export const SQLITE_TEXT = 3; +export const SQLITE_BLOB = 4; +export const SQLITE_NULL = 5; + +// Special destructor behavior. +// https://www.sqlite.org/c3ref/c_static.html +export const SQLITE_STATIC = 0; +export const SQLITE_TRANSIENT = -1; + +// Text encodings. +// https://sqlite.org/c3ref/c_any.html +export const SQLITE_UTF8 = 1; /* IMP: R-37514-35566 */ +export const SQLITE_UTF16LE = 2; /* IMP: R-03371-37637 */ +export const SQLITE_UTF16BE = 3; /* IMP: R-51971-34154 */ +export const SQLITE_UTF16 = 4; /* Use native byte order */ + +// Module constraint ops. +export const SQLITE_INDEX_CONSTRAINT_EQ = 2; +export const SQLITE_INDEX_CONSTRAINT_GT = 4; +export const SQLITE_INDEX_CONSTRAINT_LE = 8; +export const SQLITE_INDEX_CONSTRAINT_LT = 16; +export const SQLITE_INDEX_CONSTRAINT_GE = 32; +export const SQLITE_INDEX_CONSTRAINT_MATCH = 64; +export const SQLITE_INDEX_CONSTRAINT_LIKE = 65; +export const SQLITE_INDEX_CONSTRAINT_GLOB = 66; +export const SQLITE_INDEX_CONSTRAINT_REGEXP = 67; +export const SQLITE_INDEX_CONSTRAINT_NE = 68; +export const SQLITE_INDEX_CONSTRAINT_ISNOT = 69; +export const SQLITE_INDEX_CONSTRAINT_ISNOTNULL = 70; +export const SQLITE_INDEX_CONSTRAINT_ISNULL = 71; +export const SQLITE_INDEX_CONSTRAINT_IS = 72; +export const SQLITE_INDEX_CONSTRAINT_FUNCTION = 150; +export const SQLITE_INDEX_SCAN_UNIQUE = 1; /* Scan visits at most = 1 row */ + +// Function flags +export const SQLITE_DETERMINISTIC = 0x000000800; +export const SQLITE_DIRECTONLY = 0x000080000; +export const SQLITE_SUBTYPE = 0x000100000; +export const SQLITE_INNOCUOUS = 0x000200000; + +// Sync flags +export const SQLITE_SYNC_NORMAL = 0x00002; +export const SQLITE_SYNC_FULL = 0x00003; +export const SQLITE_SYNC_DATAONLY = 0x00010; + +// Authorizer action codes +export const SQLITE_CREATE_INDEX = 1; +export const SQLITE_CREATE_TABLE = 2; +export const SQLITE_CREATE_TEMP_INDEX = 3; +export const SQLITE_CREATE_TEMP_TABLE = 4; +export const SQLITE_CREATE_TEMP_TRIGGER = 5; +export const SQLITE_CREATE_TEMP_VIEW = 6; +export const SQLITE_CREATE_TRIGGER = 7; +export const SQLITE_CREATE_VIEW = 8; +export const SQLITE_DELETE = 9; +export const SQLITE_DROP_INDEX = 10; +export const SQLITE_DROP_TABLE = 11; +export const SQLITE_DROP_TEMP_INDEX = 12; +export const SQLITE_DROP_TEMP_TABLE = 13; +export const SQLITE_DROP_TEMP_TRIGGER = 14; +export const SQLITE_DROP_TEMP_VIEW = 15; +export const SQLITE_DROP_TRIGGER = 16; +export const SQLITE_DROP_VIEW = 17; +export const SQLITE_INSERT = 18; +export const SQLITE_PRAGMA = 19; +export const SQLITE_READ = 20; +export const SQLITE_SELECT = 21; +export const SQLITE_TRANSACTION = 22; +export const SQLITE_UPDATE = 23; +export const SQLITE_ATTACH = 24; +export const SQLITE_DETACH = 25; +export const SQLITE_ALTER_TABLE = 26; +export const SQLITE_REINDEX = 27; +export const SQLITE_ANALYZE = 28; +export const SQLITE_CREATE_VTABLE = 29; +export const SQLITE_DROP_VTABLE = 30; +export const SQLITE_FUNCTION = 31; +export const SQLITE_SAVEPOINT = 32; +export const SQLITE_COPY = 0; +export const SQLITE_RECURSIVE = 33; + +// Authorizer return codes +export const SQLITE_DENY = 1; +export const SQLITE_IGNORE = 2; + +// Limit categories +export const SQLITE_LIMIT_LENGTH = 0; +export const SQLITE_LIMIT_SQL_LENGTH = 1; +export const SQLITE_LIMIT_COLUMN = 2; +export const SQLITE_LIMIT_EXPR_DEPTH = 3; +export const SQLITE_LIMIT_COMPOUND_SELECT = 4; +export const SQLITE_LIMIT_VDBE_OP = 5; +export const SQLITE_LIMIT_FUNCTION_ARG = 6; +export const SQLITE_LIMIT_ATTACHED = 7; +export const SQLITE_LIMIT_LIKE_PATTERN_LENGTH = 8; +export const SQLITE_LIMIT_VARIABLE_NUMBER = 9; +export const SQLITE_LIMIT_TRIGGER_DEPTH = 10; +export const SQLITE_LIMIT_WORKER_THREADS = 11; diff --git a/apps/web/src/common/sqlite/sqlite.kysely.ts b/apps/web/src/common/sqlite/sqlite.kysely.ts new file mode 100644 index 000000000..64fda6dee --- /dev/null +++ b/apps/web/src/common/sqlite/sqlite.kysely.ts @@ -0,0 +1,124 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import type { DatabaseConnection, Driver, QueryResult } from "kysely"; +import { CompiledQuery } from "kysely"; +import Worker from "./sqlite.worker.ts?worker"; +import type { SQLiteWorker } from "./sqlite.worker"; +import SQLiteSyncURI from "./wa-sqlite.wasm?url"; +import SQLiteAsyncURI from "./wa-sqlite-async.wasm?url"; +import { wrap } from "comlink"; + +type Config = { dbName: string; async: boolean }; + +export class WaSqliteWorkerDriver implements Driver { + private connection?: DatabaseConnection; + private connectionMutex = new ConnectionMutex(); + private worker: SQLiteWorker; + constructor(private readonly config: Config) { + this.worker = wrap(new Worker()) as SQLiteWorker; + } + + async init(): Promise { + await this.worker.init( + this.config.dbName, + this.config.async, + this.config.async ? SQLiteAsyncURI : SQLiteSyncURI + ); + + this.connection = new WaSqliteWorkerConnection(this.worker); + + // await this.config.onCreateConnection?.(this.connection); + } + + async acquireConnection(): Promise { + // SQLite only has one single connection. We use a mutex here to wait + // until the single connection has been released. + await this.connectionMutex.lock(); + return this.connection!; + } + + async beginTransaction(connection: DatabaseConnection): Promise { + await connection.executeQuery(CompiledQuery.raw("begin")); + } + + async commitTransaction(connection: DatabaseConnection): Promise { + await connection.executeQuery(CompiledQuery.raw("commit")); + } + + async rollbackTransaction(connection: DatabaseConnection): Promise { + await connection.executeQuery(CompiledQuery.raw("rollback")); + } + + async releaseConnection(): Promise { + this.connectionMutex.unlock(); + } + + async destroy(): Promise { + if (!this.worker) { + return; + } + return await this.worker.close(); + } +} + +class ConnectionMutex { + private promise?: Promise; + private resolve?: () => void; + + async lock(): Promise { + while (this.promise) { + await this.promise; + } + + this.promise = new Promise((resolve) => { + this.resolve = resolve; + }); + } + + unlock(): void { + const resolve = this.resolve; + + this.promise = undefined; + this.resolve = undefined; + + resolve?.(); + } +} + +class WaSqliteWorkerConnection implements DatabaseConnection { + constructor(private readonly worker: SQLiteWorker) {} + + streamQuery(): AsyncIterableIterator> { + throw new Error("wasqlite driver doesn't support streaming"); + } + + async executeQuery( + compiledQuery: CompiledQuery + ): Promise> { + const { parameters, sql, query } = compiledQuery; + const mode = + query.kind === "SelectQueryNode" + ? "query" + : query.kind === "RawNode" + ? "raw" + : "exec"; + return await this.worker.run(mode, sql, parameters as any); + } +} diff --git a/apps/web/src/common/sqlite/sqlite.worker.ts b/apps/web/src/common/sqlite/sqlite.worker.ts new file mode 100644 index 000000000..271b83fa5 --- /dev/null +++ b/apps/web/src/common/sqlite/sqlite.worker.ts @@ -0,0 +1,133 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +// import type { SQLiteAPI, SQLiteCompatibleType } from "./index.d.ts"; +import { Factory, SQLITE_ROW } from "./sqlite-api"; +import SQLiteAsyncESMFactory from "./wa-sqlite-async"; +import SQLiteSyncESMFactory from "./wa-sqlite"; +import { IDBBatchAtomicVFS } from "./IDBBatchAtomicVFS"; +import { AccessHandlePoolVFS } from "./AccessHandlePoolVFS"; +import { expose } from "comlink"; +import type { RunMode } from "./type"; +import { QueryResult } from "kysely"; + +type PreparedStatement = { + stmt: number; + columns: string[]; +}; + +let sqlite: SQLiteAPI; +let db: number; +const preparedStatements: Map = new Map(); + +async function init(dbName: string, async: boolean, url?: string) { + const option = url ? { locateFile: () => url } : {}; + const SQLiteAsyncModule = async + ? await SQLiteAsyncESMFactory(option) + : await SQLiteSyncESMFactory(option); + sqlite = Factory(SQLiteAsyncModule); + const vfs = async + ? new IDBBatchAtomicVFS(dbName, { durability: "strict" }) + : new AccessHandlePoolVFS(dbName); + if ("isReady" in vfs) await vfs.isReady; + + sqlite.vfs_register(vfs, true); + db = await sqlite.open_v2(dbName); //, undefined, dbName); +} + +/** + * Wrapper function for preparing SQL statements with caching + * to avoid unnecessary computations. + */ +async function prepare(sql: string) { + const cached = preparedStatements.get(sql); + if (cached !== undefined) return cached; + + const str = sqlite.str_new(db, sql); + const prepared = await sqlite.prepare_v2(db, sqlite.str_value(str)); + if (!prepared) return; + + const statement: PreparedStatement = { + stmt: prepared.stmt, + columns: sqlite.column_names(prepared.stmt) + }; + preparedStatements.set(sql, statement); + return statement; +} + +async function run(sql: string, parameters?: SQLiteCompatibleType[]) { + const prepared = await prepare(sql); + if (!prepared) return []; + + if (parameters) sqlite.bind_collection(prepared.stmt, parameters); + + const rows: Record[] = []; + while ((await sqlite.step(prepared.stmt)) === SQLITE_ROW) { + const row = sqlite.row(prepared.stmt); + const acc: Record = {}; + row.forEach((v, i) => (acc[prepared.columns[i]] = v)); + rows.push(acc); + } + + await sqlite + .reset(prepared.stmt) + // we must clear/destruct the prepared statement if it can't be reset + .catch(() => + sqlite + .finalize(prepared.stmt) + // ignore error (we will just prepare a new statement) + .catch(() => false) + .finally(() => preparedStatements.delete(sql)) + ); + return rows; +} + +async function exec( + mode: RunMode, + sql: string, + parameters?: SQLiteCompatibleType[] +): Promise> { + console.time(sql); + const rows = (await run(sql, parameters)) as R[]; + console.timeEnd(sql); + if (mode === "query") return { rows }; + + const v = await run("SELECT last_insert_rowid() as id"); + return { + insertId: BigInt(v[0].id as number), + numAffectedRows: BigInt(sqlite.changes(db)), + rows: mode === "raw" ? rows : [] + }; +} + +async function close() { + for (const [_, prepared] of preparedStatements) { + await sqlite.finalize(prepared.stmt); + } + await sqlite.close(db); +} + +const worker = { + close, + init, + run: exec +}; + +export type SQLiteWorker = typeof worker; +expose(worker); diff --git a/apps/web/src/utils/compressor.worker.ts b/apps/web/src/common/sqlite/type.ts similarity index 50% rename from apps/web/src/utils/compressor.worker.ts rename to apps/web/src/common/sqlite/type.ts index acc54a1ff..1095a1401 100644 --- a/apps/web/src/utils/compressor.worker.ts +++ b/apps/web/src/common/sqlite/type.ts @@ -17,28 +17,43 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { expose } from "comlink"; -import { gzip, gunzip } from "fflate"; -import { fromBase64, toBase64 } from "@aws-sdk/util-base64-browser"; +import type { QueryResult } from "kysely"; -const module = { - gzip: ({ data, level }: { data: string; level: number }) => { - return new Promise((resolve, reject) => { - gzip( - new TextEncoder().encode(data), - { level: level as any }, - (err, data) => (err ? reject(err) : resolve(toBase64(data))) - ); - }); - }, - gunzip: ({ data }: { data: string }) => { - return new Promise((resolve, reject) => { - gunzip(fromBase64(data), (err, data) => - err ? reject(err) : resolve(new TextDecoder().decode(data)) - ); - }); - } +export type Promisable = T | Promise; + +export type RunMode = "exec" | "query" | "raw"; + +export type MainMsg = + | { + type: "run"; + mode: RunMode; + sql: string; + parameters?: readonly unknown[]; + } + | { + type: "close"; + } + | { + type: "init"; + url?: string; + dbName: string; + }; + +export type WorkerMsg = { + [K in keyof Events]: { + type: K; + data: Events[K]; + err: unknown; + }; +}[keyof Events]; +type Events = { + run: QueryResult | null; + init: null; + close: null; +}; +export type EventWithError = { + [K in keyof Events]: { + data: Events[K]; + err: unknown; + }; }; - -expose(module); -export type Compressor = typeof module; diff --git a/apps/web/src/common/sqlite/wa-sqlite-async.js b/apps/web/src/common/sqlite/wa-sqlite-async.js new file mode 100644 index 000000000..3dc98d575 --- /dev/null +++ b/apps/web/src/common/sqlite/wa-sqlite-async.js @@ -0,0 +1,114 @@ + +var Module = (() => { + var _scriptDir = import.meta.url; + + return ( +function(moduleArg = {}) { + +var f=moduleArg,aa,ba;f.ready=new Promise((a,b)=>{aa=a;ba=b});var ca=Object.assign({},f),da="./this.program",ea=(a,b)=>{throw b;},fa="object"==typeof window,ia="function"==typeof importScripts,p="",ja; +if(fa||ia)ia?p=self.location.href:"undefined"!=typeof document&&document.currentScript&&(p=document.currentScript.src),_scriptDir&&(p=_scriptDir),0!==p.indexOf("blob:")?p=p.substr(0,p.replace(/[?#].*/,"").lastIndexOf("/")+1):p="",ia&&(ja=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)});var ka=f.print||console.log.bind(console),t=f.printErr||console.error.bind(console);Object.assign(f,ca);ca=null;f.thisProgram&&(da=f.thisProgram); +f.quit&&(ea=f.quit);var la;f.wasmBinary&&(la=f.wasmBinary);var noExitRuntime=f.noExitRuntime||!0;"object"!=typeof WebAssembly&&u("no native wasm support detected");var ma,v=!1,na,w,y,oa,z,B,pa,qa;function ra(){var a=ma.buffer;f.HEAP8=w=new Int8Array(a);f.HEAP16=oa=new Int16Array(a);f.HEAPU8=y=new Uint8Array(a);f.HEAPU16=new Uint16Array(a);f.HEAP32=z=new Int32Array(a);f.HEAPU32=B=new Uint32Array(a);f.HEAPF32=pa=new Float32Array(a);f.HEAPF64=qa=new Float64Array(a)}var sa=[],ta=[],ua=[],va=[],wa=0; +function xa(){var a=f.preRun.shift();sa.unshift(a)}var C=0,ya=null,za=null;function u(a){if(f.onAbort)f.onAbort(a);a="Aborted("+a+")";t(a);v=!0;na=1;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");ba(a);throw a;}function Aa(a){return a.startsWith("data:application/octet-stream;base64,")}var Ba;if(f.locateFile){if(Ba="wa-sqlite-async.wasm",!Aa(Ba)){var Ca=Ba;Ba=f.locateFile?f.locateFile(Ca,p):p+Ca}}else Ba=(new URL("wa-sqlite-async.wasm",import.meta.url)).href; +function Da(a){if(a==Ba&&la)return new Uint8Array(la);if(ja)return ja(a);throw"both async and sync fetching of the wasm failed";}function Ea(a){return la||!fa&&!ia||"function"!=typeof fetch?Promise.resolve().then(()=>Da(a)):fetch(a,{credentials:"same-origin"}).then(b=>{if(!b.ok)throw"failed to load wasm binary file at '"+a+"'";return b.arrayBuffer()}).catch(()=>Da(a))} +function Fa(a,b,c){return Ea(a).then(d=>WebAssembly.instantiate(d,b)).then(d=>d).then(c,d=>{t(`failed to asynchronously prepare wasm: ${d}`);u(d)})}function Ga(a,b){var c=Ba;return la||"function"!=typeof WebAssembly.instantiateStreaming||Aa(c)||"function"!=typeof fetch?Fa(c,a,b):fetch(c,{credentials:"same-origin"}).then(d=>WebAssembly.instantiateStreaming(d,a).then(b,function(e){t(`wasm streaming compile failed: ${e}`);t("falling back to ArrayBuffer instantiation");return Fa(c,a,b)}))}var D,F; +function Ha(a){this.name="ExitStatus";this.message=`Program terminated with exit(${a})`;this.status=a}var Ia=a=>{for(;0>0];case "i8":return w[a>>0];case "i16":return oa[a>>1];case "i32":return z[a>>2];case "i64":u("to do getValue(i64) use WASM_BIGINT");case "float":return pa[a>>2];case "double":return qa[a>>3];case "*":return B[a>>2];default:u(`invalid type for getValue: ${b}`)}} +function J(a,b,c="i8"){c.endsWith("*")&&(c="*");switch(c){case "i1":w[a>>0]=b;break;case "i8":w[a>>0]=b;break;case "i16":oa[a>>1]=b;break;case "i32":z[a>>2]=b;break;case "i64":u("to do setValue(i64) use WASM_BIGINT");case "float":pa[a>>2]=b;break;case "double":qa[a>>3]=b;break;case "*":B[a>>2]=b;break;default:u(`invalid type for setValue: ${c}`)}} +var Ja="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,K=(a,b,c)=>{var d=b+c;for(c=b;a[c]&&!(c>=d);)++c;if(16e?d+=String.fromCharCode(e):(e-=65536,d+=String.fromCharCode(55296|e>>10,56320|e&1023))}}else d+=String.fromCharCode(e)}return d}, +Ka=(a,b)=>{for(var c=0,d=a.length-1;0<=d;d--){var e=a[d];"."===e?a.splice(d,1):".."===e?(a.splice(d,1),c++):c&&(a.splice(d,1),c--)}if(b)for(;c;c--)a.unshift("..");return a},M=a=>{var b="/"===a.charAt(0),c="/"===a.substr(-1);(a=Ka(a.split("/").filter(d=>!!d),!b).join("/"))||b||(a=".");a&&c&&(a+="/");return(b?"/":"")+a},La=a=>{var b=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(a).slice(1);a=b[0];b=b[1];if(!a&&!b)return".";b&&(b=b.substr(0,b.length-1));return a+b},Ma=a=>{if("/"=== +a)return"/";a=M(a);a=a.replace(/\/$/,"");var b=a.lastIndexOf("/");return-1===b?a:a.substr(b+1)},Na=()=>{if("object"==typeof crypto&&"function"==typeof crypto.getRandomValues)return a=>crypto.getRandomValues(a);u("initRandomDevice")},Oa=a=>(Oa=Na())(a); +function Pa(){for(var a="",b=!1,c=arguments.length-1;-1<=c&&!b;c--){b=0<=c?arguments[c]:"/";if("string"!=typeof b)throw new TypeError("Arguments to path.resolve must be strings");if(!b)return"";a=b+"/"+a;b="/"===b.charAt(0)}a=Ka(a.split("/").filter(d=>!!d),!b).join("/");return(b?"/":"")+a||"."} +var Qa=[],Ra=a=>{for(var b=0,c=0;c=d?b++:2047>=d?b+=2:55296<=d&&57343>=d?(b+=4,++c):b+=3}return b},Sa=(a,b,c,d)=>{if(!(0=g){var n=a.charCodeAt(++h);g=65536+((g&1023)<<10)|n&1023}if(127>=g){if(c>=d)break;b[c++]=g}else{if(2047>=g){if(c+1>=d)break;b[c++]=192|g>>6}else{if(65535>=g){if(c+2>=d)break;b[c++]=224|g>>12}else{if(c+3>=d)break;b[c++]=240|g>>18;b[c++]=128|g>> +12&63}b[c++]=128|g>>6&63}b[c++]=128|g&63}}b[c]=0;return c-e},Ta=[];function Ua(a,b){Ta[a]={input:[],Rb:[],bc:b};Va(a,Wa)} +var Wa={open(a){var b=Ta[a.node.ec];if(!b)throw new N(43);a.Sb=b;a.seekable=!1},close(a){a.Sb.bc.ic(a.Sb)},ic(a){a.Sb.bc.ic(a.Sb)},read(a,b,c,d){if(!a.Sb||!a.Sb.bc.xc)throw new N(60);for(var e=0,h=0;h=b||(b=Math.max(b,c*(1048576>c?2:1.125)>>>0),0!=c&&(b=Math.max(b,256)),c=a.Nb,a.Nb=new Uint8Array(b),0=a.node.Pb)return 0;a=Math.min(a.node.Pb-e,d);if(8b)throw new N(28);return b},uc(a,b,c){Za(a.node,b+c);a.node.Pb=Math.max(a.node.Pb,b+c)},kc(a,b,c,d,e){if(32768!==(a.node.mode&61440))throw new N(43);a=a.node.Nb;if(e&2||a.buffer!== +w.buffer){if(0{var c=0;a&&(c|=365);b&&(c|=146);return c},fb=null,gb={},hb=[],ib=1,Q=null,jb=!0,N=null,bb={}; +function R(a,b={}){a=Pa(a);if(!a)return{path:"",node:null};b=Object.assign({wc:!0,sc:0},b);if(8!!g);for(var c=fb,d="/",e=0;e>>0)%Q.length}function nb(a){var b=mb(a.parent.id,a.name);if(Q[b]===a)Q[b]=a.ac;else for(b=Q[b];b;){if(b.ac===a){b.ac=a.ac;break}b=b.ac}} +function cb(a,b){var c;if(c=(c=ob(a,"x"))?c:a.Cb.cc?0:2)throw new N(c,a);for(c=Q[mb(a.id,b)];c;c=c.ac){var d=c.name;if(c.parent.id===a.id&&d===b)return c}return a.Cb.cc(a,b)}function ab(a,b,c,d){a=new pb(a,b,c,d);b=mb(a.parent.id,a.name);a.ac=Q[b];return Q[b]=a}function P(a){return 16384===(a&61440)}function qb(a){var b=["r","w","rw"][a&3];a&512&&(b+="w");return b} +function ob(a,b){if(jb)return 0;if(!b.includes("r")||a.mode&292){if(b.includes("w")&&!(a.mode&146)||b.includes("x")&&!(a.mode&73))return 2}else return 2;return 0}function rb(a,b){try{return cb(a,b),20}catch(c){}return ob(a,"wx")}function sb(a,b,c){try{var d=cb(a,b)}catch(e){return e.Ob}if(a=ob(a,"wx"))return a;if(c){if(!P(d.mode))return 54;if(d===d.parent||"/"===lb(d))return 10}else if(P(d.mode))return 31;return 0}function tb(){for(var a=0;4096>=a;a++)if(!hb[a])return a;throw new N(33);} +function S(a){a=hb[a];if(!a)throw new N(8);return a}function ub(a,b=-1){vb||(vb=function(){this.hc={}},vb.prototype={},Object.defineProperties(vb.prototype,{object:{get(){return this.node},set(c){this.node=c}},flags:{get(){return this.hc.flags},set(c){this.hc.flags=c}},position:{get(){return this.hc.position},set(c){this.hc.position=c}}}));a=Object.assign(new vb,a);-1==b&&(b=tb());a.Wb=b;return hb[b]=a}var $a={open(a){a.Mb=gb[a.node.ec].Mb;a.Mb.open&&a.Mb.open(a)},Zb(){throw new N(70);}}; +function Va(a,b){gb[a]={Mb:b}}function wb(a,b){var c="/"===b,d=!b;if(c&&fb)throw new N(10);if(!c&&!d){var e=R(b,{wc:!1});b=e.path;e=e.node;if(e.$b)throw new N(10);if(!P(e.mode))throw new N(54);}b={type:a,bd:{},zc:b,Lc:[]};a=a.Ub(b);a.Ub=b;b.root=a;c?fb=a:e&&(e.$b=b,e.Ub&&e.Ub.Lc.push(b))}function xb(a,b,c){var d=R(a,{parent:!0}).node;a=Ma(a);if(!a||"."===a||".."===a)throw new N(28);var e=rb(d,a);if(e)throw new N(e);if(!d.Cb.jc)throw new N(63);return d.Cb.jc(d,a,b,c)} +function T(a,b){return xb(a,(void 0!==b?b:511)&1023|16384,0)}function yb(a,b,c){"undefined"==typeof c&&(c=b,b=438);xb(a,b|8192,c)}function zb(a,b){if(!Pa(a))throw new N(44);var c=R(b,{parent:!0}).node;if(!c)throw new N(44);b=Ma(b);var d=rb(c,b);if(d)throw new N(d);if(!c.Cb.nc)throw new N(63);c.Cb.nc(c,b,a)}function Ab(a){var b=R(a,{parent:!0}).node;a=Ma(a);var c=cb(b,a),d=sb(b,a,!0);if(d)throw new N(d);if(!b.Cb.mc)throw new N(63);if(c.$b)throw new N(10);b.Cb.mc(b,a);nb(c)} +function kb(a){a=R(a).node;if(!a)throw new N(44);if(!a.Cb.fc)throw new N(28);return Pa(lb(a.parent),a.Cb.fc(a))}function Bb(a,b){a=R(a,{Yb:!b}).node;if(!a)throw new N(44);if(!a.Cb.Tb)throw new N(63);return a.Cb.Tb(a)}function Cb(a){return Bb(a,!0)}function Db(a,b){a="string"==typeof a?R(a,{Yb:!0}).node:a;if(!a.Cb.Qb)throw new N(63);a.Cb.Qb(a,{mode:b&4095|a.mode&-4096,timestamp:Date.now()})} +function Eb(a,b){if(0>b)throw new N(28);a="string"==typeof a?R(a,{Yb:!0}).node:a;if(!a.Cb.Qb)throw new N(63);if(P(a.mode))throw new N(31);if(32768!==(a.mode&61440))throw new N(28);var c=ob(a,"w");if(c)throw new N(c);a.Cb.Qb(a,{size:b,timestamp:Date.now()})} +function Fb(a,b,c){if(""===a)throw new N(44);if("string"==typeof b){var d={r:0,"r+":2,w:577,"w+":578,a:1089,"a+":1090}[b];if("undefined"==typeof d)throw Error(`Unknown file open mode: ${b}`);b=d}c=b&64?("undefined"==typeof c?438:c)&4095|32768:0;if("object"==typeof a)var e=a;else{a=M(a);try{e=R(a,{Yb:!(b&131072)}).node}catch(h){}}d=!1;if(b&64)if(e){if(b&128)throw new N(20);}else e=xb(a,c,0),d=!0;if(!e)throw new N(44);8192===(e.mode&61440)&&(b&=-513);if(b&65536&&!P(e.mode))throw new N(54);if(!d&&(c= +e?40960===(e.mode&61440)?32:P(e.mode)&&("r"!==qb(b)||b&512)?31:ob(e,qb(b)):44))throw new N(c);b&512&&!d&&Eb(e,0);b&=-131713;e=ub({node:e,path:lb(e),flags:b,seekable:!0,position:0,Mb:e.Mb,Rc:[],error:!1});e.Mb.open&&e.Mb.open(e);!f.logReadFiles||b&1||(Gb||(Gb={}),a in Gb||(Gb[a]=1));return e}function Hb(a,b,c){if(null===a.Wb)throw new N(8);if(!a.seekable||!a.Mb.Zb)throw new N(70);if(0!=c&&1!=c&&2!=c)throw new N(28);a.position=a.Mb.Zb(a,b,c);a.Rc=[]} +function Ib(){N||(N=function(a,b){this.name="ErrnoError";this.node=b;this.Pc=function(c){this.Ob=c};this.Pc(a);this.message="FS error"},N.prototype=Error(),N.prototype.constructor=N,[44].forEach(a=>{bb[a]=new N(a);bb[a].stack=""}))}var Jb; +function Kb(a,b,c){a=M("/dev/"+a);var d=eb(!!b,!!c);Lb||(Lb=64);var e=Lb++<<8|0;Va(e,{open(h){h.seekable=!1},close(){c&&c.buffer&&c.buffer.length&&c(10)},read(h,g,n,k){for(var l=0,r=0;r>2]=d.Hc;z[c+4>>2]=d.mode;B[c+8>>2]=d.Nc;z[c+12>>2]=d.uid;z[c+16>>2]=d.Kc;z[c+20>>2]=d.ec;F=[d.size>>>0,(D=d.size,1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];z[c+24>>2]=F[0];z[c+28>>2]=F[1];z[c+32>>2]=4096;z[c+36>>2]=d.Fc;a=d.Dc.getTime();b=d.Mc.getTime();var e=d.Gc.getTime();F=[Math.floor(a/1E3)>>>0,(D=Math.floor(a/1E3),1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];z[c+40>>2]=F[0];z[c+44>>2]=F[1];B[c+48>>2]=a%1E3*1E3;F=[Math.floor(b/1E3)>>>0,(D=Math.floor(b/1E3),1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];z[c+56>>2]=F[0];z[c+60>>2]=F[1];B[c+64>>2]=b%1E3*1E3;F=[Math.floor(e/1E3)>>>0,(D=Math.floor(e/1E3),1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];z[c+72>>2]=F[0];z[c+76>>2]=F[1];B[c+80>>2]= +e%1E3*1E3;F=[d.yc>>>0,(D=d.yc,1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];z[c+88>>2]=F[0];z[c+92>>2]=F[1];return 0}var Ob=void 0;function Pb(){var a=z[Ob>>2];Ob+=4;return a} +var Qb=(a,b)=>b+2097152>>>0<4194305-!!a?(a>>>0)+4294967296*b:NaN,Rb=[0,31,60,91,121,152,182,213,244,274,305,335],Sb=[0,31,59,90,120,151,181,212,243,273,304,334],Ub=a=>{var b=Ra(a)+1,c=Tb(b);c&&Sa(a,y,c,b);return c},Vb={},Xb=()=>{if(!Wb){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:da||"./this.program"},b;for(b in Vb)void 0===Vb[b]?delete a[b]:a[b]=Vb[b]; +var c=[];for(b in a)c.push(`${b}=${a[b]}`);Wb=c}return Wb},Wb;function Yb(){}function Zb(){}function $b(){}function ac(){}function bc(){}function cc(){}function dc(){}function ec(){}function fc(){}function gc(){}function hc(){}function ic(){}function jc(){}function kc(){}function lc(){}function mc(){}function nc(){}function oc(){}function pc(){}function qc(){}function rc(){}function sc(){}function tc(){}function uc(){}function vc(){}function wc(){}function xc(){}function yc(){}function zc(){} +function Ac(){}function Bc(){}function Cc(){}function Dc(){}function Ec(){}function Fc(){}function Gc(){}function Hc(){}function Ic(){}function Jc(){}var Kc=a=>{na=a;if(!(noExitRuntime||0{a instanceof Ha||"unwind"==a||ea(1,a)},Mc=a=>{try{a()}catch(b){u(b)}}; +function Nc(a){var b={},c;for(c in a)(function(d){var e=a[d];b[d]="function"==typeof e?function(){Oc.push(d);try{return e.apply(null,arguments)}finally{v||(Oc.pop()===d||u(),V&&1===X&&0===Oc.length&&(X=0,Mc(Pc),"undefined"!=typeof Fibers&&Fibers.cd()))}}:e})(c);return b}var X=0,V=null,Qc=0,Oc=[],Rc={},Sc={},Tc=0,Uc=null,Vc=[];function Wc(){return new Promise((a,b)=>{Uc={resolve:a,reject:b}})} +function Xc(){var a=Tb(16396),b=a+12;B[a>>2]=b;B[a+4>>2]=b+16384;b=Oc[0];var c=Rc[b];void 0===c&&(c=Tc++,Rc[b]=c,Sc[c]=b);z[a+8>>2]=c;return a} +function Yc(a){if(!v){if(0===X){var b=!1,c=!1;a((d=0)=>{if(!v&&(Qc=d,b=!0,c)){X=2;Mc(()=>Zc(V));"undefined"!=typeof Browser&&Browser.qc.Jc&&Browser.qc.resume();d=!1;try{var e=(0,Y[Sc[z[V+8>>2]]])()}catch(n){e=n,d=!0}var h=!1;if(!V){var g=Uc;g&&(Uc=null,(d?g.reject:g.resolve)(e),h=!0)}if(d&&!h)throw e;}});c=!0;b||(X=1,V=Xc(),"undefined"!=typeof Browser&&Browser.qc.Jc&&Browser.qc.pause(),Mc(()=>$c(V)))}else 2===X?(X=0,Mc(ad),bd(V),V=null,Vc.forEach(d=>{if(!v)try{if(d(),!(noExitRuntime||0{a().then(b)})} +var dd={},Z=(a,b,c,d,e)=>{function h(m){--wa;0!==k&&ed(k);return"string"===b?m?K(y,m):"":"boolean"===b?!!m:m}var g={string:m=>{var q=0;if(null!==m&&void 0!==m&&0!==m){q=Ra(m)+1;var x=fd(q);Sa(m,y,x,q);q=x}return q},array:m=>{var q=fd(m.length);w.set(m,q);return q}};a=f["_"+a];var n=[],k=0;if(d)for(var l=0;l0,write:(d,e,h,g)=>g});yb("/dev/null",259);Ua(1280,Xa);Ua(1536,Ya);yb("/dev/tty",1280);yb("/dev/tty1",1536);var a=new Uint8Array(1024),b=0,c=()=>{0===b&&(b=Oa(a).byteLength);return a[--b]};Kb("random",c);Kb("urandom",c);T("/dev/shm");T("/dev/shm/tmp")})(); +(function(){T("/proc");var a=T("/proc/self");T("/proc/self/fd");wb({Ub(){var b=ab(a,"fd",16895,73);b.Cb={cc(c,d){var e=S(+d);c={parent:null,Ub:{zc:"fake"},Cb:{fc:()=>e.path}};return c.parent=c}};return b}},"/proc/self/fd")})(); +(function(){const a=new Map;f.setAuthorizer=function(b,c,d){c?a.set(b,{f:c,tc:d}):a.delete(b);return Z("set_authorizer","number",["number"],[b])};Yb=function(b,c,d,e,h,g){if(a.has(b)){const {f:n,tc:k}=a.get(b);return n(k,c,d?d?K(y,d):"":null,e?e?K(y,e):"":null,h?h?K(y,h):"":null,g?g?K(y,g):"":null)}return 0}})(); +(function(){const a=new Map,b=new Map;f.createFunction=function(c,d,e,h,g,n){const k=a.size;a.set(k,{f:n,Xb:g});return Z("create_function","number","number string number number number number".split(" "),[c,d,e,h,k,0])};f.createAggregate=function(c,d,e,h,g,n,k){const l=a.size;a.set(l,{step:n,Ic:k,Xb:g});return Z("create_function","number","number string number number number number".split(" "),[c,d,e,h,l,1])};f.getFunctionUserData=function(c){return b.get(c)};$b=function(c,d,e,h){c=a.get(c);b.set(d, +c.Xb);c.f(d,new Uint32Array(y.buffer,h,e));b.delete(d)};bc=function(c,d,e,h){c=a.get(c);b.set(d,c.Xb);c.step(d,new Uint32Array(y.buffer,h,e));b.delete(d)};Zb=function(c,d){c=a.get(c);b.set(d,c.Xb);c.Ic(d);b.delete(d)}})();(function(){const a=new Map;f.progressHandler=function(b,c,d,e){d?a.set(b,{f:d,tc:e}):a.delete(b);return Z("progress_handler",null,["number","number"],[b,c])};ac=function(b){if(a.has(b)){const {f:c,tc:d}=a.get(b);return c(d)}return 0}})(); +(function(){function a(k,l){const r=`get${k}`,m=`set${k}`;return new Proxy(new DataView(y.buffer,l,"Int32"===k?4:8),{get(q,x){if(x===r)return function(A,G){if(!G)throw Error("must be little endian");return q[x](A,G)};if(x===m)return function(A,G,E){if(!E)throw Error("must be little endian");return q[x](A,G,E)};if("string"===typeof x&&x.match(/^(get)|(set)/))throw Error("invalid type");return q[x]}})}const b="object"===typeof dd,c=new Map,d=new Map,e=new Map,h=b?new Set:null,g=b?new Set:null,n=new Map; +sc=function(k,l,r,m){n.set(k?K(y,k):"",{size:l,dc:Array.from(new Uint32Array(y.buffer,m,r))})};f.createModule=function(k,l,r,m){b&&(r.handleAsync=cd);const q=c.size;c.set(q,{module:r,Xb:m});m=0;r.xCreate&&(m|=1);r.xConnect&&(m|=2);r.xBestIndex&&(m|=4);r.xDisconnect&&(m|=8);r.xDestroy&&(m|=16);r.xOpen&&(m|=32);r.xClose&&(m|=64);r.xFilter&&(m|=128);r.xNext&&(m|=256);r.xEof&&(m|=512);r.xColumn&&(m|=1024);r.xRowid&&(m|=2048);r.xUpdate&&(m|=4096);r.xBegin&&(m|=8192);r.xSync&&(m|=16384);r.xCommit&&(m|= +32768);r.xRollback&&(m|=65536);r.xFindFunction&&(m|=131072);r.xRename&&(m|=262144);return Z("create_module","number",["number","string","number","number"],[k,l,q,m])};ic=function(k,l,r,m,q,x){l=c.get(l);d.set(q,l);if(b){h.delete(q);for(const A of h)d.delete(A)}m=Array.from(new Uint32Array(y.buffer,m,r)).map(A=>A?K(y,A):"");return l.module.xCreate(k,l.Xb,m,q,a("Int32",x))};hc=function(k,l,r,m,q,x){l=c.get(l);d.set(q,l);if(b){h.delete(q);for(const A of h)d.delete(A)}m=Array.from(new Uint32Array(y.buffer, +m,r)).map(A=>A?K(y,A):"");return l.module.xConnect(k,l.Xb,m,q,a("Int32",x))};dc=function(k,l){var r=d.get(k),m=n.get("sqlite3_index_info").dc;const q={};q.nConstraint=I(l+m[0],"i32");q.aConstraint=[];var x=I(l+m[1],"*"),A=n.get("sqlite3_index_constraint").size;for(var G=0;G>=2;return B[g]+B[g+1]*2**32}const c="object"===typeof dd,d=new Map,e=new Map;f.registerVFS= +function(g,n){if(Z("sqlite3_vfs_find","number",["string"],[g.name]))throw Error(`VFS '${g.name}' already registered`);c&&(g.handleAsync=cd);var k=g.ad??64;const l=f._malloc(4);n=Z("register_vfs","number",["string","number","number","number"],[g.name,k,n?1:0,l]);n||(k=I(l,"*"),d.set(k,g));f._free(l);return n};const h=c?new Set:null;xc=function(g){const n=e.get(g);c?h.add(g):e.delete(g);return n.xClose(g)};Ec=function(g,n,k,l){return e.get(g).xRead(g,y.subarray(n,n+k),b(l))};Jc=function(g,n,k,l){return e.get(g).xWrite(g, +y.subarray(n,n+k),b(l))};Hc=function(g,n){return e.get(g).xTruncate(g,b(n))};Gc=function(g,n){return e.get(g).xSync(g,n)};Bc=function(g,n){const k=e.get(g);n=a("BigInt64",n);return k.xFileSize(g,n)};Cc=function(g,n){return e.get(g).xLock(g,n)};Ic=function(g,n){return e.get(g).xUnlock(g,n)};wc=function(g,n){const k=e.get(g);n=a("Int32",n);return k.xCheckReservedLock(g,n)};Ac=function(g,n,k){const l=e.get(g);k=new DataView(y.buffer,k);return l.xFileControl(g,n,k)};Fc=function(g){return e.get(g).xSectorSize(g)}; +zc=function(g){return e.get(g).xDeviceCharacteristics(g)};Dc=function(g,n,k,l,r){g=d.get(g);e.set(k,g);if(c){h.delete(k);for(var m of h)e.delete(m)}m=null;if(l&64){m=1;const q=[];for(;m;){const x=y[n++];if(x)q.push(x);else switch(y[n]||(m=null),m){case 1:q.push(63);m=2;break;case 2:q.push(61);m=3;break;case 3:q.push(38),m=2}}m=(new TextDecoder).decode(new Uint8Array(q))}else n&&(m=n?K(y,n):"");r=a("Int32",r);return g.xOpen(m,k,l,r)};yc=function(g,n,k){return d.get(g).xDelete(n?K(y,n):"",k)};vc=function(g, +n,k,l){g=d.get(g);l=a("Int32",l);return g.xAccess(n?K(y,n):"",k,l)}})(); +var jd={a:(a,b,c,d)=>{u(`Assertion failed: ${a?K(y,a):""}, at: `+[b?b?K(y,b):"":"unknown filename",c,d?d?K(y,d):"":"unknown function"])},K:function(a,b){try{return a=a?K(y,a):"",Db(a,b),0}catch(c){if("undefined"==typeof U||"ErrnoError"!==c.name)throw c;return-c.Ob}},M:function(a,b,c){try{b=b?K(y,b):"";b=Mb(a,b);if(c&-8)return-28;var d=R(b,{Yb:!0}).node;if(!d)return-44;a="";c&4&&(a+="r");c&2&&(a+="w");c&1&&(a+="x");return a&&ob(d,a)?-2:0}catch(e){if("undefined"==typeof U||"ErrnoError"!==e.name)throw e; +return-e.Ob}},L:function(a,b){try{var c=S(a);Db(c.node,b);return 0}catch(d){if("undefined"==typeof U||"ErrnoError"!==d.name)throw d;return-d.Ob}},J:function(a){try{var b=S(a).node;var c="string"==typeof b?R(b,{Yb:!0}).node:b;if(!c.Cb.Qb)throw new N(63);c.Cb.Qb(c,{timestamp:Date.now()});return 0}catch(d){if("undefined"==typeof U||"ErrnoError"!==d.name)throw d;return-d.Ob}},b:function(a,b,c){Ob=c;try{var d=S(a);switch(b){case 0:var e=Pb();if(0>e)return-28;for(;hb[e];)e++;return ub(d,e).Wb;case 1:case 2:return 0; +case 3:return d.flags;case 4:return e=Pb(),d.flags|=e,0;case 5:return e=Pb(),oa[e+0>>1]=2,0;case 6:case 7:return 0;case 16:case 8:return-28;case 9:return z[hd()>>2]=28,-1;default:return-28}}catch(h){if("undefined"==typeof U||"ErrnoError"!==h.name)throw h;return-h.Ob}},I:function(a,b){try{var c=S(a);return Nb(Bb,c.path,b)}catch(d){if("undefined"==typeof U||"ErrnoError"!==d.name)throw d;return-d.Ob}},n:function(a,b,c){b=Qb(b,c);try{if(isNaN(b))return 61;var d=S(a);if(0===(d.flags&2097155))throw new N(28); +Eb(d.node,b);return 0}catch(e){if("undefined"==typeof U||"ErrnoError"!==e.name)throw e;return-e.Ob}},C:function(a,b){try{if(0===b)return-28;var c=Ra("/")+1;if(b=d)return-28; +var e=kb(b),h=Math.min(d,Ra(e)),g=w[c+h];Sa(e,y,c,d+1);w[c+h]=g;return h}catch(n){if("undefined"==typeof U||"ErrnoError"!==n.name)throw n;return-n.Ob}},u:function(a){try{return a=a?K(y,a):"",Ab(a),0}catch(b){if("undefined"==typeof U||"ErrnoError"!==b.name)throw b;return-b.Ob}},H:function(a,b){try{return a=a?K(y,a):"",Nb(Bb,a,b)}catch(c){if("undefined"==typeof U||"ErrnoError"!==c.name)throw c;return-c.Ob}},r:function(a,b,c){try{b=b?K(y,b):"";b=Mb(a,b);if(0===c){a=b;var d=R(a,{parent:!0}).node;if(!d)throw new N(44); +var e=Ma(a),h=cb(d,e),g=sb(d,e,!1);if(g)throw new N(g);if(!d.Cb.oc)throw new N(63);if(h.$b)throw new N(10);d.Cb.oc(d,e);nb(h)}else 512===c?Ab(b):u("Invalid flags passed to unlinkat");return 0}catch(n){if("undefined"==typeof U||"ErrnoError"!==n.name)throw n;return-n.Ob}},q:function(a,b,c){try{b=b?K(y,b):"";b=Mb(a,b,!0);if(c){var d=B[c>>2]+4294967296*z[c+4>>2],e=z[c+8>>2];h=1E3*d+e/1E6;c+=16;d=B[c>>2]+4294967296*z[c+4>>2];e=z[c+8>>2];g=1E3*d+e/1E6}else var h=Date.now(),g=h;a=h;var n=R(b,{Yb:!0}).node; +n.Cb.Qb(n,{timestamp:Math.max(a,g)});return 0}catch(k){if("undefined"==typeof U||"ErrnoError"!==k.name)throw k;return-k.Ob}},l:function(a,b,c){a=new Date(1E3*Qb(a,b));z[c>>2]=a.getSeconds();z[c+4>>2]=a.getMinutes();z[c+8>>2]=a.getHours();z[c+12>>2]=a.getDate();z[c+16>>2]=a.getMonth();z[c+20>>2]=a.getFullYear()-1900;z[c+24>>2]=a.getDay();b=a.getFullYear();z[c+28>>2]=(0!==b%4||0===b%100&&0!==b%400?Sb:Rb)[a.getMonth()]+a.getDate()-1|0;z[c+36>>2]=-(60*a.getTimezoneOffset());b=(new Date(a.getFullYear(), +6,1)).getTimezoneOffset();var d=(new Date(a.getFullYear(),0,1)).getTimezoneOffset();z[c+32>>2]=(b!=d&&a.getTimezoneOffset()==Math.min(d,b))|0},i:function(a,b,c,d,e,h,g,n){e=Qb(e,h);try{if(isNaN(e))return 61;var k=S(d);if(0!==(b&2)&&0===(c&2)&&2!==(k.flags&2097155))throw new N(2);if(1===(k.flags&2097155))throw new N(2);if(!k.Mb.kc)throw new N(43);var l=k.Mb.kc(k,a,e,b,c);var r=l.Oc;z[g>>2]=l.Cc;B[n>>2]=r;return 0}catch(m){if("undefined"==typeof U||"ErrnoError"!==m.name)throw m;return-m.Ob}},j:function(a, +b,c,d,e,h,g){h=Qb(h,g);try{if(isNaN(h))return 61;var n=S(e);if(c&2){if(32768!==(n.node.mode&61440))throw new N(43);d&2||n.Mb.lc&&n.Mb.lc(n,y.slice(a,a+b),h,b,d)}}catch(k){if("undefined"==typeof U||"ErrnoError"!==k.name)throw k;return-k.Ob}},s:(a,b,c)=>{function d(k){return(k=k.toTimeString().match(/\(([A-Za-z ]+)\)$/))?k[1]:"GMT"}var e=(new Date).getFullYear(),h=new Date(e,0,1),g=new Date(e,6,1);e=h.getTimezoneOffset();var n=g.getTimezoneOffset();B[a>>2]=60*Math.max(e,n);z[b>>2]=Number(e!=n);a=d(h); +b=d(g);a=Ub(a);b=Ub(b);n>2]=a,B[c+4>>2]=b):(B[c>>2]=b,B[c+4>>2]=a)},e:()=>Date.now(),d:()=>performance.now(),o:a=>{var b=y.length;a>>>=0;if(2147483648=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);var e=Math;d=Math.max(a,d);a:{e=(e.min.call(e,2147483648,d+(65536-d%65536)%65536)-ma.buffer.byteLength+65535)/65536;try{ma.grow(e);ra();var h=1;break a}catch(g){}h=void 0}if(h)return!0}return!1},A:(a,b)=>{var c=0;Xb().forEach((d,e)=>{var h=b+c;e=B[a+4*e>>2]=h;for(h= +0;h>0]=d.charCodeAt(h);w[e>>0]=0;c+=d.length+1});return 0},B:(a,b)=>{var c=Xb();B[a>>2]=c.length;var d=0;c.forEach(e=>d+=e.length+1);B[b>>2]=d;return 0},f:function(a){try{var b=S(a);if(null===b.Wb)throw new N(8);b.pc&&(b.pc=null);try{b.Mb.close&&b.Mb.close(b)}catch(c){throw c;}finally{hb[b.Wb]=null}b.Wb=null;return 0}catch(c){if("undefined"==typeof U||"ErrnoError"!==c.name)throw c;return c.Ob}},p:function(a,b){try{var c=S(a);w[b>>0]=c.Sb?2:P(c.mode)?3:40960===(c.mode&61440)?7: +4;oa[b+2>>1]=0;F=[0,(D=0,1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];z[b+8>>2]=F[0];z[b+12>>2]=F[1];F=[0,(D=0,1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];z[b+16>>2]=F[0];z[b+20>>2]=F[1];return 0}catch(d){if("undefined"==typeof U||"ErrnoError"!==d.name)throw d;return d.Ob}},x:function(a,b,c,d){try{a:{var e=S(a);a=b;for(var h,g=b=0;g>2],k=B[a+4>>2];a+=8;var l=e,r=n,m=k,q=h,x= +w;if(0>m||0>q)throw new N(28);if(null===l.Wb)throw new N(8);if(1===(l.flags&2097155))throw new N(8);if(P(l.node.mode))throw new N(31);if(!l.Mb.read)throw new N(28);var A="undefined"!=typeof q;if(!A)q=l.position;else if(!l.seekable)throw new N(70);var G=l.Mb.read(l,x,r,m,q);A||(l.position+=G);var E=G;if(0>E){var L=-1;break a}b+=E;if(E>2]=L;return 0}catch(H){if("undefined"==typeof U||"ErrnoError"!==H.name)throw H;return H.Ob}},m:function(a,b,c,d,e){b= +Qb(b,c);try{if(isNaN(b))return 61;var h=S(a);Hb(h,b,d);F=[h.position>>>0,(D=h.position,1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];z[e>>2]=F[0];z[e+4>>2]=F[1];h.pc&&0===b&&0===d&&(h.pc=null);return 0}catch(g){if("undefined"==typeof U||"ErrnoError"!==g.name)throw g;return g.Ob}},D:function(a){try{var b=S(a);return Yc(c=>{var d=b.node.Ub;d.type.Qc?d.type.Qc(d,!1,e=>{e?c(29):c(0)}):c(0)})}catch(c){if("undefined"==typeof U||"ErrnoError"!==c.name)throw c; +return c.Ob}},t:function(a,b,c,d){try{a:{var e=S(a);a=b;for(var h,g=b=0;g>2],k=B[a+4>>2];a+=8;var l=e,r=n,m=k,q=h,x=w;if(0>m||0>q)throw new N(28);if(null===l.Wb)throw new N(8);if(0===(l.flags&2097155))throw new N(8);if(P(l.node.mode))throw new N(31);if(!l.Mb.write)throw new N(28);l.seekable&&l.flags&1024&&Hb(l,0,2);var A="undefined"!=typeof q;if(!A)q=l.position;else if(!l.seekable)throw new N(70);var G=l.Mb.write(l,x,r,m,q,void 0);A||(l.position+=G);var E=G;if(0>E){var L=-1;break a}b+= +E;"undefined"!==typeof h&&(h+=E)}L=b}B[d>>2]=L;return 0}catch(H){if("undefined"==typeof U||"ErrnoError"!==H.name)throw H;return H.Ob}},ra:Yb,N:Zb,ga:$b,ca:ac,Y:bc,la:cc,G:dc,h:ec,oa:fc,ja:gc,ea:hc,fa:ic,k:jc,v:kc,pa:lc,g:mc,qa:nc,da:oc,ha:pc,ia:qc,na:rc,c:sc,ka:tc,ma:uc,aa:vc,V:wc,$:xc,ba:yc,S:zc,U:Ac,Z:Bc,X:Cc,R:Dc,Q:Ec,T:Fc,_:Gc,O:Hc,W:Ic,P:Jc},Y=function(){function a(c){c=c.exports;Y=c=Nc(c);ma=Y.sa;ra();ta.unshift(Y.ta);C--;f.monitorRunDependencies&&f.monitorRunDependencies(C);if(0==C&&(null!== +ya&&(clearInterval(ya),ya=null),za)){var d=za;za=null;d()}return c}var b={a:jd};C++;f.monitorRunDependencies&&f.monitorRunDependencies(C);if(f.instantiateWasm)try{return f.instantiateWasm(b,a)}catch(c){t(`Module.instantiateWasm callback failed with error: ${c}`),ba(c)}Ga(b,function(c){a(c.instance)}).catch(ba);return{}}();f._sqlite3_vfs_find=a=>(f._sqlite3_vfs_find=Y.ua)(a);f._sqlite3_malloc=a=>(f._sqlite3_malloc=Y.va)(a);f._sqlite3_free=a=>(f._sqlite3_free=Y.wa)(a); +f._sqlite3_prepare_v2=(a,b,c,d,e)=>(f._sqlite3_prepare_v2=Y.xa)(a,b,c,d,e);f._sqlite3_step=a=>(f._sqlite3_step=Y.ya)(a);f._sqlite3_column_int64=(a,b)=>(f._sqlite3_column_int64=Y.za)(a,b);f._sqlite3_column_int=(a,b)=>(f._sqlite3_column_int=Y.Aa)(a,b);f._sqlite3_finalize=a=>(f._sqlite3_finalize=Y.Ba)(a);f._sqlite3_reset=a=>(f._sqlite3_reset=Y.Ca)(a);f._sqlite3_clear_bindings=a=>(f._sqlite3_clear_bindings=Y.Da)(a);f._sqlite3_value_blob=a=>(f._sqlite3_value_blob=Y.Ea)(a); +f._sqlite3_value_text=a=>(f._sqlite3_value_text=Y.Fa)(a);f._sqlite3_value_bytes=a=>(f._sqlite3_value_bytes=Y.Ga)(a);f._sqlite3_value_double=a=>(f._sqlite3_value_double=Y.Ha)(a);f._sqlite3_value_int=a=>(f._sqlite3_value_int=Y.Ia)(a);f._sqlite3_value_int64=a=>(f._sqlite3_value_int64=Y.Ja)(a);f._sqlite3_value_type=a=>(f._sqlite3_value_type=Y.Ka)(a);f._sqlite3_result_blob=(a,b,c,d)=>(f._sqlite3_result_blob=Y.La)(a,b,c,d);f._sqlite3_result_double=(a,b)=>(f._sqlite3_result_double=Y.Ma)(a,b); +f._sqlite3_result_error=(a,b,c)=>(f._sqlite3_result_error=Y.Na)(a,b,c);f._sqlite3_result_int=(a,b)=>(f._sqlite3_result_int=Y.Oa)(a,b);f._sqlite3_result_int64=(a,b,c)=>(f._sqlite3_result_int64=Y.Pa)(a,b,c);f._sqlite3_result_null=a=>(f._sqlite3_result_null=Y.Qa)(a);f._sqlite3_result_text=(a,b,c,d)=>(f._sqlite3_result_text=Y.Ra)(a,b,c,d);f._sqlite3_column_count=a=>(f._sqlite3_column_count=Y.Sa)(a);f._sqlite3_data_count=a=>(f._sqlite3_data_count=Y.Ta)(a); +f._sqlite3_column_blob=(a,b)=>(f._sqlite3_column_blob=Y.Ua)(a,b);f._sqlite3_column_bytes=(a,b)=>(f._sqlite3_column_bytes=Y.Va)(a,b);f._sqlite3_column_double=(a,b)=>(f._sqlite3_column_double=Y.Wa)(a,b);f._sqlite3_column_text=(a,b)=>(f._sqlite3_column_text=Y.Xa)(a,b);f._sqlite3_column_type=(a,b)=>(f._sqlite3_column_type=Y.Ya)(a,b);f._sqlite3_column_name=(a,b)=>(f._sqlite3_column_name=Y.Za)(a,b);f._sqlite3_bind_blob=(a,b,c,d,e)=>(f._sqlite3_bind_blob=Y._a)(a,b,c,d,e); +f._sqlite3_bind_double=(a,b,c)=>(f._sqlite3_bind_double=Y.$a)(a,b,c);f._sqlite3_bind_int=(a,b,c)=>(f._sqlite3_bind_int=Y.ab)(a,b,c);f._sqlite3_bind_int64=(a,b,c,d)=>(f._sqlite3_bind_int64=Y.bb)(a,b,c,d);f._sqlite3_bind_null=(a,b)=>(f._sqlite3_bind_null=Y.cb)(a,b);f._sqlite3_bind_text=(a,b,c,d,e)=>(f._sqlite3_bind_text=Y.db)(a,b,c,d,e);f._sqlite3_bind_parameter_count=a=>(f._sqlite3_bind_parameter_count=Y.eb)(a);f._sqlite3_bind_parameter_name=(a,b)=>(f._sqlite3_bind_parameter_name=Y.fb)(a,b); +f._sqlite3_sql=a=>(f._sqlite3_sql=Y.gb)(a);f._sqlite3_exec=(a,b,c,d,e)=>(f._sqlite3_exec=Y.hb)(a,b,c,d,e);f._sqlite3_errmsg=a=>(f._sqlite3_errmsg=Y.ib)(a);f._sqlite3_declare_vtab=(a,b)=>(f._sqlite3_declare_vtab=Y.jb)(a,b);f._sqlite3_libversion=()=>(f._sqlite3_libversion=Y.kb)();f._sqlite3_libversion_number=()=>(f._sqlite3_libversion_number=Y.lb)();f._sqlite3_changes=a=>(f._sqlite3_changes=Y.mb)(a);f._sqlite3_close=a=>(f._sqlite3_close=Y.nb)(a); +f._sqlite3_limit=(a,b,c)=>(f._sqlite3_limit=Y.ob)(a,b,c);f._sqlite3_open_v2=(a,b,c,d)=>(f._sqlite3_open_v2=Y.pb)(a,b,c,d);f._sqlite3_get_autocommit=a=>(f._sqlite3_get_autocommit=Y.qb)(a);var hd=()=>(hd=Y.rb)(),Tb=f._malloc=a=>(Tb=f._malloc=Y.sb)(a),bd=f._free=a=>(bd=f._free=Y.tb)(a);f._RegisterExtensionFunctions=a=>(f._RegisterExtensionFunctions=Y.ub)(a);f._set_authorizer=a=>(f._set_authorizer=Y.vb)(a);f._create_function=(a,b,c,d,e,h)=>(f._create_function=Y.wb)(a,b,c,d,e,h); +f._create_module=(a,b,c,d)=>(f._create_module=Y.xb)(a,b,c,d);f._progress_handler=(a,b)=>(f._progress_handler=Y.yb)(a,b);f._register_vfs=(a,b,c,d)=>(f._register_vfs=Y.zb)(a,b,c,d);f._getSqliteFree=()=>(f._getSqliteFree=Y.Ab)();var kd=f._main=(a,b)=>(kd=f._main=Y.Bb)(a,b),db=(a,b)=>(db=Y.Db)(a,b),ld=()=>(ld=Y.Eb)(),gd=()=>(gd=Y.Fb)(),ed=a=>(ed=Y.Gb)(a),fd=a=>(fd=Y.Hb)(a),$c=a=>($c=Y.Ib)(a),Pc=()=>(Pc=Y.Jb)(),Zc=a=>(Zc=Y.Kb)(a),ad=()=>(ad=Y.Lb)();f.getTempRet0=ld;f.ccall=Z; +f.cwrap=(a,b,c,d)=>{var e=!c||c.every(h=>"number"===h||"boolean"===h);return"string"!==b&&e&&!d?f["_"+a]:function(){return Z(a,b,c,arguments,d)}};f.setValue=J;f.getValue=I;f.UTF8ToString=(a,b)=>a?K(y,a,b):"";f.stringToUTF8=(a,b,c)=>Sa(a,y,b,c);f.lengthBytesUTF8=Ra;var md;za=function nd(){md||od();md||(za=nd)}; +function od(){function a(){if(!md&&(md=!0,f.calledRun=!0,!v)){f.noFSInit||Jb||(Jb=!0,Ib(),f.stdin=f.stdin,f.stdout=f.stdout,f.stderr=f.stderr,f.stdin?Kb("stdin",f.stdin):zb("/dev/tty","/dev/stdin"),f.stdout?Kb("stdout",null,f.stdout):zb("/dev/tty","/dev/stdout"),f.stderr?Kb("stderr",null,f.stderr):zb("/dev/tty1","/dev/stderr"),Fb("/dev/stdin",0),Fb("/dev/stdout",1),Fb("/dev/stderr",1));jb=!1;Ia(ta);Ia(ua);aa(f);if(f.onRuntimeInitialized)f.onRuntimeInitialized();if(pd){var b=kd;try{var c=b(0,0);na= +c;Kc(c)}catch(d){Lc(d)}}if(f.postRun)for("function"==typeof f.postRun&&(f.postRun=[f.postRun]);f.postRun.length;)b=f.postRun.shift(),va.unshift(b);Ia(va)}}if(!(0 { + var _scriptDir = import.meta.url; + + return ( +function(moduleArg = {}) { + +var f=moduleArg,aa,ba;f.ready=new Promise((a,b)=>{aa=a;ba=b});var ca=Object.assign({},f),ea="./this.program",fa=(a,b)=>{throw b;},ha="object"==typeof window,ia="function"==typeof importScripts,q="",ja; +if(ha||ia)ia?q=self.location.href:"undefined"!=typeof document&&document.currentScript&&(q=document.currentScript.src),_scriptDir&&(q=_scriptDir),0!==q.indexOf("blob:")?q=q.substr(0,q.replace(/[?#].*/,"").lastIndexOf("/")+1):q="",ia&&(ja=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)});var ka=f.print||console.log.bind(console),t=f.printErr||console.error.bind(console);Object.assign(f,ca);ca=null;f.thisProgram&&(ea=f.thisProgram); +f.quit&&(fa=f.quit);var la;f.wasmBinary&&(la=f.wasmBinary);var noExitRuntime=f.noExitRuntime||!0;"object"!=typeof WebAssembly&&u("no native wasm support detected");var ma,na=!1,v,w,oa,x,z,pa,qa;function ra(){var a=ma.buffer;f.HEAP8=v=new Int8Array(a);f.HEAP16=oa=new Int16Array(a);f.HEAPU8=w=new Uint8Array(a);f.HEAPU16=new Uint16Array(a);f.HEAP32=x=new Int32Array(a);f.HEAPU32=z=new Uint32Array(a);f.HEAPF32=pa=new Float32Array(a);f.HEAPF64=qa=new Float64Array(a)}var sa=[],ta=[],ua=[],va=[]; +function wa(){var a=f.preRun.shift();sa.unshift(a)}var B=0,xa=null,ya=null;function u(a){if(f.onAbort)f.onAbort(a);a="Aborted("+a+")";t(a);na=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");ba(a);throw a;}function za(a){return a.startsWith("data:application/octet-stream;base64,")}var C;if(f.locateFile){if(C="wa-sqlite.wasm",!za(C)){var Aa=C;C=f.locateFile?f.locateFile(Aa,q):q+Aa}}else C=(new URL("wa-sqlite.wasm",import.meta.url)).href; +function Ba(a){if(a==C&&la)return new Uint8Array(la);if(ja)return ja(a);throw"both async and sync fetching of the wasm failed";}function Ca(a){return la||!ha&&!ia||"function"!=typeof fetch?Promise.resolve().then(()=>Ba(a)):fetch(a,{credentials:"same-origin"}).then(b=>{if(!b.ok)throw"failed to load wasm binary file at '"+a+"'";return b.arrayBuffer()}).catch(()=>Ba(a))} +function Da(a,b,c){return Ca(a).then(d=>WebAssembly.instantiate(d,b)).then(d=>d).then(c,d=>{t(`failed to asynchronously prepare wasm: ${d}`);u(d)})}function Ea(a,b){var c=C;return la||"function"!=typeof WebAssembly.instantiateStreaming||za(c)||"function"!=typeof fetch?Da(c,a,b):fetch(c,{credentials:"same-origin"}).then(d=>WebAssembly.instantiateStreaming(d,a).then(b,function(e){t(`wasm streaming compile failed: ${e}`);t("falling back to ArrayBuffer instantiation");return Da(c,a,b)}))}var D,F; +function Fa(a){this.name="ExitStatus";this.message=`Program terminated with exit(${a})`;this.status=a}var Ga=a=>{for(;0>0];case "i8":return v[a>>0];case "i16":return oa[a>>1];case "i32":return x[a>>2];case "i64":u("to do getValue(i64) use WASM_BIGINT");case "float":return pa[a>>2];case "double":return qa[a>>3];case "*":return z[a>>2];default:u(`invalid type for getValue: ${b}`)}} +function J(a,b,c="i8"){c.endsWith("*")&&(c="*");switch(c){case "i1":v[a>>0]=b;break;case "i8":v[a>>0]=b;break;case "i16":oa[a>>1]=b;break;case "i32":x[a>>2]=b;break;case "i64":u("to do setValue(i64) use WASM_BIGINT");case "float":pa[a>>2]=b;break;case "double":qa[a>>3]=b;break;case "*":z[a>>2]=b;break;default:u(`invalid type for setValue: ${c}`)}} +var Ha="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,K=(a,b,c)=>{var d=b+c;for(c=b;a[c]&&!(c>=d);)++c;if(16e?d+=String.fromCharCode(e):(e-=65536,d+=String.fromCharCode(55296|e>>10,56320|e&1023))}}else d+=String.fromCharCode(e)}return d}, +Ia=(a,b)=>{for(var c=0,d=a.length-1;0<=d;d--){var e=a[d];"."===e?a.splice(d,1):".."===e?(a.splice(d,1),c++):c&&(a.splice(d,1),c--)}if(b)for(;c;c--)a.unshift("..");return a},M=a=>{var b="/"===a.charAt(0),c="/"===a.substr(-1);(a=Ia(a.split("/").filter(d=>!!d),!b).join("/"))||b||(a=".");a&&c&&(a+="/");return(b?"/":"")+a},Ja=a=>{var b=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(a).slice(1);a=b[0];b=b[1];if(!a&&!b)return".";b&&(b=b.substr(0,b.length-1));return a+b},Ka=a=>{if("/"=== +a)return"/";a=M(a);a=a.replace(/\/$/,"");var b=a.lastIndexOf("/");return-1===b?a:a.substr(b+1)},La=()=>{if("object"==typeof crypto&&"function"==typeof crypto.getRandomValues)return a=>crypto.getRandomValues(a);u("initRandomDevice")},Ma=a=>(Ma=La())(a); +function Na(){for(var a="",b=!1,c=arguments.length-1;-1<=c&&!b;c--){b=0<=c?arguments[c]:"/";if("string"!=typeof b)throw new TypeError("Arguments to path.resolve must be strings");if(!b)return"";a=b+"/"+a;b="/"===b.charAt(0)}a=Ia(a.split("/").filter(d=>!!d),!b).join("/");return(b?"/":"")+a||"."} +var Oa=[],N=a=>{for(var b=0,c=0;c=d?b++:2047>=d?b+=2:55296<=d&&57343>=d?(b+=4,++c):b+=3}return b},O=(a,b,c,d)=>{if(!(0=g){var m=a.charCodeAt(++h);g=65536+((g&1023)<<10)|m&1023}if(127>=g){if(c>=d)break;b[c++]=g}else{if(2047>=g){if(c+1>=d)break;b[c++]=192|g>>6}else{if(65535>=g){if(c+2>=d)break;b[c++]=224|g>>12}else{if(c+3>=d)break;b[c++]=240|g>>18;b[c++]=128|g>> +12&63}b[c++]=128|g>>6&63}b[c++]=128|g&63}}b[c]=0;return c-e},Pa=[];function Qa(a,b){Pa[a]={input:[],Nb:[],Zb:b};Ra(a,Sa)} +var Sa={open(a){var b=Pa[a.node.bc];if(!b)throw new P(43);a.Ob=b;a.seekable=!1},close(a){a.Ob.Zb.Wb(a.Ob)},Wb(a){a.Ob.Zb.Wb(a.Ob)},read(a,b,c,d){if(!a.Ob||!a.Ob.Zb.sc)throw new P(60);for(var e=0,h=0;h=b||(b=Math.max(b,c*(1048576>c?2:1.125)>>>0),0!=c&&(b=Math.max(b,256)),c=a.Jb,a.Jb=new Uint8Array(b),0=a.node.Lb)return 0;a=Math.min(a.node.Lb-e,d);if(8b)throw new P(28);return b},pc(a,b,c){Va(a.node,b+c);a.node.Lb=Math.max(a.node.Lb,b+c)},fc(a,b,c,d,e){if(32768!==(a.node.mode&61440))throw new P(43);a=a.node.Jb;if(e&2||a.buffer!== +v.buffer){if(0{var c=0;a&&(c|=365);b&&(c|=146);return c},bb=null,cb={},db=[],eb=1,S=null,fb=!0,P=null,Ya={}; +function T(a,b={}){a=Na(a);if(!a)return{path:"",node:null};b=Object.assign({rc:!0,nc:0},b);if(8!!g);for(var c=bb,d="/",e=0;e>>0)%S.length}function jb(a){var b=ib(a.parent.id,a.name);if(S[b]===a)S[b]=a.Yb;else for(b=S[b];b;){if(b.Yb===a){b.Yb=a.Yb;break}b=b.Yb}} +function Za(a,b){var c;if(c=(c=kb(a,"x"))?c:a.Cb.$b?0:2)throw new P(c,a);for(c=S[ib(a.id,b)];c;c=c.Yb){var d=c.name;if(c.parent.id===a.id&&d===b)return c}return a.Cb.$b(a,b)}function Xa(a,b,c,d){a=new lb(a,b,c,d);b=ib(a.parent.id,a.name);a.Yb=S[b];return S[b]=a}function R(a){return 16384===(a&61440)}function mb(a){var b=["r","w","rw"][a&3];a&512&&(b+="w");return b} +function kb(a,b){if(fb)return 0;if(!b.includes("r")||a.mode&292){if(b.includes("w")&&!(a.mode&146)||b.includes("x")&&!(a.mode&73))return 2}else return 2;return 0}function nb(a,b){try{return Za(a,b),20}catch(c){}return kb(a,"wx")}function ob(a,b,c){try{var d=Za(a,b)}catch(e){return e.Kb}if(a=kb(a,"wx"))return a;if(c){if(!R(d.mode))return 54;if(d===d.parent||"/"===hb(d))return 10}else if(R(d.mode))return 31;return 0}function pb(){for(var a=0;4096>=a;a++)if(!db[a])return a;throw new P(33);} +function V(a){a=db[a];if(!a)throw new P(8);return a}function qb(a,b=-1){rb||(rb=function(){this.dc={}},rb.prototype={},Object.defineProperties(rb.prototype,{object:{get(){return this.node},set(c){this.node=c}},flags:{get(){return this.dc.flags},set(c){this.dc.flags=c}},position:{get(){return this.dc.position},set(c){this.dc.position=c}}}));a=Object.assign(new rb,a);-1==b&&(b=pb());a.Sb=b;return db[b]=a}var Wa={open(a){a.Ib=cb[a.node.bc].Ib;a.Ib.open&&a.Ib.open(a)},Vb(){throw new P(70);}}; +function Ra(a,b){cb[a]={Ib:b}}function sb(a,b){var c="/"===b,d=!b;if(c&&bb)throw new P(10);if(!c&&!d){var e=T(b,{rc:!1});b=e.path;e=e.node;if(e.Xb)throw new P(10);if(!R(e.mode))throw new P(54);}b={type:a,Wc:{},uc:b,Gc:[]};a=a.Rb(b);a.Rb=b;b.root=a;c?bb=a:e&&(e.Xb=b,e.Rb&&e.Rb.Gc.push(b))}function tb(a,b,c){var d=T(a,{parent:!0}).node;a=Ka(a);if(!a||"."===a||".."===a)throw new P(28);var e=nb(d,a);if(e)throw new P(e);if(!d.Cb.ec)throw new P(63);return d.Cb.ec(d,a,b,c)} +function W(a,b){return tb(a,(void 0!==b?b:511)&1023|16384,0)}function ub(a,b,c){"undefined"==typeof c&&(c=b,b=438);tb(a,b|8192,c)}function vb(a,b){if(!Na(a))throw new P(44);var c=T(b,{parent:!0}).node;if(!c)throw new P(44);b=Ka(b);var d=nb(c,b);if(d)throw new P(d);if(!c.Cb.jc)throw new P(63);c.Cb.jc(c,b,a)}function wb(a){var b=T(a,{parent:!0}).node;a=Ka(a);var c=Za(b,a),d=ob(b,a,!0);if(d)throw new P(d);if(!b.Cb.ic)throw new P(63);if(c.Xb)throw new P(10);b.Cb.ic(b,a);jb(c)} +function gb(a){a=T(a).node;if(!a)throw new P(44);if(!a.Cb.cc)throw new P(28);return Na(hb(a.parent),a.Cb.cc(a))}function xb(a,b){a=T(a,{Ub:!b}).node;if(!a)throw new P(44);if(!a.Cb.Pb)throw new P(63);return a.Cb.Pb(a)}function yb(a){return xb(a,!0)}function zb(a,b){a="string"==typeof a?T(a,{Ub:!0}).node:a;if(!a.Cb.Mb)throw new P(63);a.Cb.Mb(a,{mode:b&4095|a.mode&-4096,timestamp:Date.now()})} +function Ab(a,b){if(0>b)throw new P(28);a="string"==typeof a?T(a,{Ub:!0}).node:a;if(!a.Cb.Mb)throw new P(63);if(R(a.mode))throw new P(31);if(32768!==(a.mode&61440))throw new P(28);var c=kb(a,"w");if(c)throw new P(c);a.Cb.Mb(a,{size:b,timestamp:Date.now()})} +function Bb(a,b,c){if(""===a)throw new P(44);if("string"==typeof b){var d={r:0,"r+":2,w:577,"w+":578,a:1089,"a+":1090}[b];if("undefined"==typeof d)throw Error(`Unknown file open mode: ${b}`);b=d}c=b&64?("undefined"==typeof c?438:c)&4095|32768:0;if("object"==typeof a)var e=a;else{a=M(a);try{e=T(a,{Ub:!(b&131072)}).node}catch(h){}}d=!1;if(b&64)if(e){if(b&128)throw new P(20);}else e=tb(a,c,0),d=!0;if(!e)throw new P(44);8192===(e.mode&61440)&&(b&=-513);if(b&65536&&!R(e.mode))throw new P(54);if(!d&&(c= +e?40960===(e.mode&61440)?32:R(e.mode)&&("r"!==mb(b)||b&512)?31:kb(e,mb(b)):44))throw new P(c);b&512&&!d&&Ab(e,0);b&=-131713;e=qb({node:e,path:hb(e),flags:b,seekable:!0,position:0,Ib:e.Ib,Lc:[],error:!1});e.Ib.open&&e.Ib.open(e);!f.logReadFiles||b&1||(Cb||(Cb={}),a in Cb||(Cb[a]=1));return e}function Db(a,b,c){if(null===a.Sb)throw new P(8);if(!a.seekable||!a.Ib.Vb)throw new P(70);if(0!=c&&1!=c&&2!=c)throw new P(28);a.position=a.Ib.Vb(a,b,c);a.Lc=[]} +function Eb(){P||(P=function(a,b){this.name="ErrnoError";this.node=b;this.Kc=function(c){this.Kb=c};this.Kc(a);this.message="FS error"},P.prototype=Error(),P.prototype.constructor=P,[44].forEach(a=>{Ya[a]=new P(a);Ya[a].stack=""}))}var Fb; +function Gb(a,b,c){a=M("/dev/"+a);var d=ab(!!b,!!c);Hb||(Hb=64);var e=Hb++<<8|0;Ra(e,{open(h){h.seekable=!1},close(){c&&c.buffer&&c.buffer.length&&c(10)},read(h,g,m,l){for(var k=0,p=0;p>2]=d.Cc;x[c+4>>2]=d.mode;z[c+8>>2]=d.Ic;x[c+12>>2]=d.uid;x[c+16>>2]=d.Ec;x[c+20>>2]=d.bc;F=[d.size>>>0,(D=d.size,1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];x[c+24>>2]=F[0];x[c+28>>2]=F[1];x[c+32>>2]=4096;x[c+36>>2]=d.Ac;a=d.yc.getTime();b=d.Hc.getTime();var e=d.Bc.getTime();F=[Math.floor(a/1E3)>>>0,(D=Math.floor(a/1E3),1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];x[c+40>>2]=F[0];x[c+44>>2]=F[1];z[c+48>>2]=a%1E3*1E3;F=[Math.floor(b/1E3)>>>0,(D=Math.floor(b/1E3),1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];x[c+56>>2]=F[0];x[c+60>>2]=F[1];z[c+64>>2]=b%1E3*1E3;F=[Math.floor(e/1E3)>>>0,(D=Math.floor(e/1E3),1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];x[c+72>>2]=F[0];x[c+76>>2]=F[1];z[c+80>>2]= +e%1E3*1E3;F=[d.tc>>>0,(D=d.tc,1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];x[c+88>>2]=F[0];x[c+92>>2]=F[1];return 0}var Kb=void 0;function Lb(){var a=x[Kb>>2];Kb+=4;return a} +var Mb=(a,b)=>b+2097152>>>0<4194305-!!a?(a>>>0)+4294967296*b:NaN,Nb=[0,31,60,91,121,152,182,213,244,274,305,335],Ob=[0,31,59,90,120,151,181,212,243,273,304,334],Qb=a=>{var b=N(a)+1,c=Pb(b);c&&O(a,w,c,b);return c},Rb={},Tb=()=>{if(!Sb){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:ea||"./this.program"},b;for(b in Rb)void 0===Rb[b]?delete a[b]:a[b]=Rb[b]; +var c=[];for(b in a)c.push(`${b}=${a[b]}`);Sb=c}return Sb},Sb;function Ub(){}function Vb(){}function Wb(){}function Xb(){}function Yb(){}function Zb(){}function $b(){}function ac(){}function bc(){}function cc(){}function dc(){}function ec(){}function fc(){}function gc(){}function hc(){}function ic(){}function jc(){}function kc(){}function lc(){}function mc(){}function nc(){}function oc(){}function pc(){}function qc(){}function rc(){}function sc(){}function tc(){}function uc(){}function vc(){} +function wc(){}function xc(){}function yc(){}function zc(){}function Ac(){}function Bc(){}function Cc(){}function Dc(){}function Ec(){}function Fc(){} +var Y=(a,b,c,d)=>{var e={string:k=>{var p=0;if(null!==k&&void 0!==k&&0!==k){p=N(k)+1;var n=Gc(p);O(k,w,n,p);p=n}return p},array:k=>{var p=Gc(k.length);v.set(k,p);return p}};a=f["_"+a];var h=[],g=0;if(d)for(var m=0;m0,write:(d,e,h,g)=>g});ub("/dev/null",259);Qa(1280,Ta);Qa(1536,Ua);ub("/dev/tty",1280);ub("/dev/tty1",1536);var a=new Uint8Array(1024),b=0,c=()=>{0===b&&(b=Ma(a).byteLength);return a[--b]};Gb("random",c);Gb("urandom",c);W("/dev/shm");W("/dev/shm/tmp")})(); +(function(){W("/proc");var a=W("/proc/self");W("/proc/self/fd");sb({Rb(){var b=Xa(a,"fd",16895,73);b.Cb={$b(c,d){var e=V(+d);c={parent:null,Rb:{uc:"fake"},Cb:{cc:()=>e.path}};return c.parent=c}};return b}},"/proc/self/fd")})(); +(function(){const a=new Map;f.setAuthorizer=function(b,c,d){c?a.set(b,{f:c,oc:d}):a.delete(b);return Y("set_authorizer","number",["number"],[b])};Ub=function(b,c,d,e,h,g){if(a.has(b)){const {f:m,oc:l}=a.get(b);return m(l,c,d?d?K(w,d):"":null,e?e?K(w,e):"":null,h?h?K(w,h):"":null,g?g?K(w,g):"":null)}return 0}})(); +(function(){const a=new Map,b=new Map;f.createFunction=function(c,d,e,h,g,m){const l=a.size;a.set(l,{f:m,Tb:g});return Y("create_function","number","number string number number number number".split(" "),[c,d,e,h,l,0])};f.createAggregate=function(c,d,e,h,g,m,l){const k=a.size;a.set(k,{step:m,Dc:l,Tb:g});return Y("create_function","number","number string number number number number".split(" "),[c,d,e,h,k,1])};f.getFunctionUserData=function(c){return b.get(c)};Wb=function(c,d,e,h){c=a.get(c);b.set(d, +c.Tb);c.f(d,new Uint32Array(w.buffer,h,e));b.delete(d)};Yb=function(c,d,e,h){c=a.get(c);b.set(d,c.Tb);c.step(d,new Uint32Array(w.buffer,h,e));b.delete(d)};Vb=function(c,d){c=a.get(c);b.set(d,c.Tb);c.Dc(d);b.delete(d)}})();(function(){const a=new Map;f.progressHandler=function(b,c,d,e){d?a.set(b,{f:d,oc:e}):a.delete(b);return Y("progress_handler",null,["number","number"],[b,c])};Xb=function(b){if(a.has(b)){const {f:c,oc:d}=a.get(b);return c(d)}return 0}})(); +(function(){function a(l,k){const p=`get${l}`,n=`set${l}`;return new Proxy(new DataView(w.buffer,k,"Int32"===l?4:8),{get(r,y){if(y===p)return function(A,G){if(!G)throw Error("must be little endian");return r[y](A,G)};if(y===n)return function(A,G,E){if(!E)throw Error("must be little endian");return r[y](A,G,E)};if("string"===typeof y&&y.match(/^(get)|(set)/))throw Error("invalid type");return r[y]}})}const b="object"===typeof Asyncify,c=new Map,d=new Map,e=new Map,h=b?new Set:null,g=b?new Set:null, +m=new Map;oc=function(l,k,p,n){m.set(l?K(w,l):"",{size:k,ac:Array.from(new Uint32Array(w.buffer,n,p))})};f.createModule=function(l,k,p,n){b&&(p.handleAsync=Asyncify.Fc);const r=c.size;c.set(r,{module:p,Tb:n});n=0;p.xCreate&&(n|=1);p.xConnect&&(n|=2);p.xBestIndex&&(n|=4);p.xDisconnect&&(n|=8);p.xDestroy&&(n|=16);p.xOpen&&(n|=32);p.xClose&&(n|=64);p.xFilter&&(n|=128);p.xNext&&(n|=256);p.xEof&&(n|=512);p.xColumn&&(n|=1024);p.xRowid&&(n|=2048);p.xUpdate&&(n|=4096);p.xBegin&&(n|=8192);p.xSync&&(n|=16384); +p.xCommit&&(n|=32768);p.xRollback&&(n|=65536);p.xFindFunction&&(n|=131072);p.xRename&&(n|=262144);return Y("create_module","number",["number","string","number","number"],[l,k,r,n])};ec=function(l,k,p,n,r,y){k=c.get(k);d.set(r,k);if(b){h.delete(r);for(const A of h)d.delete(A)}n=Array.from(new Uint32Array(w.buffer,n,p)).map(A=>A?K(w,A):"");return k.module.xCreate(l,k.Tb,n,r,a("Int32",y))};dc=function(l,k,p,n,r,y){k=c.get(k);d.set(r,k);if(b){h.delete(r);for(const A of h)d.delete(A)}n=Array.from(new Uint32Array(w.buffer, +n,p)).map(A=>A?K(w,A):"");return k.module.xConnect(l,k.Tb,n,r,a("Int32",y))};$b=function(l,k){var p=d.get(l),n=m.get("sqlite3_index_info").ac;const r={};r.nConstraint=I(k+n[0],"i32");r.aConstraint=[];var y=I(k+n[1],"*"),A=m.get("sqlite3_index_constraint").size;for(var G=0;G>=2;return z[g]+z[g+1]*2**32}const c="object"===typeof Asyncify,d=new Map,e=new Map; +f.registerVFS=function(g,m){if(Y("sqlite3_vfs_find","number",["string"],[g.name]))throw Error(`VFS '${g.name}' already registered`);c&&(g.handleAsync=Asyncify.Fc);var l=g.Vc??64;const k=f._malloc(4);m=Y("register_vfs","number",["string","number","number","number"],[g.name,l,m?1:0,k]);m||(l=I(k,"*"),d.set(l,g));f._free(k);return m};const h=c?new Set:null;tc=function(g){const m=e.get(g);c?h.add(g):e.delete(g);return m.xClose(g)};Ac=function(g,m,l,k){return e.get(g).xRead(g,w.subarray(m,m+l),b(k))}; +Fc=function(g,m,l,k){return e.get(g).xWrite(g,w.subarray(m,m+l),b(k))};Dc=function(g,m){return e.get(g).xTruncate(g,b(m))};Cc=function(g,m){return e.get(g).xSync(g,m)};xc=function(g,m){const l=e.get(g);m=a("BigInt64",m);return l.xFileSize(g,m)};yc=function(g,m){return e.get(g).xLock(g,m)};Ec=function(g,m){return e.get(g).xUnlock(g,m)};sc=function(g,m){const l=e.get(g);m=a("Int32",m);return l.xCheckReservedLock(g,m)};wc=function(g,m,l){const k=e.get(g);l=new DataView(w.buffer,l);return k.xFileControl(g, +m,l)};Bc=function(g){return e.get(g).xSectorSize(g)};vc=function(g){return e.get(g).xDeviceCharacteristics(g)};zc=function(g,m,l,k,p){g=d.get(g);e.set(l,g);if(c){h.delete(l);for(var n of h)e.delete(n)}n=null;if(k&64){n=1;const r=[];for(;n;){const y=w[m++];if(y)r.push(y);else switch(w[m]||(n=null),n){case 1:r.push(63);n=2;break;case 2:r.push(61);n=3;break;case 3:r.push(38),n=2}}n=(new TextDecoder).decode(new Uint8Array(r))}else m&&(n=m?K(w,m):"");p=a("Int32",p);return g.xOpen(n,l,k,p)};uc=function(g, +m,l){return d.get(g).xDelete(m?K(w,m):"",l)};rc=function(g,m,l,k){g=d.get(g);k=a("Int32",k);return g.xAccess(m?K(w,m):"",l,k)}})(); +var Kc={a:(a,b,c,d)=>{u(`Assertion failed: ${a?K(w,a):""}, at: `+[b?b?K(w,b):"":"unknown filename",c,d?d?K(w,d):"":"unknown function"])},K:function(a,b){try{return a=a?K(w,a):"",zb(a,b),0}catch(c){if("undefined"==typeof X||"ErrnoError"!==c.name)throw c;return-c.Kb}},M:function(a,b,c){try{b=b?K(w,b):"";b=Ib(a,b);if(c&-8)return-28;var d=T(b,{Ub:!0}).node;if(!d)return-44;a="";c&4&&(a+="r");c&2&&(a+="w");c&1&&(a+="x");return a&&kb(d,a)?-2:0}catch(e){if("undefined"==typeof X||"ErrnoError"!==e.name)throw e; +return-e.Kb}},L:function(a,b){try{var c=V(a);zb(c.node,b);return 0}catch(d){if("undefined"==typeof X||"ErrnoError"!==d.name)throw d;return-d.Kb}},J:function(a){try{var b=V(a).node;var c="string"==typeof b?T(b,{Ub:!0}).node:b;if(!c.Cb.Mb)throw new P(63);c.Cb.Mb(c,{timestamp:Date.now()});return 0}catch(d){if("undefined"==typeof X||"ErrnoError"!==d.name)throw d;return-d.Kb}},b:function(a,b,c){Kb=c;try{var d=V(a);switch(b){case 0:var e=Lb();if(0>e)return-28;for(;db[e];)e++;return qb(d,e).Sb;case 1:case 2:return 0; +case 3:return d.flags;case 4:return e=Lb(),d.flags|=e,0;case 5:return e=Lb(),oa[e+0>>1]=2,0;case 6:case 7:return 0;case 16:case 8:return-28;case 9:return x[Jc()>>2]=28,-1;default:return-28}}catch(h){if("undefined"==typeof X||"ErrnoError"!==h.name)throw h;return-h.Kb}},I:function(a,b){try{var c=V(a);return Jb(xb,c.path,b)}catch(d){if("undefined"==typeof X||"ErrnoError"!==d.name)throw d;return-d.Kb}},n:function(a,b,c){b=Mb(b,c);try{if(isNaN(b))return 61;var d=V(a);if(0===(d.flags&2097155))throw new P(28); +Ab(d.node,b);return 0}catch(e){if("undefined"==typeof X||"ErrnoError"!==e.name)throw e;return-e.Kb}},C:function(a,b){try{if(0===b)return-28;var c=N("/")+1;if(b=d)return-28;var e=gb(b), +h=Math.min(d,N(e)),g=v[c+h];O(e,w,c,d+1);v[c+h]=g;return h}catch(m){if("undefined"==typeof X||"ErrnoError"!==m.name)throw m;return-m.Kb}},u:function(a){try{return a=a?K(w,a):"",wb(a),0}catch(b){if("undefined"==typeof X||"ErrnoError"!==b.name)throw b;return-b.Kb}},H:function(a,b){try{return a=a?K(w,a):"",Jb(xb,a,b)}catch(c){if("undefined"==typeof X||"ErrnoError"!==c.name)throw c;return-c.Kb}},r:function(a,b,c){try{b=b?K(w,b):"";b=Ib(a,b);if(0===c){a=b;var d=T(a,{parent:!0}).node;if(!d)throw new P(44); +var e=Ka(a),h=Za(d,e),g=ob(d,e,!1);if(g)throw new P(g);if(!d.Cb.kc)throw new P(63);if(h.Xb)throw new P(10);d.Cb.kc(d,e);jb(h)}else 512===c?wb(b):u("Invalid flags passed to unlinkat");return 0}catch(m){if("undefined"==typeof X||"ErrnoError"!==m.name)throw m;return-m.Kb}},q:function(a,b,c){try{b=b?K(w,b):"";b=Ib(a,b,!0);if(c){var d=z[c>>2]+4294967296*x[c+4>>2],e=x[c+8>>2];h=1E3*d+e/1E6;c+=16;d=z[c>>2]+4294967296*x[c+4>>2];e=x[c+8>>2];g=1E3*d+e/1E6}else var h=Date.now(),g=h;a=h;var m=T(b,{Ub:!0}).node; +m.Cb.Mb(m,{timestamp:Math.max(a,g)});return 0}catch(l){if("undefined"==typeof X||"ErrnoError"!==l.name)throw l;return-l.Kb}},l:function(a,b,c){a=new Date(1E3*Mb(a,b));x[c>>2]=a.getSeconds();x[c+4>>2]=a.getMinutes();x[c+8>>2]=a.getHours();x[c+12>>2]=a.getDate();x[c+16>>2]=a.getMonth();x[c+20>>2]=a.getFullYear()-1900;x[c+24>>2]=a.getDay();b=a.getFullYear();x[c+28>>2]=(0!==b%4||0===b%100&&0!==b%400?Ob:Nb)[a.getMonth()]+a.getDate()-1|0;x[c+36>>2]=-(60*a.getTimezoneOffset());b=(new Date(a.getFullYear(), +6,1)).getTimezoneOffset();var d=(new Date(a.getFullYear(),0,1)).getTimezoneOffset();x[c+32>>2]=(b!=d&&a.getTimezoneOffset()==Math.min(d,b))|0},i:function(a,b,c,d,e,h,g,m){e=Mb(e,h);try{if(isNaN(e))return 61;var l=V(d);if(0!==(b&2)&&0===(c&2)&&2!==(l.flags&2097155))throw new P(2);if(1===(l.flags&2097155))throw new P(2);if(!l.Ib.fc)throw new P(43);var k=l.Ib.fc(l,a,e,b,c);var p=k.Jc;x[g>>2]=k.xc;z[m>>2]=p;return 0}catch(n){if("undefined"==typeof X||"ErrnoError"!==n.name)throw n;return-n.Kb}},j:function(a, +b,c,d,e,h,g){h=Mb(h,g);try{if(isNaN(h))return 61;var m=V(e);if(c&2){if(32768!==(m.node.mode&61440))throw new P(43);d&2||m.Ib.hc&&m.Ib.hc(m,w.slice(a,a+b),h,b,d)}}catch(l){if("undefined"==typeof X||"ErrnoError"!==l.name)throw l;return-l.Kb}},s:(a,b,c)=>{function d(l){return(l=l.toTimeString().match(/\(([A-Za-z ]+)\)$/))?l[1]:"GMT"}var e=(new Date).getFullYear(),h=new Date(e,0,1),g=new Date(e,6,1);e=h.getTimezoneOffset();var m=g.getTimezoneOffset();z[a>>2]=60*Math.max(e,m);x[b>>2]=Number(e!=m);a=d(h); +b=d(g);a=Qb(a);b=Qb(b);m>2]=a,z[c+4>>2]=b):(z[c>>2]=b,z[c+4>>2]=a)},e:()=>Date.now(),d:()=>performance.now(),o:a=>{var b=w.length;a>>>=0;if(2147483648=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);var e=Math;d=Math.max(a,d);a:{e=(e.min.call(e,2147483648,d+(65536-d%65536)%65536)-ma.buffer.byteLength+65535)/65536;try{ma.grow(e);ra();var h=1;break a}catch(g){}h=void 0}if(h)return!0}return!1},A:(a,b)=>{var c=0;Tb().forEach((d,e)=>{var h=b+c;e=z[a+4*e>>2]=h;for(h= +0;h>0]=d.charCodeAt(h);v[e>>0]=0;c+=d.length+1});return 0},B:(a,b)=>{var c=Tb();z[a>>2]=c.length;var d=0;c.forEach(e=>d+=e.length+1);z[b>>2]=d;return 0},f:function(a){try{var b=V(a);if(null===b.Sb)throw new P(8);b.lc&&(b.lc=null);try{b.Ib.close&&b.Ib.close(b)}catch(c){throw c;}finally{db[b.Sb]=null}b.Sb=null;return 0}catch(c){if("undefined"==typeof X||"ErrnoError"!==c.name)throw c;return c.Kb}},p:function(a,b){try{var c=V(a);v[b>>0]=c.Ob?2:R(c.mode)?3:40960===(c.mode&61440)?7: +4;oa[b+2>>1]=0;F=[0,(D=0,1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];x[b+8>>2]=F[0];x[b+12>>2]=F[1];F=[0,(D=0,1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];x[b+16>>2]=F[0];x[b+20>>2]=F[1];return 0}catch(d){if("undefined"==typeof X||"ErrnoError"!==d.name)throw d;return d.Kb}},x:function(a,b,c,d){try{a:{var e=V(a);a=b;for(var h,g=b=0;g>2],l=z[a+4>>2];a+=8;var k=e,p=m,n=l,r=h,y= +v;if(0>n||0>r)throw new P(28);if(null===k.Sb)throw new P(8);if(1===(k.flags&2097155))throw new P(8);if(R(k.node.mode))throw new P(31);if(!k.Ib.read)throw new P(28);var A="undefined"!=typeof r;if(!A)r=k.position;else if(!k.seekable)throw new P(70);var G=k.Ib.read(k,y,p,n,r);A||(k.position+=G);var E=G;if(0>E){var L=-1;break a}b+=E;if(E>2]=L;return 0}catch(H){if("undefined"==typeof X||"ErrnoError"!==H.name)throw H;return H.Kb}},m:function(a,b,c,d,e){b= +Mb(b,c);try{if(isNaN(b))return 61;var h=V(a);Db(h,b,d);F=[h.position>>>0,(D=h.position,1<=+Math.abs(D)?0>>0:~~+Math.ceil((D-+(~~D>>>0))/4294967296)>>>0:0)];x[e>>2]=F[0];x[e+4>>2]=F[1];h.lc&&0===b&&0===d&&(h.lc=null);return 0}catch(g){if("undefined"==typeof X||"ErrnoError"!==g.name)throw g;return g.Kb}},D:function(a){try{var b=V(a);return b.Ib&&b.Ib.Wb?b.Ib.Wb(b):0}catch(c){if("undefined"==typeof X||"ErrnoError"!==c.name)throw c;return c.Kb}},t:function(a,b,c,d){try{a:{var e= +V(a);a=b;for(var h,g=b=0;g>2],l=z[a+4>>2];a+=8;var k=e,p=m,n=l,r=h,y=v;if(0>n||0>r)throw new P(28);if(null===k.Sb)throw new P(8);if(0===(k.flags&2097155))throw new P(8);if(R(k.node.mode))throw new P(31);if(!k.Ib.write)throw new P(28);k.seekable&&k.flags&1024&&Db(k,0,2);var A="undefined"!=typeof r;if(!A)r=k.position;else if(!k.seekable)throw new P(70);var G=k.Ib.write(k,y,p,n,r,void 0);A||(k.position+=G);var E=G;if(0>E){var L=-1;break a}b+=E;"undefined"!==typeof h&&(h+=E)}L=b}z[d>> +2]=L;return 0}catch(H){if("undefined"==typeof X||"ErrnoError"!==H.name)throw H;return H.Kb}},ra:Ub,N:Vb,ga:Wb,ca:Xb,Y:Yb,la:Zb,G:$b,h:ac,oa:bc,ja:cc,ea:dc,fa:ec,k:fc,v:gc,pa:hc,g:ic,qa:jc,da:kc,ha:lc,ia:mc,na:nc,c:oc,ka:pc,ma:qc,aa:rc,V:sc,$:tc,ba:uc,S:vc,U:wc,Z:xc,X:yc,R:zc,Q:Ac,T:Bc,_:Cc,O:Dc,W:Ec,P:Fc},Z=function(){function a(c){Z=c=c.exports;ma=Z.sa;ra();ta.unshift(Z.ta);B--;f.monitorRunDependencies&&f.monitorRunDependencies(B);if(0==B&&(null!==xa&&(clearInterval(xa),xa=null),ya)){var d=ya;ya= +null;d()}return c}var b={a:Kc};B++;f.monitorRunDependencies&&f.monitorRunDependencies(B);if(f.instantiateWasm)try{return f.instantiateWasm(b,a)}catch(c){t(`Module.instantiateWasm callback failed with error: ${c}`),ba(c)}Ea(b,function(c){a(c.instance)}).catch(ba);return{}}();f._sqlite3_vfs_find=a=>(f._sqlite3_vfs_find=Z.ua)(a);f._sqlite3_malloc=a=>(f._sqlite3_malloc=Z.va)(a);f._sqlite3_free=a=>(f._sqlite3_free=Z.wa)(a);f._sqlite3_prepare_v2=(a,b,c,d,e)=>(f._sqlite3_prepare_v2=Z.xa)(a,b,c,d,e); +f._sqlite3_step=a=>(f._sqlite3_step=Z.ya)(a);f._sqlite3_column_int64=(a,b)=>(f._sqlite3_column_int64=Z.za)(a,b);f._sqlite3_column_int=(a,b)=>(f._sqlite3_column_int=Z.Aa)(a,b);f._sqlite3_finalize=a=>(f._sqlite3_finalize=Z.Ba)(a);f._sqlite3_reset=a=>(f._sqlite3_reset=Z.Ca)(a);f._sqlite3_clear_bindings=a=>(f._sqlite3_clear_bindings=Z.Da)(a);f._sqlite3_value_blob=a=>(f._sqlite3_value_blob=Z.Ea)(a);f._sqlite3_value_text=a=>(f._sqlite3_value_text=Z.Fa)(a); +f._sqlite3_value_bytes=a=>(f._sqlite3_value_bytes=Z.Ga)(a);f._sqlite3_value_double=a=>(f._sqlite3_value_double=Z.Ha)(a);f._sqlite3_value_int=a=>(f._sqlite3_value_int=Z.Ia)(a);f._sqlite3_value_int64=a=>(f._sqlite3_value_int64=Z.Ja)(a);f._sqlite3_value_type=a=>(f._sqlite3_value_type=Z.Ka)(a);f._sqlite3_result_blob=(a,b,c,d)=>(f._sqlite3_result_blob=Z.La)(a,b,c,d);f._sqlite3_result_double=(a,b)=>(f._sqlite3_result_double=Z.Ma)(a,b); +f._sqlite3_result_error=(a,b,c)=>(f._sqlite3_result_error=Z.Na)(a,b,c);f._sqlite3_result_int=(a,b)=>(f._sqlite3_result_int=Z.Oa)(a,b);f._sqlite3_result_int64=(a,b,c)=>(f._sqlite3_result_int64=Z.Pa)(a,b,c);f._sqlite3_result_null=a=>(f._sqlite3_result_null=Z.Qa)(a);f._sqlite3_result_text=(a,b,c,d)=>(f._sqlite3_result_text=Z.Ra)(a,b,c,d);f._sqlite3_column_count=a=>(f._sqlite3_column_count=Z.Sa)(a);f._sqlite3_data_count=a=>(f._sqlite3_data_count=Z.Ta)(a); +f._sqlite3_column_blob=(a,b)=>(f._sqlite3_column_blob=Z.Ua)(a,b);f._sqlite3_column_bytes=(a,b)=>(f._sqlite3_column_bytes=Z.Va)(a,b);f._sqlite3_column_double=(a,b)=>(f._sqlite3_column_double=Z.Wa)(a,b);f._sqlite3_column_text=(a,b)=>(f._sqlite3_column_text=Z.Xa)(a,b);f._sqlite3_column_type=(a,b)=>(f._sqlite3_column_type=Z.Ya)(a,b);f._sqlite3_column_name=(a,b)=>(f._sqlite3_column_name=Z.Za)(a,b);f._sqlite3_bind_blob=(a,b,c,d,e)=>(f._sqlite3_bind_blob=Z._a)(a,b,c,d,e); +f._sqlite3_bind_double=(a,b,c)=>(f._sqlite3_bind_double=Z.$a)(a,b,c);f._sqlite3_bind_int=(a,b,c)=>(f._sqlite3_bind_int=Z.ab)(a,b,c);f._sqlite3_bind_int64=(a,b,c,d)=>(f._sqlite3_bind_int64=Z.bb)(a,b,c,d);f._sqlite3_bind_null=(a,b)=>(f._sqlite3_bind_null=Z.cb)(a,b);f._sqlite3_bind_text=(a,b,c,d,e)=>(f._sqlite3_bind_text=Z.db)(a,b,c,d,e);f._sqlite3_bind_parameter_count=a=>(f._sqlite3_bind_parameter_count=Z.eb)(a);f._sqlite3_bind_parameter_name=(a,b)=>(f._sqlite3_bind_parameter_name=Z.fb)(a,b); +f._sqlite3_sql=a=>(f._sqlite3_sql=Z.gb)(a);f._sqlite3_exec=(a,b,c,d,e)=>(f._sqlite3_exec=Z.hb)(a,b,c,d,e);f._sqlite3_errmsg=a=>(f._sqlite3_errmsg=Z.ib)(a);f._sqlite3_declare_vtab=(a,b)=>(f._sqlite3_declare_vtab=Z.jb)(a,b);f._sqlite3_libversion=()=>(f._sqlite3_libversion=Z.kb)();f._sqlite3_libversion_number=()=>(f._sqlite3_libversion_number=Z.lb)();f._sqlite3_changes=a=>(f._sqlite3_changes=Z.mb)(a);f._sqlite3_close=a=>(f._sqlite3_close=Z.nb)(a); +f._sqlite3_limit=(a,b,c)=>(f._sqlite3_limit=Z.ob)(a,b,c);f._sqlite3_open_v2=(a,b,c,d)=>(f._sqlite3_open_v2=Z.pb)(a,b,c,d);f._sqlite3_get_autocommit=a=>(f._sqlite3_get_autocommit=Z.qb)(a);var Jc=()=>(Jc=Z.rb)(),Pb=f._malloc=a=>(Pb=f._malloc=Z.sb)(a);f._free=a=>(f._free=Z.tb)(a);f._RegisterExtensionFunctions=a=>(f._RegisterExtensionFunctions=Z.ub)(a);f._set_authorizer=a=>(f._set_authorizer=Z.vb)(a);f._create_function=(a,b,c,d,e,h)=>(f._create_function=Z.wb)(a,b,c,d,e,h); +f._create_module=(a,b,c,d)=>(f._create_module=Z.xb)(a,b,c,d);f._progress_handler=(a,b)=>(f._progress_handler=Z.yb)(a,b);f._register_vfs=(a,b,c,d)=>(f._register_vfs=Z.zb)(a,b,c,d);f._getSqliteFree=()=>(f._getSqliteFree=Z.Ab)();var Lc=f._main=(a,b)=>(Lc=f._main=Z.Bb)(a,b),$a=(a,b)=>($a=Z.Db)(a,b),Mc=()=>(Mc=Z.Eb)(),Hc=()=>(Hc=Z.Fb)(),Ic=a=>(Ic=Z.Gb)(a),Gc=a=>(Gc=Z.Hb)(a);f.getTempRet0=Mc;f.ccall=Y; +f.cwrap=(a,b,c,d)=>{var e=!c||c.every(h=>"number"===h||"boolean"===h);return"string"!==b&&e&&!d?f["_"+a]:function(){return Y(a,b,c,arguments,d)}};f.setValue=J;f.getValue=I;f.UTF8ToString=(a,b)=>a?K(w,a,b):"";f.stringToUTF8=(a,b,c)=>O(a,w,b,c);f.lengthBytesUTF8=N;var Nc;ya=function Oc(){Nc||Pc();Nc||(ya=Oc)}; +function Pc(){function a(){if(!Nc&&(Nc=!0,f.calledRun=!0,!na)){f.noFSInit||Fb||(Fb=!0,Eb(),f.stdin=f.stdin,f.stdout=f.stdout,f.stderr=f.stderr,f.stdin?Gb("stdin",f.stdin):vb("/dev/tty","/dev/stdin"),f.stdout?Gb("stdout",null,f.stdout):vb("/dev/tty","/dev/stdout"),f.stderr?Gb("stderr",null,f.stderr):vb("/dev/tty1","/dev/stderr"),Bb("/dev/stdin",0),Bb("/dev/stdout",1),Bb("/dev/stderr",1));fb=!1;Ga(ta);Ga(ua);aa(f);if(f.onRuntimeInitialized)f.onRuntimeInitialized();if(Qc){var b=Lc;try{var c=b(0,0);if(!noExitRuntime){if(f.onExit)f.onExit(c); +na=!0}fa(c,new Fa(c))}catch(d){d instanceof Fa||"unwind"==d||fa(1,d)}}if(f.postRun)for("function"==typeof f.postRun&&(f.postRun=[f.postRun]);f.postRun.length;)b=f.postRun.shift(),va.unshift(b);Ga(va)}}if(!(0 store.session.id); const tags = useStore((store) => store.tags); const setTag = useStore((store) => store.setTag); - console.log(tags); + return ( <> {id && ( diff --git a/apps/web/src/components/editor/index.tsx b/apps/web/src/components/editor/index.tsx index 42ff67952..f916e8156 100644 --- a/apps/web/src/components/editor/index.tsx +++ b/apps/web/src/components/editor/index.tsx @@ -78,7 +78,7 @@ type DocumentPreview = { function onEditorChange( noteId: string | undefined, - sessionId: number, + sessionId: string, content: string, ignoreEdit: boolean ) { @@ -94,10 +94,10 @@ export default function EditorManager({ noteId, nonce }: { - noteId: string | number; + noteId?: string; nonce?: string; }) { - const isNewSession = !!nonce && noteId === 0; + const isNewSession = !!nonce && !noteId; const isOldSession = !nonce && !!noteId; // the only state that changes. Everything else is @@ -166,7 +166,7 @@ export default function EditorManager({ }; }, [editorInstance, isPreviewSession]); - const openSession = useCallback(async (noteId: string | number) => { + const openSession = useCallback(async (noteId: string) => { await editorstore.get().openSession(noteId); previewSession.current = undefined; @@ -212,7 +212,7 @@ export default function EditorManager({ background: "background" }} > - {previewSession.current && ( + {previewSession.current && noteId && ( openSession(noteId)} @@ -390,8 +390,8 @@ export function Editor(props: EditorProps) { onDownloadAttachment={(attachment) => saveAttachment(attachment.hash)} onPreviewAttachment={async (data) => { const { hash } = data; - const attachment = db.attachments.attachment(hash); - if (attachment && attachment.metadata.type.startsWith("image/")) { + const attachment = await db.attachments.attachment(hash); + if (attachment && attachment.mimeType.startsWith("image/")) { const container = document.getElementById("dialogContainer"); if (!(container instanceof HTMLElement)) return; @@ -600,7 +600,8 @@ function PreviewModeNotice(props: PreviewModeNoticeProps) { Preview You are previewing note version edited from{" "} - {getFormattedDate(dateCreated)} to {getFormattedDate(dateEdited)}. + {getFormattedDate(dateCreated, "date-time")} to{" "} + {getFormattedDate(dateEdited, "date-time")}. diff --git a/apps/web/src/components/editor/tiptap.tsx b/apps/web/src/components/editor/tiptap.tsx index 9b4c3bc75..57266e585 100644 --- a/apps/web/src/components/editor/tiptap.tsx +++ b/apps/web/src/components/editor/tiptap.tsx @@ -60,7 +60,7 @@ import { useStore as useThemeStore } from "../../stores/theme-store"; type OnChangeHandler = ( id: string | undefined, - sessionId: number, + sessionId: string, content: string, ignoreEdit: boolean ) => void; @@ -88,7 +88,7 @@ type TipTapProps = { const SAVE_INTERVAL = IS_TESTING ? 100 : 300; function save( - sessionId: number, + sessionId: string, noteId: string | undefined, editor: Editor, content: Fragment, diff --git a/apps/web/src/components/editor/title-box.tsx b/apps/web/src/components/editor/title-box.tsx index 91ba1fab8..822cff971 100644 --- a/apps/web/src/components/editor/title-box.tsx +++ b/apps/web/src/components/editor/title-box.tsx @@ -61,8 +61,8 @@ function TitleBox(props: TitleBoxProps) { ); useEffect(() => { + const { title = "" } = useStore.getState().session; if (!inputRef.current) return; - const { title } = useStore.getState().session; inputRef.current.value = title; updateFontSize(title.length); }, [id, updateFontSize]); diff --git a/apps/web/src/components/filtered-list/index.tsx b/apps/web/src/components/filtered-list/index.tsx index 900b546ad..e3591b92b 100644 --- a/apps/web/src/components/filtered-list/index.tsx +++ b/apps/web/src/components/filtered-list/index.tsx @@ -22,14 +22,14 @@ import Field from "../field"; import { Plus, Search } from "../icons"; import { Button, Flex, Text } from "@theme-ui/components"; -type FilterableItem = { - id: string; - title: string; -}; +// type FilterableItem = { +// id: string; +// title: string; +// }; -type FilteredListProps = { +type FilteredListProps = { placeholders: { filter: string; empty: string }; - items: () => T[]; + items: () => Promise; filter: (items: T[], query: string) => T[]; onCreateNewItem: (title: string) => Promise; renderItem: ( @@ -40,9 +40,7 @@ type FilteredListProps = { ) => JSX.Element; }; -export function FilteredList( - props: FilteredListProps -) { +export function FilteredList(props: FilteredListProps) { const { items: _items, filter, @@ -56,8 +54,8 @@ export function FilteredList( const noItemsFound = items.length <= 0 && query && query.length > 0; const inputRef = useRef(null); - const refresh = useCallback(() => { - setItems(_items()); + const refresh = useCallback(async () => { + setItems(await _items()); }, [_items]); useEffect(() => { @@ -65,14 +63,10 @@ export function FilteredList( }, [refresh]); const _filter = useCallback( - (query) => { - setItems(() => { - const items = _items(); - if (!query) { - return items; - } - return filter(items, query); - }); + async (query) => { + const items = await _items(); + if (!query) return; + setItems(filter(items, query)); setQuery(query); }, [_items, filter] @@ -81,7 +75,7 @@ export function FilteredList( const _createNewItem = useCallback( async (title) => { await onCreateNewItem(title); - refresh(); + await refresh(); setQuery(undefined); if (inputRef.current) inputRef.current.value = ""; }, diff --git a/apps/web/src/components/group-header/index.tsx b/apps/web/src/components/group-header/index.tsx index aa4efad8c..c3cdd0805 100644 --- a/apps/web/src/components/group-header/index.tsx +++ b/apps/web/src/components/group-header/index.tsx @@ -188,7 +188,7 @@ export function showSortMenu(groupingKey: GroupingKey, refresh: () => void) { ); } -function changeGroupOptions( +async function changeGroupOptions( options: GroupingMenuOptions, item: Omit ) { @@ -201,7 +201,7 @@ function changeGroupOptions( if (item.key === "abc") groupOptions.sortBy = "title"; else groupOptions.sortBy = "dateEdited"; } - db.settings.setGroupOptions(options.groupingKey, groupOptions); + await db.settings.setGroupOptions(options.groupingKey, groupOptions); options.refresh(); } diff --git a/apps/web/src/components/list-container/index.tsx b/apps/web/src/components/list-container/index.tsx index e8564094a..a59634dc7 100644 --- a/apps/web/src/components/list-container/index.tsx +++ b/apps/web/src/components/list-container/index.tsx @@ -20,7 +20,12 @@ along with this program. If not, see . import { forwardRef, useEffect, useRef, useState } from "react"; import { Flex, Button } from "@theme-ui/components"; import { Plus } from "../icons"; -import { ScrollerProps, Virtuoso, VirtuosoHandle } from "react-virtuoso"; +import { + ItemProps, + ScrollerProps, + Virtuoso, + VirtuosoHandle +} from "react-virtuoso"; import { useStore as useSelectionStore, store as selectionStore @@ -33,12 +38,12 @@ import ScrollContainer from "../scroll-container"; import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation"; import { Context } from "./types"; import { + VirtualizedGrouping, GroupHeader as GroupHeaderType, - GroupedItems, GroupingKey, Item, isGroupHeader -} from "@notesnook/core/dist/types"; +} from "@notesnook/core"; export const CustomScrollbarsVirtualList = forwardRef< HTMLDivElement, @@ -57,7 +62,7 @@ export const CustomScrollbarsVirtualList = forwardRef< type ListContainerProps = { group?: GroupingKey; - items: GroupedItems; + items: VirtualizedGrouping; compact?: boolean; context?: Context; refresh: () => void; @@ -92,28 +97,28 @@ function ListContainer(props: ListContainerProps) { }, []); const { onFocus, onMouseDown, onKeyDown } = useKeyboardListNavigation({ - length: items.length, + length: items.ids.length, reset: () => toggleSelection(false), - deselect: (index) => deselectItem(items[index]), + deselect: (index) => deselectItem(items.ids[index]), select: (index, toggleable) => - toggleable && isSelected(items[index]) - ? deselectItem(items[index]) - : selectItem(items[index]), - bulkSelect: (indices) => setSelectedItems(indices.map((i) => items[i])), + toggleable && isSelected(items.ids[index]) + ? deselectItem(items.ids[index]) + : selectItem(items.ids[index]), + bulkSelect: (indices) => setSelectedItems(indices.map((i) => items.ids[i])), focusItemAt: (index) => { - const item = items[index]; + const item = items.ids[index]; if (!item || !listRef.current) return; - waitForElement(listRef.current, index, `id_${item.id}`, (element) => + waitForElement(listRef.current, index, `id_${item}`, (element) => element.focus() ); }, - skip: (index) => !items[index] || items[index].type === "header", + skip: (index) => !items.ids[index] || isGroupHeader(items.ids[index]), open: (index) => { - const item = items[index]; + const item = items.ids[index]; if (!item || !listRef.current) return; - waitForElement(listRef.current, index, `id_${item.id}`, (element) => + waitForElement(listRef.current, index, `id_${item}`, (element) => element.click() ); } @@ -121,7 +126,7 @@ function ListContainer(props: ListContainerProps) { return ( - {!props.items.length && props.placeholder ? ( + {!props.items.ids.length && props.placeholder ? ( <> {header} {props.isLoading ? ( @@ -141,85 +146,79 @@ function ListContainer(props: ListContainerProps) { > items[index].id} + data={items.ids} + computeItemKey={(index) => items.getKey(index)} defaultItemHeight={DEFAULT_ITEM_HEIGHT} - totalCount={items.length} + totalCount={items.ids.length} onBlur={() => setFocusedGroupIndex(-1)} onKeyDown={(e) => onKeyDown(e.nativeEvent)} components={{ Scroller: CustomScrollbarsVirtualList, - Item: ({ item: _item, ...props }) => ( -
onFocus(props["data-item-index"])} - onMouseDown={(e) => - onMouseDown(e.nativeEvent, props["data-item-index"]) - } - > - {props.children} -
- ), + Item: VirtuosoItem, Header: () => (header ? header : ) }} + context={{ + onMouseDown, + onFocus + }} itemContent={(index, item) => { - if (!item) return null; - - switch (item.type) { - case "header": - if (!group) return null; - return ( - { - let endIndex; - for (let i = index + 1; i < props.items.length; ++i) { - if (props.items[i].type === "header") { - endIndex = i; - break; - } + if (isGroupHeader(item)) { + if (!group) + return
; + return ( + { + let endIndex; + for ( + let i = index + 1; + i < props.items.ids.length; + ++i + ) { + if (typeof props.items.ids[i] === "object") { + endIndex = i; + break; } - setSelectedItems([ - ...selectionStore.get().selectedItems, - ...props.items.slice( - index, - endIndex || props.items.length - ) - ]); - }} - groups={ - props.items.filter((v) => - isGroupHeader(v) - ) as GroupHeaderType[] } - onJump={(title: string) => { - const index = props.items.findIndex( - (v) => isGroupHeader(v) && v.title === title - ); - if (index < 0) return; - listRef.current?.scrollToIndex({ + setSelectedItems([ + ...selectionStore.get().selectedItems, + ...props.items.ids.slice( index, - align: "center", - behavior: "auto" - }); - setFocusedGroupIndex(index); - }} - /> - ); - default: - return ( - - ); + endIndex || props.items.ids.length + ) + ]); + }} + groups={props.items.groups} + onJump={(title: string) => { + const index = props.items.ids.findIndex( + (v) => isGroupHeader(v) && v.title === title + ); + if (index < 0) return; + listRef.current?.scrollToIndex({ + index, + align: "center", + behavior: "auto" + }); + setFocusedGroupIndex(index); + }} + /> + ); } + + return ( + + ); }} /> @@ -252,6 +251,29 @@ function ListContainer(props: ListContainerProps) { } export default ListContainer; +function VirtuosoItem({ + item: _item, + context, + ...props +}: ItemProps & { + context?: { + onMouseDown: (e: MouseEvent, itemIndex: number) => void; + onFocus: (itemIndex: number) => void; + }; +}) { + return ( +
context?.onFocus(props["data-item-index"])} + onMouseDown={(e) => + context?.onMouseDown(e.nativeEvent, props["data-item-index"]) + } + > + {props.children} +
+ ); +} + /** * Scroll the element at the specified index into view and * wait until it renders into the DOM. This function keeps diff --git a/apps/web/src/components/list-container/list-profiles.tsx b/apps/web/src/components/list-container/list-profiles.tsx index 075a4d35b..5f4242e4d 100644 --- a/apps/web/src/components/list-container/list-profiles.tsx +++ b/apps/web/src/components/list-container/list-profiles.tsx @@ -23,15 +23,22 @@ import Tag from "../tag"; import Topic from "../topic"; import TrashItem from "../trash-item"; import { db } from "../../common/db"; -import { getTotalNotes } from "@notesnook/common"; import Reminder from "../reminder"; -import { ReferencesWithDateEdited, Reference, Context } from "./types"; +import { + Context, + TagsWithDateEdited, + WithDateEdited, + NotebooksWithDateEdited +} from "./types"; +import { getSortValue } from "@notesnook/core/dist/utils/grouping"; import { GroupingKey, Item, - NotebookReference -} from "@notesnook/core/dist/types"; -import { getSortValue } from "@notesnook/core/dist/utils/grouping"; + VirtualizedGrouping, + Color, + Reminder as ReminderItem +} from "@notesnook/core"; +import { useEffect, useRef, useState } from "react"; const SINGLE_LINE_HEIGHT = 1.4; const DEFAULT_LINE_HEIGHT = @@ -40,27 +47,53 @@ export const DEFAULT_ITEM_HEIGHT = SINGLE_LINE_HEIGHT * 2 * DEFAULT_LINE_HEIGHT; type ListItemWrapperProps = { group?: GroupingKey; - item: TItem; + items: VirtualizedGrouping; + id: string; context?: Context; compact?: boolean; }; -export function ListItemWrapper(props: ListItemWrapperProps) { - const { item, group, compact, context } = props; - const { type } = item; +export function ListItemWrapper(props: ListItemWrapperProps) { + const { id, items, group, compact, context } = props; + const [item, setItem] = useState(); + const tags = useRef(); + const notebooks = useRef(); + const reminder = useRef(); + const color = useRef(); + const totalNotes = useRef(0); + + useEffect(() => { + (async function () { + const { item, data } = (await items.item(id, resolveItems)) || {}; + if (!item) return; + if (item.type === "note" && isNoteResolvedData(data)) { + tags.current = data.tags; + notebooks.current = data.notebooks; + reminder.current = data.reminder; + color.current = data.color; + } else if (item.type === "notebook" && typeof data === "number") { + totalNotes.current = data; + } else if (item.type === "tag" && typeof data === "number") { + totalNotes.current = data; + } + setItem(item); + })(); + }, [id, items]); + + if (!item) + return
; + + const { type } = item; switch (type) { case "note": { - const tags = db.relations.to(item, "tag").resolved(3) || []; - const color = db.relations.to(item, "color").resolved(1)?.[0]; - const references = getReferences(item.id, item.notebooks, context?.type); return ( @@ -70,7 +103,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) { return ( ); @@ -81,71 +114,148 @@ export function ListItemWrapper(props: ListItemWrapperProps) { case "topic": return ; case "tag": - return ; + return ; default: return null; } } -function getReferences( - noteId: string, - notebooks?: NotebookReference[], - contextType?: string -): ReferencesWithDateEdited | undefined { - if (["topic", "notebook"].includes(contextType || "")) return; - - const references: Reference[] = []; +function withDateEdited< + T extends { dateEdited: number } | { dateModified: number } +>(items: T[]): WithDateEdited { let latestDateEdited = 0; - - db.relations - ?.to({ id: noteId, type: "note" }, "notebook") - ?.resolved() - .forEach((notebook) => { - references.push({ - type: "notebook", - url: `/notebooks/${notebook.id}`, - title: notebook.title - }); - - if (latestDateEdited < notebook.dateEdited) - latestDateEdited = notebook.dateEdited; - }); - - notebooks?.forEach((curr) => { - const topicId = (curr as NotebookReference).topics[0]; - const notebook = db.notebooks.notebook(curr.id)?.data; - if (!notebook) return; - - const topic = notebook.topics.find((t) => t.id === topicId); - if (!topic) return; - - references.push({ - url: `/notebooks/${curr.id}/${topicId}`, - title: topic.title, - type: "topic" - }); - if (latestDateEdited < (topic.dateEdited as number)) - latestDateEdited = topic.dateEdited as number; + items.forEach((item) => { + const date = "dateEdited" in item ? item.dateEdited : item.dateModified; + if (latestDateEdited < date) latestDateEdited = date; }); - - return { dateEdited: latestDateEdited, references: references.slice(0, 3) }; -} - -function getReminder(noteId: string) { - return db.relations - ?.from({ id: noteId, type: "note" }, "reminder") - .resolved(1)[0]; + return { dateEdited: latestDateEdited, items }; } function getDate(item: Item, groupType?: GroupingKey): number { - return getSortValue( - groupType - ? db.settings.getGroupOptions(groupType) - : { - groupBy: "default", - sortBy: "dateEdited", - sortDirection: "desc" - }, - item + return ( + getSortValue( + groupType + ? db.settings.getGroupOptions(groupType) + : { + groupBy: "default", + sortBy: "dateEdited", + sortDirection: "desc" + }, + item + ) || 0 + ); +} + +async function resolveItems(ids: string[], items: Record) { + const { type } = items[ids[0]]; + if (type === "note") return resolveNotes(ids); + else if (type === "notebook") { + const data: Record = {}; + for (const id of ids) data[id] = await db.notebooks.totalNotes(id); + return data; + } else if (type === "tag") { + const data: Record = {}; + for (const id of ids) + data[id] = await db.relations.from({ id, type: "tag" }, "note").count(); + return data; + } + return {}; +} + +type NoteResolvedData = { + notebooks?: NotebooksWithDateEdited; + reminder?: ReminderItem; + color?: Color; + tags?: TagsWithDateEdited; +}; +async function resolveNotes(ids: string[]) { + console.time("relations"); + const relations = [ + ...(await db.relations + .to({ type: "note", ids }, ["notebook", "tag", "color"]) + .get()), + ...(await db.relations.from({ type: "note", ids }, "reminder").get()) + ]; + console.timeEnd("relations"); + + const relationIds: { + notebooks: Set; + colors: Set; + tags: Set; + reminders: Set; + } = { + colors: new Set(), + notebooks: new Set(), + tags: new Set(), + reminders: new Set() + }; + + const grouped: Record< + string, + { + notebooks: string[]; + color?: string; + tags: string[]; + reminder?: string; + } + > = {}; + for (const relation of relations) { + const noteId = + relation.toType === "relation" ? relation.fromId : relation.toId; + const data = grouped[noteId] || { + notebooks: [], + tags: [] + }; + + if (relation.toType === "relation" && !data.reminder) { + data.reminder = relation.fromId; + relationIds.reminders.add(relation.fromId); + } else if (relation.fromType === "notebook" && data.notebooks.length < 2) { + data.notebooks.push(relation.fromId); + relationIds.notebooks.add(relation.fromId); + } else if (relation.fromType === "tag" && data.tags.length < 3) { + data.tags.push(relation.fromId); + relationIds.tags.add(relation.fromId); + } else if (relation.fromType === "color" && !data.color) { + data.color = relation.fromId; + relationIds.colors.add(relation.fromId); + } + grouped[relation.toId] = data; + } + + console.time("resolve"); + const resolved = { + notebooks: await db.notebooks.all.records( + Array.from(relationIds.notebooks) + ), + tags: await db.tags.all.records(Array.from(relationIds.tags)), + colors: await db.colors.all.records(Array.from(relationIds.colors)), + reminders: await db.reminders.all.records(Array.from(relationIds.reminders)) + }; + console.timeEnd("resolve"); + + const data: Record = {}; + for (const noteId in grouped) { + const group = grouped[noteId]; + data[noteId] = { + color: group.color ? resolved.colors[group.color] : undefined, + reminder: group.reminder ? resolved.reminders[group.reminder] : undefined, + tags: withDateEdited(group.tags.map((id) => resolved.tags[id])), + notebooks: withDateEdited( + group.notebooks.map((id) => resolved.notebooks[id]) + ) + }; + } + return data; +} + +function isNoteResolvedData(data: unknown): data is NoteResolvedData { + return ( + typeof data === "object" && + !!data && + "notebooks" in data && + "reminder" in data && + "color" in data && + "tags" in data ); } diff --git a/apps/web/src/components/list-container/types.ts b/apps/web/src/components/list-container/types.ts index a04ea07cb..2a5a813f2 100644 --- a/apps/web/src/components/list-container/types.ts +++ b/apps/web/src/components/list-container/types.ts @@ -17,21 +17,17 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { Note } from "@notesnook/core/dist/types"; +import { Notebook, Tag } from "@notesnook/core"; -export type Context = { - type: string; - notes?: Note[]; - value?: { topic?: string }; -} & Record; +export type Context = + | { + type: "notebook" | "tag" | "color"; + id: string; + } + | { + type: "favorite" | "monographs"; + }; -export type Reference = { - type: "topic" | "notebook"; - url: string; - title: string; -}; - -export type ReferencesWithDateEdited = { - dateEdited: number; - references: Reference[]; -}; +export type WithDateEdited = { items: T[]; dateEdited: number }; +export type NotebooksWithDateEdited = WithDateEdited; +export type TagsWithDateEdited = WithDateEdited; diff --git a/apps/web/src/components/list-item/index.tsx b/apps/web/src/components/list-item/index.tsx index 7993d6ec2..6243e9adb 100644 --- a/apps/web/src/components/list-item/index.tsx +++ b/apps/web/src/components/list-item/index.tsx @@ -29,7 +29,7 @@ import { MenuItem } from "@notesnook/ui"; import { alpha } from "@theme-ui/color"; import { Item } from "@notesnook/core/dist/types"; -type ListItemProps = { +type ListItemProps = { colors?: { heading: SchemeColors; accent: SchemeColors; @@ -39,7 +39,7 @@ type ListItemProps = { isCompact?: boolean; isDisabled?: boolean; isSimple?: boolean; - item: Item; + item: TItem; onKeyPress?: (e: React.KeyboardEvent) => void; onClick?: () => void; @@ -48,10 +48,13 @@ type ListItemProps = { body?: JSX.Element | string; footer?: JSX.Element; - menuItems?: (item: any, items?: any[]) => MenuItem[]; + context?: TContext; + menuItems?: (item: TItem, items?: string[], context?: TContext) => MenuItem[]; }; -function ListItem(props: ListItemProps) { +function ListItem( + props: ListItemProps +) { const { colors: { heading, background, accent } = { heading: "heading", @@ -71,7 +74,7 @@ function ListItem(props: ListItemProps) { const isSelected = useSelectionStore((store) => { const isInSelection = - store.selectedItems.findIndex((item) => item.id === props.item.id) > -1; + store.selectedItems.findIndex((item) => item === props.item.id) > -1; return isFocused ? store.selectedItems.length > 1 && isInSelection : isInSelection; @@ -88,11 +91,9 @@ function ListItem(props: ListItemProps) { e.stopPropagation(); let title = undefined; - let selectedItems = selectionStore - .get() - .selectedItems.filter((i) => i.type === item.type); + let selectedItems = selectionStore.get().selectedItems; // .filter((i) => i.type === item.type); - if (selectedItems.findIndex((i) => i.id === item.id) === -1) { + if (selectedItems.findIndex((i) => i === item.id) === -1) { selectedItems = []; selectedItems.push(item); } diff --git a/apps/web/src/components/navigation-menu/index.tsx b/apps/web/src/components/navigation-menu/index.tsx index ab83723dd..fc8b49809 100644 --- a/apps/web/src/components/navigation-menu/index.tsx +++ b/apps/web/src/components/navigation-menu/index.tsx @@ -58,14 +58,9 @@ type Route = { }; const navigationHistory = new Map(); -function shouldSelectNavItem( - route: string, - pin: { type: string; id: string; notebookId: string } -) { +function shouldSelectNavItem(route: string, pin: { type: string; id: string }) { if (pin.type === "notebook") { return route === `/notebooks/${pin.id}`; - } else if (pin.type === "topic") { - return route === `/notebooks/${pin.notebookId}/${pin.id}`; } else if (pin.type === "tag") { return route === `/tags/${pin.id}`; } @@ -261,8 +256,6 @@ function NavigationMenu(props: NavigationMenuProps) { onClick={() => { if (item.type === "notebook") { _navigate(`/notebooks/${item.id}`); - } else if (item.type === "topic") { - _navigate(`/notebooks/${item.notebookId}/${item.id}`); } else if (item.type === "tag") { _navigate(`/tags/${item.id}`); } diff --git a/apps/web/src/components/note/index.tsx b/apps/web/src/components/note/index.tsx index 927c4b790..d725aaf3d 100644 --- a/apps/web/src/components/note/index.tsx +++ b/apps/web/src/components/note/index.tsx @@ -17,10 +17,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import React, { useMemo } from "react"; +import React from "react"; import { Button, Flex, Text } from "@theme-ui/components"; import { - Topic, Notebook, Reminder, Alert, @@ -49,7 +48,7 @@ import { AddToNotebook, RemoveShortcutLink, Plus, - Copy + Copy, Tag as TagIcon } from "../icons"; import TimeAgo from "../time-ago"; @@ -64,9 +63,7 @@ import { store, useStore } from "../../stores/note-store"; import { store as userstore } from "../../stores/user-store"; import { store as editorStore } from "../../stores/editor-store"; import { store as tagStore } from "../../stores/tag-store"; -import { useStore as useAttachmentStore } from "../../stores/attachment-store"; import { db } from "../../common/db"; -import { showUnpinnedToast } from "../../common/toasts"; import { showToast } from "../../utils/toast"; import { hashNavigate, navigate } from "../../navigation"; import { showPublishView } from "../publish-view"; @@ -79,25 +76,30 @@ import { isReminderActive, isReminderToday } from "@notesnook/core/dist/collections/reminders"; -import { getFormattedReminderTime } from "@notesnook/common"; -import { - Context, - ReferencesWithDateEdited -} from "../list-container/types"; +import { getFormattedReminderTime, pluralize } from "@notesnook/common"; +import { Context, ReferencesWithDateEdited } from "../list-container/types"; import { SchemeColors, StaticColors } from "@notesnook/theme"; import FileSaver from "file-saver"; import { Reminder as ReminderType, - Tag, Color, - Note -} from "@notesnook/core/dist/types"; + Note, + Notebook as NotebookItem, + Tag +} from "@notesnook/core"; import { MenuItem } from "@notesnook/ui"; +import { + Context, + NotebooksWithDateEdited, + TagsWithDateEdited +} from "../list-container/types"; +import { SchemeColors, StaticColors } from "@notesnook/theme"; +import Vault from "../../common/vault"; type NoteProps = { - tags: Tag[]; + tags?: TagsWithDateEdited; color?: Color; - references?: ReferencesWithDateEdited; + notebooks?: NotebooksWithDateEdited; item: Note; context?: Context; date: number; @@ -107,18 +109,22 @@ type NoteProps = { }; function Note(props: NoteProps) { - const { tags, color, references, item, date, reminder, simplified, compact } = + const { tags, color, notebooks, item, date, reminder, simplified, compact } = props; const note = item; const isOpened = useStore((store) => store.selectedNote === note.id); - const attachments = useAttachmentStore((store) => - store.attachments.filter((a) => a.noteIds.includes(note.id)) - ); - const failed = useMemo( - () => attachments.filter((a) => a.failed), - [attachments] - ); + const attachments = []; + + // useAttachmentStore((store) => + // store.attachments.filter((a) => a.noteIds.includes(note.id)) + // ); + const failed = []; + + // useMemo( + // () => attachments.filter((a) => a.failed), + // [attachments] + // ); const primary: SchemeColors = color ? color.colorCode : "accent-selected"; return ( @@ -142,6 +148,7 @@ function Note(props: NoteProps) { heading: color ? primary : "heading", background: "background" }} + context={{ color }} menuItems={menuItems} onClick={() => { if (note.conflicted) { @@ -156,14 +163,14 @@ function Note(props: NoteProps) { - {references?.references?.map((reference) => ( + {notebooks?.items.map((notebook) => ( { - navigate(reference.url); + navigate(`/notebooks/${notebook.id}`); }} - text={reference.title} - icon={reference.type === "topic" ? Topic : Notebook} + text={notebook.title} + icon={Notebook} /> ))} {reminder && isReminderActive(reminder) && ( @@ -236,7 +243,7 @@ function Note(props: NoteProps) { {note.favorite && } - {tags.map((tag) => { + {tags?.items.map((tag) => { return (