web: start initial migration to sqlite

This commit is contained in:
Abdullah Atta
2023-10-24 11:41:30 +05:00
parent 01e774bff5
commit 404f2072a1
57 changed files with 7484 additions and 1282 deletions

View File

@@ -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,

View File

@@ -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",

View File

@@ -83,10 +83,10 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
initStore();
initAttachments();
refreshNavItems();
initEditorStore();
(async function () {
await refreshNavItems();
await updateLastSynced();
if (await initUser()) {
showUpgradeReminderDialogs();

View File

@@ -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<SQLiteWorker>(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) {

View File

@@ -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);

View File

@@ -34,14 +34,14 @@ type Item = {
metadata?: Record<string, unknown>;
};
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[]) {

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
// 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<number>}
*/
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<number>}
*/
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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
// 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<number>} [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<number, OpenedFileEntry>} */ #mapIdToFile = new Map();
/** @type {IDBContext} */ #idb;
/** @type {Set<string>} */ #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<number>}
*/
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);
});
});
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
// 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<IDBDatabase>} */ #dbReady;
#txOptions;
/** @type {IDBTransaction} */ #tx = null;
#txTimestamp = 0;
#runChain = Promise.resolve();
#putChain = Promise.resolve();
/**
* @param {IDBDatabase|Promise<IDBDatabase>} 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.<string, ObjectStore>) => 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.<string, ObjectStore>) => 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<IDBValidKey>}
*/
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<IDBValidKey[]>}
*/
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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
// 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<number>} 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);

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
// 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<string, (value: any) => 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<boolean>}
*/
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<?Lock>}
*/
_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);
}
}

85
apps/web/src/common/sqlite/globals.d.ts vendored Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
/* eslint-disable no-var */
declare namespace Asyncify {
function handleAsync(f: () => Promise<any>);
}
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<any>,
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;

1726
apps/web/src/common/sqlite/index.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<SQLiteWorker>(new Worker()) as SQLiteWorker;
}
async init(): Promise<void> {
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<DatabaseConnection> {
// 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<void> {
await connection.executeQuery(CompiledQuery.raw("begin"));
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("commit"));
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("rollback"));
}
async releaseConnection(): Promise<void> {
this.connectionMutex.unlock();
}
async destroy(): Promise<void> {
if (!this.worker) {
return;
}
return await this.worker.close();
}
}
class ConnectionMutex {
private promise?: Promise<void>;
private resolve?: () => void;
async lock(): Promise<void> {
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<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error("wasqlite driver doesn't support streaming");
}
async executeQuery<R>(
compiledQuery: CompiledQuery<unknown>
): Promise<QueryResult<R>> {
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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
// 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<string, PreparedStatement> = 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<string, SQLiteCompatibleType>[] = [];
while ((await sqlite.step(prepared.stmt)) === SQLITE_ROW) {
const row = sqlite.row(prepared.stmt);
const acc: Record<string, SQLiteCompatibleType> = {};
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<R>(
mode: RunMode,
sql: string,
parameters?: SQLiteCompatibleType[]
): Promise<QueryResult<R>> {
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);

View File

@@ -17,28 +17,43 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<string>((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<string>((resolve, reject) => {
gunzip(fromBase64(data), (err, data) =>
err ? reject(err) : resolve(new TextDecoder().decode(data))
);
});
}
export type Promisable<T> = T | Promise<T>;
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<any> | null;
init: null;
close: null;
};
export type EventWithError = {
[K in keyof Events]: {
data: Events[K];
err: unknown;
};
};
expose(module);
export type Compressor = typeof module;

View File

@@ -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<a.length;)a.shift()(f)};function I(a,b="i8"){b.endsWith("*")&&(b="*");switch(b){case "i1":return w[a>>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(16<c-b&&a.buffer&&Ja)return Ja.decode(a.subarray(b,c));for(d="";b<c;){var e=a[b++];if(e&128){var h=a[b++]&63;if(192==(e&224))d+=String.fromCharCode((e&31)<<6|h);else{var g=a[b++]&63;e=224==(e&240)?(e&15)<<12|h<<6|g:(e&7)<<18|h<<12|g<<6|a[b++]&63;65536>e?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<a.length;++c){var d=a.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=d?(b+=4,++c):b+=3}return b},Sa=(a,b,c,d)=>{if(!(0<d))return 0;var e=c;d=c+d-1;for(var h=0;h<a.length;++h){var g=a.charCodeAt(h);if(55296<=g&&57343>=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<d;h++){try{var g=a.Sb.bc.xc(a.Sb)}catch(n){throw new N(29);}if(void 0===g&&0===e)throw new N(6);if(null===g||void 0===g)break;e++;b[c+h]=g}e&&(a.node.timestamp=Date.now());return e},write(a,b,c,d){if(!a.Sb||!a.Sb.bc.rc)throw new N(60);try{for(var e=0;e<d;e++)a.Sb.bc.rc(a.Sb,b[c+e])}catch(h){throw new N(29);
}d&&(a.node.timestamp=Date.now());return e}},Xa={xc(){a:{if(!Qa.length){var a=null;"undefined"!=typeof window&&"function"==typeof window.prompt?(a=window.prompt("Input: "),null!==a&&(a+="\n")):"function"==typeof readline&&(a=readline(),null!==a&&(a+="\n"));if(!a){var b=null;break a}b=Array(Ra(a)+1);a=Sa(a,b,0,b.length);b.length=a;Qa=b}b=Qa.shift()}return b},rc(a,b){null===b||10===b?(ka(K(a.Rb,0)),a.Rb=[]):0!=b&&a.Rb.push(b)},ic(a){a.Rb&&0<a.Rb.length&&(ka(K(a.Rb,0)),a.Rb=[])},Yc(){return{Uc:25856,
Wc:5,Tc:191,Vc:35387,Sc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},Zc(){return 0},$c(){return[24,80]}},Ya={rc(a,b){null===b||10===b?(t(K(a.Rb,0)),a.Rb=[]):0!=b&&a.Rb.push(b)},ic(a){a.Rb&&0<a.Rb.length&&(t(K(a.Rb,0)),a.Rb=[])}};function Za(a,b){var c=a.Nb?a.Nb.length:0;c>=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.Pb&&a.Nb.set(c.subarray(0,a.Pb),0))}
var O={Vb:null,Ub(){return O.createNode(null,"/",16895,0)},createNode(a,b,c,d){if(24576===(c&61440)||4096===(c&61440))throw new N(63);O.Vb||(O.Vb={dir:{node:{Tb:O.Cb.Tb,Qb:O.Cb.Qb,cc:O.Cb.cc,jc:O.Cb.jc,Bc:O.Cb.Bc,oc:O.Cb.oc,mc:O.Cb.mc,Ac:O.Cb.Ac,nc:O.Cb.nc},stream:{Zb:O.Mb.Zb}},file:{node:{Tb:O.Cb.Tb,Qb:O.Cb.Qb},stream:{Zb:O.Mb.Zb,read:O.Mb.read,write:O.Mb.write,uc:O.Mb.uc,kc:O.Mb.kc,lc:O.Mb.lc}},link:{node:{Tb:O.Cb.Tb,Qb:O.Cb.Qb,fc:O.Cb.fc},stream:{}},vc:{node:{Tb:O.Cb.Tb,Qb:O.Cb.Qb},stream:$a}});
c=ab(a,b,c,d);P(c.mode)?(c.Cb=O.Vb.dir.node,c.Mb=O.Vb.dir.stream,c.Nb={}):32768===(c.mode&61440)?(c.Cb=O.Vb.file.node,c.Mb=O.Vb.file.stream,c.Pb=0,c.Nb=null):40960===(c.mode&61440)?(c.Cb=O.Vb.link.node,c.Mb=O.Vb.link.stream):8192===(c.mode&61440)&&(c.Cb=O.Vb.vc.node,c.Mb=O.Vb.vc.stream);c.timestamp=Date.now();a&&(a.Nb[b]=c,a.timestamp=c.timestamp);return c},Xc(a){return a.Nb?a.Nb.subarray?a.Nb.subarray(0,a.Pb):new Uint8Array(a.Nb):new Uint8Array(0)},Cb:{Tb(a){var b={};b.Hc=8192===(a.mode&61440)?a.id:
1;b.yc=a.id;b.mode=a.mode;b.Nc=1;b.uid=0;b.Kc=0;b.ec=a.ec;P(a.mode)?b.size=4096:32768===(a.mode&61440)?b.size=a.Pb:40960===(a.mode&61440)?b.size=a.link.length:b.size=0;b.Dc=new Date(a.timestamp);b.Mc=new Date(a.timestamp);b.Gc=new Date(a.timestamp);b.Ec=4096;b.Fc=Math.ceil(b.size/b.Ec);return b},Qb(a,b){void 0!==b.mode&&(a.mode=b.mode);void 0!==b.timestamp&&(a.timestamp=b.timestamp);if(void 0!==b.size&&(b=b.size,a.Pb!=b))if(0==b)a.Nb=null,a.Pb=0;else{var c=a.Nb;a.Nb=new Uint8Array(b);c&&a.Nb.set(c.subarray(0,
Math.min(b,a.Pb)));a.Pb=b}},cc(){throw bb[44];},jc(a,b,c,d){return O.createNode(a,b,c,d)},Bc(a,b,c){if(P(a.mode)){try{var d=cb(b,c)}catch(h){}if(d)for(var e in d.Nb)throw new N(55);}delete a.parent.Nb[a.name];a.parent.timestamp=Date.now();a.name=c;b.Nb[c]=a;b.timestamp=a.parent.timestamp;a.parent=b},oc(a,b){delete a.Nb[b];a.timestamp=Date.now()},mc(a,b){var c=cb(a,b),d;for(d in c.Nb)throw new N(55);delete a.Nb[b];a.timestamp=Date.now()},Ac(a){var b=[".",".."],c;for(c in a.Nb)a.Nb.hasOwnProperty(c)&&
b.push(c);return b},nc(a,b,c){a=O.createNode(a,b,41471,0);a.link=c;return a},fc(a){if(40960!==(a.mode&61440))throw new N(28);return a.link}},Mb:{read(a,b,c,d,e){var h=a.node.Nb;if(e>=a.node.Pb)return 0;a=Math.min(a.node.Pb-e,d);if(8<a&&h.subarray)b.set(h.subarray(e,e+a),c);else for(d=0;d<a;d++)b[c+d]=h[e+d];return a},write(a,b,c,d,e,h){b.buffer===w.buffer&&(h=!1);if(!d)return 0;a=a.node;a.timestamp=Date.now();if(b.subarray&&(!a.Nb||a.Nb.subarray)){if(h)return a.Nb=b.subarray(c,c+d),a.Pb=d;if(0===
a.Pb&&0===e)return a.Nb=b.slice(c,c+d),a.Pb=d;if(e+d<=a.Pb)return a.Nb.set(b.subarray(c,c+d),e),d}Za(a,e+d);if(a.Nb.subarray&&b.subarray)a.Nb.set(b.subarray(c,c+d),e);else for(h=0;h<d;h++)a.Nb[e+h]=b[c+h];a.Pb=Math.max(a.Pb,e+d);return d},Zb(a,b,c){1===c?b+=a.position:2===c&&32768===(a.node.mode&61440)&&(b+=a.node.Pb);if(0>b)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<c||c+b<a.length)a.subarray?a=a.subarray(c,c+b):a=Array.prototype.slice.call(a,c,c+b);c=!0;b=65536*Math.ceil(b/65536);(e=db(65536,b))?(y.fill(0,e,e+b),b=e):b=0;if(!b)throw new N(48);w.set(a,b)}else c=!1,b=a.byteOffset;return{Oc:b,Cc:c}},lc(a,b,c,d){O.Mb.write(a,b,0,d,c,!1);return 0}}},eb=(a,b)=>{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<b.sc)throw new N(32);a=a.split("/").filter(g=>!!g);for(var c=fb,d="/",e=0;e<a.length;e++){var h=e===a.length-1;if(h&&b.parent)break;c=cb(c,a[e]);d=M(d+"/"+a[e]);c.$b&&(!h||h&&b.wc)&&(c=c.$b.root);if(!h||b.Yb)for(h=0;40960===(c.mode&61440);)if(c=kb(d),d=Pa(La(d),c),c=R(d,{sc:b.sc+1}).node,40<h++)throw new N(32);}return{path:d,node:c}}
function lb(a){for(var b;;){if(a===a.parent)return a=a.Ub.zc,b?"/"!==a[a.length-1]?`${a}/${b}`:a+b:a;b=b?`${a.name}/${b}`:a.name;a=a.parent}}function mb(a,b){for(var c=0,d=0;d<b.length;d++)c=(c<<5)-c+b.charCodeAt(d)|0;return(a+c>>>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="<generic error, no 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<k;r++){try{var m=b()}catch(q){throw new N(29);}if(void 0===m&&0===l)throw new N(6);if(null===m||void 0===m)break;l++;g[n+r]=m}l&&(h.node.timestamp=Date.now());return l},write(h,g,n,k){for(var l=0;l<k;l++)try{c(g[n+l])}catch(r){throw new N(29);}k&&(h.node.timestamp=Date.now());return l}});yb(a,d,e)}var Lb,U={},vb,Gb;
function Mb(a,b,c){if("/"===b.charAt(0))return b;a=-100===a?"/":S(a).path;if(0==b.length){if(!c)throw new N(44);return a}return M(a+"/"+b)}
function Nb(a,b,c){try{var d=a(b)}catch(h){if(h&&h.node&&M(b)!==M(lb(h.node)))return-54;throw h;}z[c>>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<D?+Math.floor(D/4294967296)>>>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<D?+Math.floor(D/
4294967296)>>>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<D?+Math.floor(D/4294967296)>>>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<D?+Math.floor(D/4294967296)>>>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<D?+Math.floor(D/4294967296)>>>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<wa)){if(f.onExit)f.onExit(a);v=!0}ea(a,new Ha(a))},Lc=a=>{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<wa))try{na=
d=na,Kc(d)}catch(e){Lc(e)}}catch(e){Lc(e)}})):u(`invalid state: ${X}`);return Qc}}function cd(a){return Yc(b=>{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;l<d.length;l++){var r=g[c[l]];r?(0===k&&(k=gd()),n[l]=r(d[l])):n[l]=d[l]}c=V;d=a.apply(null,n);e=e&&e.async;wa+=1;if(V!=c)return Wc().then(h);d=h(d);return e?Promise.resolve(d):d};
function pb(a,b,c,d){a||(a=this);this.parent=a;this.Ub=a.Ub;this.$b=null;this.id=ib++;this.name=b;this.mode=c;this.Cb={};this.Mb={};this.ec=d}Object.defineProperties(pb.prototype,{read:{get:function(){return 365===(this.mode&365)},set:function(a){a?this.mode|=365:this.mode&=-366}},write:{get:function(){return 146===(this.mode&146)},set:function(a){a?this.mode|=146:this.mode&=-147}}});Ib();Q=Array(4096);wb(O,"/");T("/tmp");T("/home");T("/home/web_user");
(function(){T("/dev");Va(259,{read:()=>0,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<q.nConstraint;++G){var E=q.aConstraint,L=E.push,H=x+G*A,ha=n.get("sqlite3_index_constraint").dc,W={};W.iColumn=I(H+ha[0],"i32");W.op=I(H+ha[1],"i8");W.usable=!!I(H+ha[2],"i8");L.call(E,W)}q.nOrderBy=I(l+m[2],"i32");q.aOrderBy=
[];x=I(l+m[3],"*");A=n.get("sqlite3_index_orderby").size;for(G=0;G<q.nOrderBy;++G)E=q.aOrderBy,L=E.push,H=x+G*A,ha=n.get("sqlite3_index_orderby").dc,W={},W.iColumn=I(H+ha[0],"i32"),W.desc=!!I(H+ha[1],"i8"),L.call(E,W);q.aConstraintUsage=[];for(x=0;x<q.nConstraint;++x)q.aConstraintUsage.push({argvIndex:0,omit:!1});q.idxNum=I(l+m[5],"i32");q.idxStr=null;q.orderByConsumed=!!I(l+m[8],"i8");q.estimatedCost=I(l+m[9],"double");q.estimatedRows=I(l+m[10],"i32");q.idxFlags=I(l+m[11],"i32");q.colUsed=I(l+m[12],
"i32");k=r.module.xBestIndex(k,q);r=n.get("sqlite3_index_info").dc;m=I(l+r[4],"*");x=n.get("sqlite3_index_constraint_usage").size;for(L=0;L<q.nConstraint;++L)A=m+L*x,E=q.aConstraintUsage[L],H=n.get("sqlite3_index_constraint_usage").dc,J(A+H[0],E.argvIndex,"i32"),J(A+H[1],E.omit?1:0,"i8");J(l+r[5],q.idxNum,"i32");"string"===typeof q.idxStr&&(m=Ra(q.idxStr),x=Z("sqlite3_malloc","number",["number"],[m+1]),Sa(q.idxStr,y,x,m+1),J(l+r[6],x,"*"),J(l+r[7],1,"i32"));J(l+r[8],q.orderByConsumed,"i32");J(l+r[9],
q.estimatedCost,"double");J(l+r[10],q.estimatedRows,"i32");J(l+r[11],q.idxFlags,"i32");return k};kc=function(k){const l=d.get(k);b?h.add(k):d.delete(k);return l.module.xDisconnect(k)};jc=function(k){const l=d.get(k);b?h.add(k):d.delete(k);return l.module.xDestroy(k)};oc=function(k,l){const r=d.get(k);e.set(l,r);if(b){g.delete(l);for(const m of g)e.delete(m)}return r.module.xOpen(k,l)};ec=function(k){const l=e.get(k);b?g.add(k):e.delete(k);return l.module.xClose(k)};lc=function(k){return e.get(k).module.xEof(k)?
1:0};mc=function(k,l,r,m,q){const x=e.get(k);r=r?r?K(y,r):"":null;q=new Uint32Array(y.buffer,q,m);return x.module.xFilter(k,l,r,q)};nc=function(k){return e.get(k).module.xNext(k)};fc=function(k,l,r){return e.get(k).module.xColumn(k,l,r)};rc=function(k,l){return e.get(k).module.xRowid(k,a("BigInt64",l))};uc=function(k,l,r,m){const q=d.get(k);r=new Uint32Array(y.buffer,r,l);return q.module.xUpdate(k,r,a("BigInt64",m))};cc=function(k){return d.get(k).module.xBegin(k)};tc=function(k){return d.get(k).module.xSync(k)};
gc=function(k){return d.get(k).module.xCommit(k)};qc=function(k){return d.get(k).module.xRollback(k)};pc=function(k,l){const r=d.get(k);l=l?K(y,l):"";return r.module.xRename(k,l)}})();
(function(){function a(g,n){const k=`get${g}`,l=`set${g}`;return new Proxy(new DataView(y.buffer,n,"Int32"===g?4:8),{get(r,m){if(m===k)return function(q,x){if(!x)throw Error("must be little endian");return r[m](q,x)};if(m===l)return function(q,x,A){if(!A)throw Error("must be little endian");return r[m](q,x,A)};if("string"===typeof m&&m.match(/^(get)|(set)/))throw Error("invalid type");return r[m]}})}function b(g){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<c)return-68;Sa("/",y,a,b);return c}catch(d){if("undefined"==typeof U||"ErrnoError"!==d.name)throw d;return-d.Ob}},F:function(a,b){try{return a=a?K(y,a):"",Nb(Cb,a,b)}catch(c){if("undefined"==typeof U||"ErrnoError"!==c.name)throw c;return-c.Ob}},z:function(a,b,c){try{return b=b?K(y,b):"",b=Mb(a,b),b=M(b),"/"===b[b.length-1]&&(b=b.substr(0,
b.length-1)),T(b,c),0}catch(d){if("undefined"==typeof U||"ErrnoError"!==d.name)throw d;return-d.Ob}},E:function(a,b,c,d){try{b=b?K(y,b):"";var e=d&256;b=Mb(a,b,d&4096);return Nb(e?Cb:Bb,b,c)}catch(h){if("undefined"==typeof U||"ErrnoError"!==h.name)throw h;return-h.Ob}},y:function(a,b,c,d){Ob=d;try{b=b?K(y,b):"";b=Mb(a,b);var e=d?Pb():0;return Fb(b,c,e).Wb}catch(h){if("undefined"==typeof U||"ErrnoError"!==h.name)throw h;return-h.Ob}},w:function(a,b,c,d){try{b=b?K(y,b):"";b=Mb(a,b);if(0>=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<e?(B[c>>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<a)return!1;for(var c=1;4>=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<d.length;++h)w[e++>>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<D?+Math.floor(D/4294967296)>>>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<D?+Math.floor(D/4294967296)>>>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<c;g++){var n=B[a>>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<k)break;"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}},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<D?+Math.floor(D/4294967296)>>>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<c;g++){var n=B[a>>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<C)){if(f.preRun)for("function"==typeof f.preRun&&(f.preRun=[f.preRun]);f.preRun.length;)xa();Ia(sa);0<C||(f.setStatus?(f.setStatus("Running..."),setTimeout(function(){setTimeout(function(){f.setStatus("")},1);a()},1)):a())}}if(f.preInit)for("function"==typeof f.preInit&&(f.preInit=[f.preInit]);0<f.preInit.length;)f.preInit.pop()();
var pd=!0;f.noInitialRun&&(pd=!1);od();
return moduleArg.ready
}
);
})();
export default Module;

Binary file not shown.

View File

@@ -0,0 +1,110 @@
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),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<a.length;)a.shift()(f)};function I(a,b="i8"){b.endsWith("*")&&(b="*");switch(b){case "i1":return v[a>>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(16<c-b&&a.buffer&&Ha)return Ha.decode(a.subarray(b,c));for(d="";b<c;){var e=a[b++];if(e&128){var h=a[b++]&63;if(192==(e&224))d+=String.fromCharCode((e&31)<<6|h);else{var g=a[b++]&63;e=224==(e&240)?(e&15)<<12|h<<6|g:(e&7)<<18|h<<12|g<<6|a[b++]&63;65536>e?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<a.length;++c){var d=a.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=d?(b+=4,++c):b+=3}return b},O=(a,b,c,d)=>{if(!(0<d))return 0;var e=c;d=c+d-1;for(var h=0;h<a.length;++h){var g=a.charCodeAt(h);if(55296<=g&&57343>=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<d;h++){try{var g=a.Ob.Zb.sc(a.Ob)}catch(m){throw new P(29);}if(void 0===g&&0===e)throw new P(6);if(null===g||void 0===g)break;e++;b[c+h]=g}e&&(a.node.timestamp=Date.now());return e},write(a,b,c,d){if(!a.Ob||!a.Ob.Zb.mc)throw new P(60);try{for(var e=0;e<d;e++)a.Ob.Zb.mc(a.Ob,b[c+e])}catch(h){throw new P(29);
}d&&(a.node.timestamp=Date.now());return e}},Ta={sc(){a:{if(!Oa.length){var a=null;"undefined"!=typeof window&&"function"==typeof window.prompt?(a=window.prompt("Input: "),null!==a&&(a+="\n")):"function"==typeof readline&&(a=readline(),null!==a&&(a+="\n"));if(!a){var b=null;break a}b=Array(N(a)+1);a=O(a,b,0,b.length);b.length=a;Oa=b}b=Oa.shift()}return b},mc(a,b){null===b||10===b?(ka(K(a.Nb,0)),a.Nb=[]):0!=b&&a.Nb.push(b)},Wb(a){a.Nb&&0<a.Nb.length&&(ka(K(a.Nb,0)),a.Nb=[])},Sc(){return{Oc:25856,Qc:5,
Nc:191,Pc:35387,Mc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},Tc(){return 0},Uc(){return[24,80]}},Ua={mc(a,b){null===b||10===b?(t(K(a.Nb,0)),a.Nb=[]):0!=b&&a.Nb.push(b)},Wb(a){a.Nb&&0<a.Nb.length&&(t(K(a.Nb,0)),a.Nb=[])}};function Va(a,b){var c=a.Jb?a.Jb.length:0;c>=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.Lb&&a.Jb.set(c.subarray(0,a.Lb),0))}
var Q={Qb:null,Rb(){return Q.createNode(null,"/",16895,0)},createNode(a,b,c,d){if(24576===(c&61440)||4096===(c&61440))throw new P(63);Q.Qb||(Q.Qb={dir:{node:{Pb:Q.Cb.Pb,Mb:Q.Cb.Mb,$b:Q.Cb.$b,ec:Q.Cb.ec,wc:Q.Cb.wc,kc:Q.Cb.kc,ic:Q.Cb.ic,vc:Q.Cb.vc,jc:Q.Cb.jc},stream:{Vb:Q.Ib.Vb}},file:{node:{Pb:Q.Cb.Pb,Mb:Q.Cb.Mb},stream:{Vb:Q.Ib.Vb,read:Q.Ib.read,write:Q.Ib.write,pc:Q.Ib.pc,fc:Q.Ib.fc,hc:Q.Ib.hc}},link:{node:{Pb:Q.Cb.Pb,Mb:Q.Cb.Mb,cc:Q.Cb.cc},stream:{}},qc:{node:{Pb:Q.Cb.Pb,Mb:Q.Cb.Mb},stream:Wa}});
c=Xa(a,b,c,d);R(c.mode)?(c.Cb=Q.Qb.dir.node,c.Ib=Q.Qb.dir.stream,c.Jb={}):32768===(c.mode&61440)?(c.Cb=Q.Qb.file.node,c.Ib=Q.Qb.file.stream,c.Lb=0,c.Jb=null):40960===(c.mode&61440)?(c.Cb=Q.Qb.link.node,c.Ib=Q.Qb.link.stream):8192===(c.mode&61440)&&(c.Cb=Q.Qb.qc.node,c.Ib=Q.Qb.qc.stream);c.timestamp=Date.now();a&&(a.Jb[b]=c,a.timestamp=c.timestamp);return c},Rc(a){return a.Jb?a.Jb.subarray?a.Jb.subarray(0,a.Lb):new Uint8Array(a.Jb):new Uint8Array(0)},Cb:{Pb(a){var b={};b.Cc=8192===(a.mode&61440)?a.id:
1;b.tc=a.id;b.mode=a.mode;b.Ic=1;b.uid=0;b.Ec=0;b.bc=a.bc;R(a.mode)?b.size=4096:32768===(a.mode&61440)?b.size=a.Lb:40960===(a.mode&61440)?b.size=a.link.length:b.size=0;b.yc=new Date(a.timestamp);b.Hc=new Date(a.timestamp);b.Bc=new Date(a.timestamp);b.zc=4096;b.Ac=Math.ceil(b.size/b.zc);return b},Mb(a,b){void 0!==b.mode&&(a.mode=b.mode);void 0!==b.timestamp&&(a.timestamp=b.timestamp);if(void 0!==b.size&&(b=b.size,a.Lb!=b))if(0==b)a.Jb=null,a.Lb=0;else{var c=a.Jb;a.Jb=new Uint8Array(b);c&&a.Jb.set(c.subarray(0,
Math.min(b,a.Lb)));a.Lb=b}},$b(){throw Ya[44];},ec(a,b,c,d){return Q.createNode(a,b,c,d)},wc(a,b,c){if(R(a.mode)){try{var d=Za(b,c)}catch(h){}if(d)for(var e in d.Jb)throw new P(55);}delete a.parent.Jb[a.name];a.parent.timestamp=Date.now();a.name=c;b.Jb[c]=a;b.timestamp=a.parent.timestamp;a.parent=b},kc(a,b){delete a.Jb[b];a.timestamp=Date.now()},ic(a,b){var c=Za(a,b),d;for(d in c.Jb)throw new P(55);delete a.Jb[b];a.timestamp=Date.now()},vc(a){var b=[".",".."],c;for(c in a.Jb)a.Jb.hasOwnProperty(c)&&
b.push(c);return b},jc(a,b,c){a=Q.createNode(a,b,41471,0);a.link=c;return a},cc(a){if(40960!==(a.mode&61440))throw new P(28);return a.link}},Ib:{read(a,b,c,d,e){var h=a.node.Jb;if(e>=a.node.Lb)return 0;a=Math.min(a.node.Lb-e,d);if(8<a&&h.subarray)b.set(h.subarray(e,e+a),c);else for(d=0;d<a;d++)b[c+d]=h[e+d];return a},write(a,b,c,d,e,h){b.buffer===v.buffer&&(h=!1);if(!d)return 0;a=a.node;a.timestamp=Date.now();if(b.subarray&&(!a.Jb||a.Jb.subarray)){if(h)return a.Jb=b.subarray(c,c+d),a.Lb=d;if(0===
a.Lb&&0===e)return a.Jb=b.slice(c,c+d),a.Lb=d;if(e+d<=a.Lb)return a.Jb.set(b.subarray(c,c+d),e),d}Va(a,e+d);if(a.Jb.subarray&&b.subarray)a.Jb.set(b.subarray(c,c+d),e);else for(h=0;h<d;h++)a.Jb[e+h]=b[c+h];a.Lb=Math.max(a.Lb,e+d);return d},Vb(a,b,c){1===c?b+=a.position:2===c&&32768===(a.node.mode&61440)&&(b+=a.node.Lb);if(0>b)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<c||c+b<a.length)a.subarray?a=a.subarray(c,c+b):a=Array.prototype.slice.call(a,c,c+b);c=!0;b=65536*Math.ceil(b/65536);(e=$a(65536,b))?(w.fill(0,e,e+b),b=e):b=0;if(!b)throw new P(48);v.set(a,b)}else c=!1,b=a.byteOffset;return{Jc:b,xc:c}},hc(a,b,c,d){Q.Ib.write(a,b,0,d,c,!1);return 0}}},ab=(a,b)=>{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<b.nc)throw new P(32);a=a.split("/").filter(g=>!!g);for(var c=bb,d="/",e=0;e<a.length;e++){var h=e===a.length-1;if(h&&b.parent)break;c=Za(c,a[e]);d=M(d+"/"+a[e]);c.Xb&&(!h||h&&b.rc)&&(c=c.Xb.root);if(!h||b.Ub)for(h=0;40960===(c.mode&61440);)if(c=gb(d),d=Na(Ja(d),c),c=T(d,{nc:b.nc+1}).node,40<h++)throw new P(32);}return{path:d,node:c}}
function hb(a){for(var b;;){if(a===a.parent)return a=a.Rb.uc,b?"/"!==a[a.length-1]?`${a}/${b}`:a+b:a;b=b?`${a.name}/${b}`:a.name;a=a.parent}}function ib(a,b){for(var c=0,d=0;d<b.length;d++)c=(c<<5)-c+b.charCodeAt(d)|0;return(a+c>>>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="<generic error, no 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<l;p++){try{var n=b()}catch(r){throw new P(29);}if(void 0===n&&0===k)throw new P(6);if(null===n||void 0===n)break;k++;g[m+p]=n}k&&(h.node.timestamp=Date.now());return k},write(h,g,m,l){for(var k=0;k<l;k++)try{c(g[m+k])}catch(p){throw new P(29);}l&&(h.node.timestamp=Date.now());return k}});ub(a,d,e)}var Hb,X={},rb,Cb;
function Ib(a,b,c){if("/"===b.charAt(0))return b;a=-100===a?"/":V(a).path;if(0==b.length){if(!c)throw new P(44);return a}return M(a+"/"+b)}
function Jb(a,b,c){try{var d=a(b)}catch(h){if(h&&h.node&&M(b)!==M(hb(h.node)))return-54;throw h;}x[c>>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<D?+Math.floor(D/4294967296)>>>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<D?+Math.floor(D/
4294967296)>>>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<D?+Math.floor(D/4294967296)>>>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<D?+Math.floor(D/4294967296)>>>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<D?+Math.floor(D/4294967296)>>>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;m<d.length;m++){var l=e[c[m]];l?(0===g&&(g=Hc()),h[m]=l(d[m])):h[m]=d[m]}c=a.apply(null,h);return c=function(k){0!==g&&Ic(g);return"string"===b?k?K(w,k):"":"boolean"===b?!!k:k}(c)};
function lb(a,b,c,d){a||(a=this);this.parent=a;this.Rb=a.Rb;this.Xb=null;this.id=eb++;this.name=b;this.mode=c;this.Cb={};this.Ib={};this.bc=d}Object.defineProperties(lb.prototype,{read:{get:function(){return 365===(this.mode&365)},set:function(a){a?this.mode|=365:this.mode&=-366}},write:{get:function(){return 146===(this.mode&146)},set:function(a){a?this.mode|=146:this.mode&=-147}}});Eb();S=Array(4096);sb(Q,"/");W("/tmp");W("/home");W("/home/web_user");
(function(){W("/dev");Ra(259,{read:()=>0,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<r.nConstraint;++G){var E=r.aConstraint,L=E.push,H=y+G*A,da=m.get("sqlite3_index_constraint").ac,U={};U.iColumn=I(H+da[0],"i32");U.op=I(H+da[1],"i8");U.usable=!!I(H+da[2],"i8");L.call(E,U)}r.nOrderBy=I(k+n[2],"i32");r.aOrderBy=
[];y=I(k+n[3],"*");A=m.get("sqlite3_index_orderby").size;for(G=0;G<r.nOrderBy;++G)E=r.aOrderBy,L=E.push,H=y+G*A,da=m.get("sqlite3_index_orderby").ac,U={},U.iColumn=I(H+da[0],"i32"),U.desc=!!I(H+da[1],"i8"),L.call(E,U);r.aConstraintUsage=[];for(y=0;y<r.nConstraint;++y)r.aConstraintUsage.push({argvIndex:0,omit:!1});r.idxNum=I(k+n[5],"i32");r.idxStr=null;r.orderByConsumed=!!I(k+n[8],"i8");r.estimatedCost=I(k+n[9],"double");r.estimatedRows=I(k+n[10],"i32");r.idxFlags=I(k+n[11],"i32");r.colUsed=I(k+n[12],
"i32");l=p.module.xBestIndex(l,r);p=m.get("sqlite3_index_info").ac;n=I(k+p[4],"*");y=m.get("sqlite3_index_constraint_usage").size;for(L=0;L<r.nConstraint;++L)A=n+L*y,E=r.aConstraintUsage[L],H=m.get("sqlite3_index_constraint_usage").ac,J(A+H[0],E.argvIndex,"i32"),J(A+H[1],E.omit?1:0,"i8");J(k+p[5],r.idxNum,"i32");"string"===typeof r.idxStr&&(n=N(r.idxStr),y=Y("sqlite3_malloc","number",["number"],[n+1]),O(r.idxStr,w,y,n+1),J(k+p[6],y,"*"),J(k+p[7],1,"i32"));J(k+p[8],r.orderByConsumed,"i32");J(k+p[9],
r.estimatedCost,"double");J(k+p[10],r.estimatedRows,"i32");J(k+p[11],r.idxFlags,"i32");return l};gc=function(l){const k=d.get(l);b?h.add(l):d.delete(l);return k.module.xDisconnect(l)};fc=function(l){const k=d.get(l);b?h.add(l):d.delete(l);return k.module.xDestroy(l)};kc=function(l,k){const p=d.get(l);e.set(k,p);if(b){g.delete(k);for(const n of g)e.delete(n)}return p.module.xOpen(l,k)};ac=function(l){const k=e.get(l);b?g.add(l):e.delete(l);return k.module.xClose(l)};hc=function(l){return e.get(l).module.xEof(l)?
1:0};ic=function(l,k,p,n,r){const y=e.get(l);p=p?p?K(w,p):"":null;r=new Uint32Array(w.buffer,r,n);return y.module.xFilter(l,k,p,r)};jc=function(l){return e.get(l).module.xNext(l)};bc=function(l,k,p){return e.get(l).module.xColumn(l,k,p)};nc=function(l,k){return e.get(l).module.xRowid(l,a("BigInt64",k))};qc=function(l,k,p,n){const r=d.get(l);p=new Uint32Array(w.buffer,p,k);return r.module.xUpdate(l,p,a("BigInt64",n))};Zb=function(l){return d.get(l).module.xBegin(l)};pc=function(l){return d.get(l).module.xSync(l)};
cc=function(l){return d.get(l).module.xCommit(l)};mc=function(l){return d.get(l).module.xRollback(l)};lc=function(l,k){const p=d.get(l);k=k?K(w,k):"";return p.module.xRename(l,k)}})();
(function(){function a(g,m){const l=`get${g}`,k=`set${g}`;return new Proxy(new DataView(w.buffer,m,"Int32"===g?4:8),{get(p,n){if(n===l)return function(r,y){if(!y)throw Error("must be little endian");return p[n](r,y)};if(n===k)return function(r,y,A){if(!A)throw Error("must be little endian");return p[n](r,y,A)};if("string"===typeof n&&n.match(/^(get)|(set)/))throw Error("invalid type");return p[n]}})}function b(g){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<c)return-68;O("/",w,a,b);return c}catch(d){if("undefined"==typeof X||"ErrnoError"!==d.name)throw d;return-d.Kb}},F:function(a,b){try{return a=a?K(w,a):"",Jb(yb,a,b)}catch(c){if("undefined"==typeof X||"ErrnoError"!==c.name)throw c;return-c.Kb}},z:function(a,b,c){try{return b=b?K(w,b):"",b=Ib(a,b),b=M(b),"/"===b[b.length-1]&&(b=b.substr(0,b.length-
1)),W(b,c),0}catch(d){if("undefined"==typeof X||"ErrnoError"!==d.name)throw d;return-d.Kb}},E:function(a,b,c,d){try{b=b?K(w,b):"";var e=d&256;b=Ib(a,b,d&4096);return Jb(e?yb:xb,b,c)}catch(h){if("undefined"==typeof X||"ErrnoError"!==h.name)throw h;return-h.Kb}},y:function(a,b,c,d){Kb=d;try{b=b?K(w,b):"";b=Ib(a,b);var e=d?Lb():0;return Bb(b,c,e).Sb}catch(h){if("undefined"==typeof X||"ErrnoError"!==h.name)throw h;return-h.Kb}},w:function(a,b,c,d){try{b=b?K(w,b):"";b=Ib(a,b);if(0>=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<e?(z[c>>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<a)return!1;for(var c=1;4>=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<d.length;++h)v[e++>>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<D?+Math.floor(D/4294967296)>>>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<D?+Math.floor(D/4294967296)>>>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<c;g++){var m=z[a>>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<l)break;"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}},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<D?+Math.floor(D/4294967296)>>>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<c;g++){var m=z[a>>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<B)){if(f.preRun)for("function"==typeof f.preRun&&(f.preRun=[f.preRun]);f.preRun.length;)wa();Ga(sa);0<B||(f.setStatus?(f.setStatus("Running..."),setTimeout(function(){setTimeout(function(){f.setStatus("")},1);a()},1)):a())}}
if(f.preInit)for("function"==typeof f.preInit&&(f.preInit=[f.preInit]);0<f.preInit.length;)f.preInit.pop()();var Qc=!0;f.noInitialRun&&(Qc=!1);Pc();
return moduleArg.ready
}
);
})();
export default Module;

Binary file not shown.

View File

@@ -34,7 +34,7 @@ function Header(props: HeaderProps) {
const id = useStore((store) => store.session.id);
const tags = useStore((store) => store.tags);
const setTag = useStore((store) => store.setTag);
console.log(tags);
return (
<>
{id && (

View File

@@ -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 && (
<PreviewModeNotice
{...previewSession.current}
onDiscard={() => 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) {
<Text variant={"subtitle"}>Preview</Text>
<Text variant={"body"}>
You are previewing note version edited from{" "}
{getFormattedDate(dateCreated)} to {getFormattedDate(dateEdited)}.
{getFormattedDate(dateCreated, "date-time")} to{" "}
{getFormattedDate(dateEdited, "date-time")}.
</Text>
</Flex>
<Flex>

View File

@@ -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,

View File

@@ -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]);

View File

@@ -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<T extends FilterableItem> = {
type FilteredListProps<T> = {
placeholders: { filter: string; empty: string };
items: () => T[];
items: () => Promise<T[]>;
filter: (items: T[], query: string) => T[];
onCreateNewItem: (title: string) => Promise<void>;
renderItem: (
@@ -40,9 +40,7 @@ type FilteredListProps<T extends FilterableItem> = {
) => JSX.Element;
};
export function FilteredList<T extends FilterableItem>(
props: FilteredListProps<T>
) {
export function FilteredList<T>(props: FilteredListProps<T>) {
const {
items: _items,
filter,
@@ -56,8 +54,8 @@ export function FilteredList<T extends FilterableItem>(
const noItemsFound = items.length <= 0 && query && query.length > 0;
const inputRef = useRef<HTMLInputElement>(null);
const refresh = useCallback(() => {
setItems(_items());
const refresh = useCallback(async () => {
setItems(await _items());
}, [_items]);
useEffect(() => {
@@ -65,14 +63,10 @@ export function FilteredList<T extends FilterableItem>(
}, [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<T extends FilterableItem>(
const _createNewItem = useCallback(
async (title) => {
await onCreateNewItem(title);
refresh();
await refresh();
setQuery(undefined);
if (inputRef.current) inputRef.current.value = "";
},

View File

@@ -188,7 +188,7 @@ export function showSortMenu(groupingKey: GroupingKey, refresh: () => void) {
);
}
function changeGroupOptions(
async function changeGroupOptions(
options: GroupingMenuOptions,
item: Omit<MenuButtonItem, "type">
) {
@@ -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();
}

View File

@@ -20,7 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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<Item>;
items: VirtualizedGrouping<Item>;
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 (
<Flex variant="columnFill">
{!props.items.length && props.placeholder ? (
{!props.items.ids.length && props.placeholder ? (
<>
{header}
{props.isLoading ? (
@@ -141,85 +146,79 @@ function ListContainer(props: ListContainerProps) {
>
<Virtuoso
ref={listRef}
data={items}
computeItemKey={(index) => 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 }) => (
<div
{...props}
onFocus={() => onFocus(props["data-item-index"])}
onMouseDown={(e) =>
onMouseDown(e.nativeEvent, props["data-item-index"])
}
>
{props.children}
</div>
),
Item: VirtuosoItem,
Header: () => (header ? header : <Announcements />)
}}
context={{
onMouseDown,
onFocus
}}
itemContent={(index, item) => {
if (!item) return null;
switch (item.type) {
case "header":
if (!group) return null;
return (
<GroupHeader
groupingKey={group}
refresh={refresh}
title={item.title}
isFocused={index === focusedGroupIndex}
index={index}
onSelectGroup={() => {
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 <div style={{ height: 28, width: "100%" }} />;
return (
<GroupHeader
groupingKey={group}
refresh={refresh}
title={item.title}
isFocused={index === focusedGroupIndex}
index={index}
onSelectGroup={() => {
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 (
<ListItemWrapper
item={item}
context={context}
group={group}
compact={compact}
/>
);
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 (
<ListItemWrapper
key={item}
items={items}
id={item}
context={context}
group={group}
compact={compact}
/>
);
}}
/>
</Flex>
@@ -252,6 +251,29 @@ function ListContainer(props: ListContainerProps) {
}
export default ListContainer;
function VirtuosoItem({
item: _item,
context,
...props
}: ItemProps<string | GroupHeaderType> & {
context?: {
onMouseDown: (e: MouseEvent, itemIndex: number) => void;
onFocus: (itemIndex: number) => void;
};
}) {
return (
<div
{...props}
onFocus={() => context?.onFocus(props["data-item-index"])}
onMouseDown={(e) =>
context?.onMouseDown(e.nativeEvent, props["data-item-index"])
}
>
{props.children}
</div>
);
}
/**
* Scroll the element at the specified index into view and
* wait until it renders into the DOM. This function keeps

View File

@@ -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<TItem = Item> = {
group?: GroupingKey;
item: TItem;
items: VirtualizedGrouping<TItem>;
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<Item>();
const tags = useRef<TagsWithDateEdited>();
const notebooks = useRef<NotebooksWithDateEdited>();
const reminder = useRef<ReminderItem>();
const color = useRef<Color>();
const totalNotes = useRef<number>(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 <div style={{ height: DEFAULT_ITEM_HEIGHT, width: "100%" }} />;
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 (
<Note
compact={compact}
item={item}
tags={tags}
color={color}
references={references}
reminder={getReminder(item.id)}
tags={tags.current}
color={color.current}
notebooks={notebooks.current}
reminder={reminder.current}
date={getDate(item, group)}
context={context}
/>
@@ -70,7 +103,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
return (
<Notebook
item={item}
totalNotes={getTotalNotes(item)}
totalNotes={totalNotes.current}
date={getDate(item, group)}
/>
);
@@ -81,71 +114,148 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
case "topic":
return <Topic item={item} />;
case "tag":
return <Tag item={item} />;
return <Tag item={item} totalNotes={totalNotes.current} />;
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<T> {
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<string, Item>) {
const { type } = items[ids[0]];
if (type === "note") return resolveNotes(ids);
else if (type === "notebook") {
const data: Record<string, number> = {};
for (const id of ids) data[id] = await db.notebooks.totalNotes(id);
return data;
} else if (type === "tag") {
const data: Record<string, number> = {};
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<string>;
colors: Set<string>;
tags: Set<string>;
reminders: Set<string>;
} = {
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<string, NoteResolvedData> = {};
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
);
}

View File

@@ -17,21 +17,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Note } from "@notesnook/core/dist/types";
import { Notebook, Tag } from "@notesnook/core";
export type Context = {
type: string;
notes?: Note[];
value?: { topic?: string };
} & Record<string, unknown>;
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<T> = { items: T[]; dateEdited: number };
export type NotebooksWithDateEdited = WithDateEdited<Notebook>;
export type TagsWithDateEdited = WithDateEdited<Tag>;

View File

@@ -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<TItem extends Item, TContext> = {
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<HTMLDivElement>) => 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<TItem extends Item, TContext>(
props: ListItemProps<TItem, TContext>
) {
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);
}

View File

@@ -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}`);
}

View File

@@ -17,10 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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) {
<Flex
sx={{ alignItems: "center", flexWrap: "wrap", gap: 1, mt: "small" }}
>
{references?.references?.map((reference) => (
{notebooks?.items.map((notebook) => (
<IconTag
key={reference.url}
key={notebook.id}
onClick={() => {
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 && <Star color={primary} size={15} />}
{tags.map((tag) => {
{tags?.items.map((tag) => {
return (
<Button
data-test-id={`tag-item`}
@@ -249,7 +256,7 @@ function Note(props: NoteProps) {
navigate(`/tags/${tag.id}`);
}}
sx={{
maxWidth: `calc(100% / ${tags.length})`,
maxWidth: `calc(100% / ${tags.items.length})`,
overflow: "hidden",
textOverflow: "ellipsis",
color: "var(--paragraph-secondary)"
@@ -272,30 +279,17 @@ export default React.memo(Note, function (prevProps, nextProps) {
const nextItem = nextProps.item;
return (
prevProps.date === nextProps.date &&
prevItem.pinned === nextItem.pinned &&
prevItem.favorite === nextItem.favorite &&
prevItem.localOnly === nextItem.localOnly &&
prevItem.headline === nextItem.headline &&
prevItem.title === nextItem.title &&
prevItem.locked === nextItem.locked &&
prevItem.conflicted === nextItem.conflicted &&
prevItem.color === nextItem.color &&
prevProps.compact === nextProps.compact &&
prevProps.references?.dateEdited === nextProps.references?.dateEdited &&
prevProps.reminder?.dateModified === nextProps.reminder?.dateModified &&
JSON.stringify(prevProps.tags) === JSON.stringify(nextProps.tags) &&
JSON.stringify(prevProps.context) === JSON.stringify(nextProps.context)
prevItem.dateModified === nextItem.dateModified &&
prevProps.notebooks?.dateEdited === nextProps.notebooks?.dateEdited &&
prevProps.tags?.dateEdited === nextProps.tags?.dateEdited &&
prevProps.reminder?.dateModified === nextProps.reminder?.dateModified
);
});
const pin = (note: Note) => {
return store
.pin(note.id)
.then(async () => {
if (note.pinned) await showUnpinnedToast(note.id, "note");
})
.catch((error) => showToast("error", error.message));
};
// const pin = (note: Note) => {
// return store
// .pin(note.id);
// };
const formats = [
{
@@ -333,12 +327,12 @@ const formats = [
const notFullySyncedText =
"Cannot perform this action because note is not fully synced.";
const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
note,
items = []
) => {
const isSynced = db.notes.note(note.id)?.synced();
const ids = items.map((i) => i.id);
const menuItems: (
note: Note,
ids?: string[],
context?: { color?: Color }
) => MenuItem[] = (note, ids = [], context) => {
// const isSynced = db.notes.note(note.id)?.synced();
return [
{
@@ -347,7 +341,8 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
title: "Pin",
isChecked: note.pinned,
icon: Pin.path,
onClick: () => pin(note)
onClick: () => store.pin(!note.pinned, ...ids),
multiSelect: true
},
{
type: "button",
@@ -355,7 +350,8 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
title: "Readonly",
isChecked: note.readonly,
icon: Readonly.path,
onClick: () => store.readonly(note.id)
onClick: () => store.readonly(!note.readonly, ...ids),
multiSelect: true
},
{
type: "button",
@@ -363,12 +359,13 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
title: "Favorite",
isChecked: note.favorite,
icon: StarOutline.path,
onClick: () => store.favorite(note.id)
onClick: () => store.favorite(!note.favorite, ...ids),
multiSelect: true
},
{
type: "button",
key: "lock",
isDisabled: !isSynced,
//isDisabled: !isSynced,
title: "Lock",
isChecked: note.locked,
icon: Lock.path,
@@ -398,7 +395,7 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
key: "notebooks",
title: "Notebooks",
icon: Notebook.path,
menu: { items: notebooksMenuItems(items) },
menu: { items: notebooksMenuItems(ids) },
multiSelect: true
},
{
@@ -406,7 +403,7 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
key: "colors",
title: "Assign color",
icon: Colors.path,
menu: { items: colorsToMenuItems(note) }
menu: { items: colorsToMenuItems(context?.color, ids) }
},
{
type: "button",
@@ -414,17 +411,14 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
title: "Tags",
icon: Tag2.path,
multiSelect: true,
menu: { items: tagsMenuItems(items) }
// onClick: async ({ items }) => {
// await showAddTagsDialog(items.map((i) => i.id));
// }
menu: { items: tagsMenuItems(ids) }
},
{ key: "sep2", type: "separator" },
{
type: "button",
key: "print",
title: "Print",
isDisabled: !isSynced,
//isDisabled: !isSynced,
icon: Print.path,
onClick: async () => {
const item = db.notes?.note(note);
@@ -439,7 +433,8 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
type: "button",
key: "publish",
isDisabled:
!isSynced || (!db.monographs.isPublished(note.id) && note.locked),
//!isSynced ||
!db.monographs.isPublished(note.id) && note.locked,
icon: Publish.path,
title: "Publish",
isChecked: db.monographs.isPublished(note.id),
@@ -454,7 +449,7 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
key: "export",
title: "Export as",
icon: Export.path,
isDisabled: !isSynced,
//isDisabled: !isSynced,
menu: {
items: formats.map((format) => ({
type: "button",
@@ -462,7 +457,7 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
title: format.title,
tooltip: `Export as ${format.title} - ${format.subtitle}`,
icon: format.icon.path,
isDisabled: format.type === "pdf" && items.length > 1,
isDisabled: format.type === "pdf" && ids.length > 1,
// ? "Multiple notes cannot be exported as PDF."
// : false,
isPro: format.type !== "txt",
@@ -519,27 +514,17 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
type: "button",
key: "duplicate",
title: "Duplicate",
isDisabled: !isSynced || note.locked,
//!isSynced ||
isDisabled: note.locked,
icon: Duplicate.path,
onClick: async () => {
const id = await store.get().duplicate(note);
if (
await confirm({
title: "Open duplicated note?",
message: "Do you want to open the duplicated note?",
negativeButtonText: "No",
positiveButtonText: "Yes"
})
) {
hashNavigate(`/notes/${id}/edit`, { replace: true });
}
}
onClick: () => store.get().duplicate(...ids),
multiSelect: true
},
{
type: "button",
key: "local-only",
isHidden: !userstore.get().isLoggedIn,
isDisabled: !isSynced,
//isDisabled: !isSynced,
title: "Local only",
isChecked: note.localOnly,
icon: note.localOnly ? Sync.path : SyncOff.path,
@@ -547,15 +532,18 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
if (
note.localOnly ||
(await confirm({
title: "Prevent this item from syncing?",
message:
"Turning sync off for this item will automatically delete it from all other devices & any future changes to this item won't get synced. Are you sure you want to continue?",
title: `Prevent ${pluralize(ids.length, "note")} from syncing?`,
message: `${pluralize(
ids.length,
"note"
)} will be automatically deleted from all other devices & any future changes won't get synced. Are you sure you want to continue?`,
positiveButtonText: "Yes",
negativeButtonText: "No"
}))
)
await store.get().localOnly(note.id);
}
await store.localOnly(!note.localOnly, ...ids);
},
multiSelect: true
},
{ key: "sep3", type: "separator" },
{
@@ -564,18 +552,17 @@ const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
title: "Move to trash",
variant: "dangerous",
icon: Trash.path,
isDisabled:
items.length === 1
? db.monographs?.isPublished(note.id)
: items.some((item) => !db.notes?.note(item.id).synced()),
onClick: () => Multiselect.moveNotesToTrash(items, items.length > 1),
isDisabled: ids.length === 1 && db.monographs.isPublished(note.id),
onClick: () => Multiselect.moveNotesToTrash(ids, ids.length > 1),
multiSelect: true
}
];
};
function colorsToMenuItems(note: Note): MenuItem[] {
const noteColor = db.relations.to(note, "color").resolved(1)[0];
function colorsToMenuItems(
noteColor: Color | undefined,
ids: string[]
): MenuItem[] {
return COLORS.map((color) => {
return {
type: "button",
@@ -583,160 +570,133 @@ function colorsToMenuItems(note: Note): MenuItem[] {
title: color.title,
icon: Circle.path,
styles: { icon: { color: StaticColors[color.key] } },
isChecked: noteColor.title === color.title,
onClick: () => store.setColor(note.id, color.title)
isChecked: noteColor && noteColor.title === color.title,
onClick: () => store.setColor(color.title, ...ids)
} satisfies MenuItem;
});
}
function notebooksMenuItems(items: Note[]): MenuItem[] {
const noteIds = items.map((i) => i.id);
const menuItems: MenuItem[] = [];
menuItems.push({
type: "button",
key: "link-notebooks",
title: "Link to...",
icon: AddToNotebook.path,
onClick: () => showMoveNoteDialog(noteIds)
});
const notebooks = items
.map((note) => db.relations.to(note, "notebook").resolved())
.flat();
const topics = items.map((note) => note.notebooks || []).flat();
if (topics?.length > 0 || notebooks.length > 0) {
menuItems.push(
{
type: "button",
key: "remove-from-all-notebooks",
title: "Unlink from all",
icon: RemoveShortcutLink.path,
onClick: async () => {
await db.notes.removeFromAllNotebooks(...noteIds);
store.refresh();
function notebooksMenuItems(ids: string[]): MenuItem[] {
return [
{
type: "button",
key: "link-notebooks",
title: "Link to...",
icon: AddToNotebook.path,
onClick: () => showMoveNoteDialog(ids)
},
{
type: "lazy-loader",
key: "notebooks-lazy-loader",
async items() {
const notebooks: Map<string, NotebookItem> = new Map();
for (const id of ids) {
const linkedNotebooks = await db.relations
.to({ id, type: "note" }, "notebook")
.resolve();
linkedNotebooks.forEach((nb) => notebooks.set(nb.id, nb));
}
},
{ key: "sep", type: "separator" }
);
if (notebooks.size <= 0) return [];
const menuItems: MenuItem[] = [
{
type: "button",
key: "remove-from-all-notebooks",
title: "Unlink from all",
icon: RemoveShortcutLink.path,
onClick: async () => {
await db.notes.removeFromAllNotebooks(...ids);
store.refresh();
}
},
{ key: "sep", type: "separator" }
];
notebooks.forEach((notebook) => {
if (!notebook || menuItems.find((item) => item.key === notebook.id))
return;
menuItems.push({
type: "button",
key: notebook.id,
title: notebook.title,
icon: Notebook.path,
isChecked: true,
tooltip: "Click to remove from this notebook",
onClick: async () => {
await db.notes.removeFromNotebook({ id: notebook.id }, ...noteIds);
store.refresh();
}
});
});
topics.forEach((ref) => {
const notebook = db.notebooks.notebook(ref.id);
if (!notebook) return;
for (const topicId of ref.topics) {
if (!notebook.topics.topic(topicId)) continue;
if (menuItems.find((item) => item.key === topicId)) continue;
const topic = notebook.topics.topic(topicId)?._topic;
if (!topic) continue;
menuItems.push({
type: "button",
key: topicId,
title: topic.title,
icon: Topic.path,
isChecked: true,
tooltip: "Click to remove from this topic",
onClick: async () => {
await db.notes.removeFromNotebook(
{ id: ref.id, topic: topic.id },
...noteIds
);
store.refresh();
}
notebooks.forEach((notebook) => {
menuItems.push({
type: "button",
key: notebook.id,
title: notebook.title,
icon: Notebook.path,
isChecked: true,
tooltip: "Click to remove from this notebook",
onClick: async () => {
await db.notes.removeFromNotebook(notebook.id, ...ids);
store.refresh();
}
});
});
return menuItems;
}
});
}
return menuItems;
}
];
}
function tagsMenuItems(items: Note[]): MenuItem[] {
const noteIds = items.map((i) => i.id);
const menuItems: MenuItem[] = [];
menuItems.push({
type: "button",
key: "assign-tags",
title: "Assign to...",
icon: Plus.path,
onClick: async () => {
await showAddTagsDialog(noteIds);
}
});
const tags = items
.map((note) => db.relations.to(note, "tag").resolved())
.flat();
if (tags.length > 0) {
menuItems.push(
{
type: "button",
key: "remove-from-all-tags",
title: "Remove from all",
icon: RemoveShortcutLink.path,
onClick: async () => {
for (const note of items) {
for (const tag of tags) {
await db.relations.unlink(tag, note);
function tagsMenuItems(ids: string[]): MenuItem[] {
return [
{
type: "button",
key: "assign-tags",
title: "Assign to...",
icon: Plus.path,
onClick: () => showAddTagsDialog(ids)
},
{
type: "lazy-loader",
key: "tags-lazy-loader",
async items() {
const tags: Map<string, Tag> = new Map();
for (const id of ids) {
const linkedTags = await db.relations
.to({ id, type: "note" }, "tag")
.resolve();
linkedTags.forEach((tag) => tags.set(tag.id, tag));
}
if (tags.size <= 0) return [];
const menuItems: MenuItem[] = [
{
type: "button",
key: "remove-from-all-tags",
title: "Remove from all",
icon: RemoveShortcutLink.path,
onClick: async () => {
for (const id of ids) {
await db.relations.to({ id, type: "note" }, "tag").unlink();
}
tagStore.get().refresh();
await editorStore.get().refreshTags();
await store.get().refresh();
}
}
tagStore.get().refresh();
editorStore.get().refreshTags();
store.get().refresh();
}
},
{ key: "sep", type: "separator" }
);
},
{ key: "sep", type: "separator" }
];
tags.forEach((tag) => {
if (menuItems.find((item) => item.key === tag.id)) return;
menuItems.push({
type: "button",
key: tag.id,
title: tag.title,
icon: TagIcon.path,
isChecked: true,
tooltip: "Click to remove from this tag",
onClick: async () => {
for (const note of items) {
await db.relations.unlink(tag, note);
}
tagStore.get().refresh();
editorStore.get().refreshTags();
store.get().refresh();
}
});
});
}
return menuItems;
tags.forEach((tag) => {
menuItems.push({
type: "button",
key: tag.id,
title: tag.title,
icon: TagIcon.path,
isChecked: true,
tooltip: "Click to remove from this tag",
onClick: async () => {
for (const id of ids) {
await db.relations.unlink(tag, { id, type: "note" });
}
tagStore.get().refresh();
await editorStore.get().refreshTags();
await store.get().refresh();
}
});
});
return menuItems;
}
}
];
}
async function copyNote(noteId: string, format: "md" | "txt") {
try {
const note = db.notes?.note(noteId);
const note = await db.notes?.note(noteId);
if (!note) throw new Error("No note with this id exists.");
const result = await exportNote(note, format, true);

View File

@@ -45,7 +45,6 @@ import { Attachment } from "../attachment";
import { formatBytes } from "@notesnook/common";
import { getTotalSize } from "../../common/attachments";
import Notebook from "../notebook";
import { getTotalNotes } from "@notesnook/common";
import Reminder from "../reminder";
import { getFormattedDate } from "@notesnook/common";
import { ScopedThemeProvider } from "../theme-provider";
@@ -282,7 +281,7 @@ function Properties(props) {
key={notebook.id}
item={notebook}
date={notebook.dateCreated}
totalNotes={getTotalNotes(notebook)}
totalNotes={0} // getTotalNotes(notebook)}
simplified
/>
))}

View File

@@ -31,11 +31,10 @@ import { pluralize } from "@notesnook/common";
import { MenuItem } from "@notesnook/ui";
import { Tag } from "@notesnook/core/dist/types";
type TagProps = { item: Tag };
type TagProps = { item: Tag; totalNotes: number };
function Tag(props: TagProps) {
const { item } = props;
const { item, totalNotes } = props;
const { id, title } = item;
const totalNotes = db.relations.to(item, "note").length;
return (
<ListItem
@@ -63,10 +62,7 @@ function Tag(props: TagProps) {
}
export default Tag;
const menuItems: (tag: Tag, items?: Tag[]) => MenuItem[] = (
tag,
items = []
) => {
const menuItems: (tag: Tag, ids?: string[]) => MenuItem[] = (tag, ids = []) => {
return [
{
type: "button",
@@ -94,13 +90,11 @@ const menuItems: (tag: Tag, items?: Tag[]) => MenuItem[] = (
title: "Delete",
icon: DeleteForver.path,
onClick: async () => {
for (const tag of items) {
await db.tags.remove(tag.id);
}
showToast("success", `${pluralize(items.length, "tag")} deleted`);
editorStore.refreshTags();
tagStore.refresh();
noteStore.refresh();
await db.tags.remove(...ids);
showToast("success", `${pluralize(ids.length, "tag")} deleted`);
await editorStore.refreshTags();
await tagStore.refresh();
await noteStore.refresh();
},
multiSelect: true
}

View File

@@ -28,7 +28,6 @@ import { Multiselect } from "../../common/multi-select";
import { confirm } from "../../common/dialog-controller";
import { useStore as useNotesStore } from "../../stores/note-store";
import { pluralize } from "@notesnook/common";
import { getTotalNotes } from "@notesnook/common";
import { MenuItem } from "@notesnook/ui";
import { Note, Topic } from "@notesnook/core/dist/types";
@@ -46,7 +45,7 @@ function Topic(props: TopicProps) {
item={topic}
onClick={() => navigate(`/notebooks/${topic.notebookId}/${topic.id}`)}
title={topic.title}
footer={<Text variant="subBody">{getTotalNotes(topic)}</Text>}
footer={<Text variant="subBody">0</Text>} // getTotalNotes(topic)}
menuItems={menuItems}
/>
);

View File

@@ -31,7 +31,7 @@ import { store as notestore } from "../stores/note-store";
import { store as editorstore } from "../stores/editor-store";
import { Perform } from "../common/dialog-controller";
import { FilteredList } from "../components/filtered-list";
import { ItemReference, Tag } from "@notesnook/core/dist/types";
import { ItemReference, Tag, isGroupHeader } from "@notesnook/core/dist/types";
type SelectedReference = {
id: string;
@@ -39,12 +39,6 @@ type SelectedReference = {
op: "add" | "remove";
};
type Item = {
id: string;
type: "tag" | "header";
title: string;
};
export type AddTagsDialogProps = {
onClose: Perform;
noteIds: string[];
@@ -62,29 +56,30 @@ function AddTagsDialog(props: AddTagsDialogProps) {
const [selected, setSelected] = useState<SelectedReference[]>([]);
const getAllTags = useCallback(() => {
refreshTags();
return store.get().tags.filter((a) => a.type !== "header");
const getAllTags = useCallback(async () => {
await refreshTags();
return (store.get().tags?.ids.filter((a) => !isGroupHeader(a)) ||
[]) as string[];
}, [refreshTags]);
useEffect(() => {
if (!tags) return;
(async function () {
const copy = selected.slice();
for (const tag of tags.ids) {
if (isGroupHeader(tag)) continue;
if (copy.findIndex((a) => a.id === tag) > -1) continue;
setSelected((s) => {
const selected = s.slice();
for (const tag of tags) {
if (tag.type === "header") continue;
if (selected.findIndex((a) => a.id === tag.id) > -1) continue;
if (tagHasNotes(tag, noteIds)) {
selected.push({
id: tag.id,
if (await tagHasNotes(tag, noteIds)) {
copy.push({
id: tag,
op: "add",
new: false
});
}
}
return selected;
});
setSelected(copy);
})();
}, [noteIds, tags, setSelected]);
return (
@@ -127,7 +122,8 @@ function AddTagsDialog(props: AddTagsDialogProps) {
empty: "Add a new tag",
filter: "Search or add a new tag"
}}
filter={(tags, query) => db.lookup.tags(tags, query) || []}
filter={(tags, query) => []}
// db.lookup.tags(tags, query) || []}
onCreateNewItem={async (title) => {
const tagId = await db.tags.add({ title });
if (!tagId) return;
@@ -136,17 +132,18 @@ function AddTagsDialog(props: AddTagsDialogProps) {
{ id: tagId, new: true, op: "add" }
]);
}}
renderItem={(tag, _index) => {
const selectedTag = selected.find((item) => item.id === tag.id);
renderItem={(tagId, _index) => {
const selectedTag = selected.find((item) => item.id === tagId);
return (
<TagItem
key={tag.id}
tag={tag}
key={tagId}
id={tagId}
resolve={(id) => tags?.item(id)}
selected={selectedTag ? selectedTag.op : false}
onSelect={() => {
setSelected((selected) => {
const copy = selected.slice();
const index = copy.findIndex((item) => item.id === tag.id);
const index = copy.findIndex((item) => item.id === tagId);
const isNew = copy[index] && copy[index].new;
if (isNew) {
copy.splice(index, 1);
@@ -156,7 +153,7 @@ function AddTagsDialog(props: AddTagsDialogProps) {
op: copy[index].op === "add" ? "remove" : "add"
};
} else {
copy.push({ id: tag.id, new: true, op: "add" });
copy.push({ id: tagId, new: true, op: "add" });
}
return copy;
});
@@ -171,12 +168,22 @@ function AddTagsDialog(props: AddTagsDialogProps) {
}
function TagItem(props: {
tag: Item;
id: string;
resolve: (id: string) => Promise<Tag | undefined> | undefined;
selected: boolean | SelectedReference["op"];
onSelect: () => void;
}) {
const { tag, selected, onSelect } = props;
const { id, resolve, selected, onSelect } = props;
const [tag, setTag] = useState<Tag>();
useEffect(() => {
(async function () {
setTag(await resolve(id));
})();
}, [id, resolve]);
if (!tag) return null;
return (
<Flex
as="li"
@@ -224,8 +231,6 @@ function SelectedCheck({
);
}
function tagHasNotes(tag: Tag, noteIds: string[]) {
return db.relations
?.from(tag, "note")
?.some((r) => noteIds.indexOf(r.to.id) > -1);
function tagHasNotes(tagId: string, noteIds: string[]) {
return db.relations.from({ type: "tag", id: tagId }, "note").has(...noteIds);
}

View File

@@ -35,20 +35,23 @@ import { useStore, store } from "../stores/notebook-store";
import { store as notestore } from "../stores/note-store";
import { Perform } from "../common/dialog-controller";
import { showToast } from "../utils/toast";
import { pluralize } from "@notesnook/common";
import { isMac } from "../utils/platform";
import { create } from "zustand";
import { FilteredList } from "../components/filtered-list";
import { Topic, Notebook, GroupHeader } from "@notesnook/core/dist/types";
import {
Notebook,
GroupHeader,
isGroupHeader
} from "@notesnook/core/dist/types";
type MoveDialogProps = { onClose: Perform; noteIds: string[] };
type NotebookReference = {
id: string;
topic?: string;
//topic?: string;
new: boolean;
op: "add" | "remove";
};
type Item = Topic | Notebook | GroupHeader;
type Item = Notebook | GroupHeader;
interface ISelectionStore {
selected: NotebookReference[];
@@ -70,51 +73,71 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
const refreshNotebooks = useStore((store) => store.refresh);
const notebooks = useStore((store) => store.notebooks);
const getAllNotebooks = useCallback(() => {
refreshNotebooks();
return store.get().notebooks.filter((a) => a.type !== "header");
const getAllNotebooks = useCallback(async () => {
await refreshNotebooks();
return (store.get().notebooks?.ids.filter((a) => !isGroupHeader(a)) ||
[]) as string[];
}, [refreshNotebooks]);
useEffect(() => {
if (!notebooks) return;
const selected: NotebookReference[] = useSelectionStore
.getState()
.selected.slice();
for (const notebook of notebooks) {
if (notebook.type === "header") continue;
for (const topic of notebook.topics) {
// for (const notebook of notebooks.ids) {
// if (isGroupHeader(notebook)) continue;
// // for (const topic of notebook.topics) {
// // const isSelected =
// // selected.findIndex(
// // (item) => item.id === notebook.id && item.topic === topic.id
// // ) > -1;
// // if (!isSelected && topicHasNotes(topic, noteIds)) {
// // selected.push({
// // id: notebook.id,
// // topic: topic.id,
// // op: "add",
// // new: false
// // });
// // }
// // }
// }
(async function () {
const selected: NotebookReference[] = useSelectionStore
.getState()
.selected.slice();
for (const { fromId: notebookId } of await db.relations
.to({ type: "note", ids: noteIds }, "notebook")
.get()) {
const isSelected =
selected.findIndex(
(item) => item.id === notebook.id && item.topic === topic.id
) > -1;
if (!isSelected && topicHasNotes(topic, noteIds)) {
selected.findIndex((item) => item.id === notebookId) > -1;
if (isSelected) continue;
if (await notebookHasNotes(notebookId, noteIds)) {
selected.push({
id: notebook.id,
topic: topic.id,
id: notebookId,
op: "add",
new: false
});
}
}
}
for (const notebook of noteIds
.map((id) => db.relations.to({ id, type: "note" }, "notebook"))
.flat()) {
const isSelected =
notebook && selected.findIndex((item) => item.id === notebook.id) > -1;
if (!notebook || isSelected) continue;
setSelected(selected);
setIsMultiselect(selected.length > 1);
})();
selected.push({
id: notebook.id,
op: "add",
new: false
});
}
// for (const notebook of noteIds
// .map((id) => db.relations.to({ id, type: "note" }, "notebook"))
// .flat()) {
// const isSelected =
// notebook && selected.findIndex((item) => item.id === notebook.id) > -1;
// if (!notebook || isSelected) continue;
setSelected(selected);
setIsMultiselect(selected.length > 1);
// selected.push({
// id: notebook.id,
// op: "add",
// new: false
// });
// }
}, [noteIds, notebooks, setSelected, setIsMultiselect]);
const _onClose = useCallback(
@@ -142,9 +165,9 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
for (const item of selected) {
try {
if (item.op === "remove") {
await db.notes.removeFromNotebook(item, ...noteIds);
await db.notes.removeFromNotebook(item.id, ...noteIds);
} else if (item.op === "add") {
await db.notes.addToNotebook(item, ...noteIds);
await db.notes.addToNotebook(item.id, ...noteIds);
}
} catch (e) {
if (e instanceof Error) showToast("error", e.message);
@@ -154,13 +177,13 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
notestore.refresh();
const stringified = stringifySelected(selected);
if (stringified) {
showToast(
"success",
`${pluralize(noteIds.length, "note")} ${stringified}`
);
}
// const stringified = stringifySelected(selected);
// if (stringified) {
// showToast(
// "success",
// `${pluralize(noteIds.length, "note")} ${stringified}`
// );
// }
_onClose(true);
}
@@ -197,8 +220,10 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
filter: "Search or add a new notebook"
}}
items={getAllNotebooks}
filter={(notebooks, query) =>
db.lookup.notebooks(notebooks, query) || []
filter={
(notebooks, query) => []
//db.lookup.notebooks(notebooks, query) || []
}
onCreateNewItem={async (title) => {
await db.notebooks.add({
@@ -207,12 +232,13 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
}}
renderItem={(notebook, _index, refresh, isSearching) => (
<NotebookItem
key={notebook.id}
notebook={notebook}
key={notebook}
id={notebook}
resolve={(id) => notebooks?.item(id)}
isSearching={isSearching}
onCreateItem={async (title) => {
await db.notebooks.topics(notebook.id).add({ title });
refresh();
// await db.notebooks.topics(notebook).add({ title });
// refresh();
}}
/>
)}
@@ -223,18 +249,27 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
}
function NotebookItem(props: {
notebook: Notebook;
id: string;
isSearching: boolean;
resolve: (id: string) => Promise<Notebook | undefined> | undefined;
onCreateItem: (title: string) => void;
}) {
const { notebook, isSearching, onCreateItem } = props;
const { id, resolve, isSearching, onCreateItem } = props;
const [isCreatingNew, setIsCreatingNew] = useState(false);
const [notebook, setNotebook] = useState<Notebook>();
const setIsMultiselect = useSelectionStore((store) => store.setIsMultiselect);
const setSelected = useSelectionStore((store) => store.setSelected);
const isMultiselect = useSelectionStore((store) => store.isMultiselect);
useEffect(() => {
(async function () {
setNotebook(await resolve(id));
})();
}, [id, resolve]);
if (!notebook) return null;
return (
<Box as="li" data-test-id="notebook">
<Box
@@ -286,10 +321,10 @@ function NotebookItem(props: {
sx={{ fontWeight: "body" }}
>
{notebook.title}
<Text variant="subBody" sx={{ fontWeight: "body" }}>
{/* <Text variant="subBody" sx={{ fontWeight: "body" }}>
{" "}
({pluralize(notebook.topics.length, "topic")})
</Text>
</Text> */}
</Text>
</Flex>
<Flex data-test-id="notebook-tools" sx={{ alignItems: "center" }}>
@@ -359,9 +394,9 @@ function NotebookItem(props: {
/>
</Flex>
)}
{notebook.topics.map((topic) => (
{/* {notebook.topics.map((topic) => (
<TopicItem key={topic.id} topic={topic} />
))}
))} */}
</Box>
</Box>
</Box>
@@ -370,54 +405,52 @@ function NotebookItem(props: {
function TopicSelectionIndicator({ notebook }: { notebook: Notebook }) {
const hasSelectedTopics = useSelectionStore(
(store) =>
store.selected.filter((nb) => nb.id === notebook.id && !!nb.topic)
.length > 0
(store) => store.selected.filter((nb) => nb.id === notebook.id).length > 0
);
if (!hasSelectedTopics) return null;
return <Circle size={8} color="accent" sx={{ mr: 1 }} />;
}
function TopicItem(props: { topic: Topic }) {
const { topic } = props;
// function TopicItem(props: { topic: Topic }) {
// const { topic } = props;
const setSelected = useSelectionStore((store) => store.setSelected);
const setIsMultiselect = useSelectionStore((store) => store.setIsMultiselect);
const isMultiselect = useSelectionStore((store) => store.isMultiselect);
// const setSelected = useSelectionStore((store) => store.setSelected);
// const setIsMultiselect = useSelectionStore((store) => store.setIsMultiselect);
// const isMultiselect = useSelectionStore((store) => store.isMultiselect);
return (
<Flex
as="li"
key={topic.id}
data-test-id="topic"
sx={{
alignItems: "center",
p: "small",
borderRadius: "default",
cursor: "pointer",
":hover": { bg: "hover" }
}}
onClick={(e) => {
const { selected } = useSelectionStore.getState();
// return (
// <Flex
// as="li"
// key={topic.id}
// data-test-id="topic"
// sx={{
// alignItems: "center",
// p: "small",
// borderRadius: "default",
// cursor: "pointer",
// ":hover": { bg: "hover" }
// }}
// onClick={(e) => {
// const { selected } = useSelectionStore.getState();
const isCtrlPressed = e.ctrlKey || e.metaKey;
if (isCtrlPressed) setIsMultiselect(true);
// const isCtrlPressed = e.ctrlKey || e.metaKey;
// if (isCtrlPressed) setIsMultiselect(true);
if (isMultiselect || isCtrlPressed) {
setSelected(selectMultiple(topic, selected));
} else {
setSelected(selectSingle(topic, selected));
}
}}
>
<SelectedCheck item={topic} />
<Text variant="body" sx={{ fontSize: "subtitle" }}>
{topic.title}
</Text>
</Flex>
);
}
// if (isMultiselect || isCtrlPressed) {
// setSelected(selectMultiple(topic, selected));
// } else {
// setSelected(selectSingle(topic, selected));
// }
// }}
// >
// <SelectedCheck item={topic} />
// <Text variant="body" sx={{ fontSize: "subtitle" }}>
// {topic.title}
// </Text>
// </Flex>
// );
// }
export default MoveDialog;
@@ -425,7 +458,7 @@ function SelectedCheck({
item,
size = 20
}: {
item?: Topic | Notebook;
item?: Notebook;
size?: number;
}) {
const selectedItems = useSelectionStore((store) => store.selected);
@@ -450,35 +483,28 @@ function SelectedCheck({
);
}
function createSelection(topic: Topic | Notebook): NotebookReference {
function createSelection(notebook: Notebook): NotebookReference {
return {
id: "notebookId" in topic ? topic.notebookId : topic.id,
topic: "notebookId" in topic ? topic.id : undefined,
id: notebook.id,
op: "add",
new: true
};
}
function findSelectionIndex(
topic: Topic | NotebookReference | Notebook,
ref: NotebookReference | Notebook,
array: NotebookReference[]
) {
return "op" in topic
? array.findIndex((a) => a.id === topic.id && a.topic === topic.topic)
: "notebookId" in topic
? array.findIndex((a) => a.id === topic.notebookId && a.topic === topic.id)
: array.findIndex((a) => a.id === topic.id && !a.topic);
return array.findIndex((a) => a.id === ref.id);
}
function topicHasNotes(topic: Item, noteIds: string[]) {
const notes: string[] = db.notes.topicReferences.get(topic.id) || [];
return noteIds.some((id) => notes.indexOf(id) > -1);
function notebookHasNotes(notebookId: string, noteIds: string[]) {
return db.relations
.from({ type: "notebook", id: notebookId }, "note")
.has(...noteIds);
}
function selectMultiple(
topic: Topic | Notebook,
selected: NotebookReference[]
) {
function selectMultiple(topic: Notebook, selected: NotebookReference[]) {
const index = findSelectionIndex(topic, selected);
const isSelected = index > -1;
const item = selected[index];
@@ -494,7 +520,7 @@ function selectMultiple(
return selected;
}
function selectSingle(topic: Topic | Notebook, array: NotebookReference[]) {
function selectSingle(topic: Notebook, array: NotebookReference[]) {
const selected: NotebookReference[] = array.filter((ref) => !ref.new);
const index = findSelectionIndex(topic, array);
@@ -511,39 +537,39 @@ function selectSingle(topic: Topic | Notebook, array: NotebookReference[]) {
return selected;
}
function stringifySelected(suggestion: NotebookReference[]) {
const added = suggestion
.filter((a) => a.new && a.op === "add")
.map(resolveReference)
.filter(Boolean);
const removed = suggestion
.filter((a) => a.op === "remove")
.map(resolveReference)
.filter(Boolean);
if (!added.length && !removed.length) return;
// function stringifySelected(suggestion: NotebookReference[]) {
// const added = suggestion
// .filter((a) => a.new && a.op === "add")
// .map(resolveReference)
// .filter(Boolean);
// const removed = suggestion
// .filter((a) => a.op === "remove")
// .map(resolveReference)
// .filter(Boolean);
// if (!added.length && !removed.length) return;
const parts = [];
if (added.length > 0) parts.push("added to");
if (added.length >= 1) parts.push(added[0]);
if (added.length > 1) parts.push(`and ${added.length - 1} others`);
// const parts = [];
// if (added.length > 0) parts.push("added to");
// if (added.length >= 1) parts.push(added[0]);
// if (added.length > 1) parts.push(`and ${added.length - 1} others`);
if (removed.length >= 1) {
if (parts.length > 0) parts.push("&");
parts.push("removed from");
parts.push(removed[0]);
}
if (removed.length > 1) parts.push(`and ${removed.length - 1} others`);
// if (removed.length >= 1) {
// if (parts.length > 0) parts.push("&");
// parts.push("removed from");
// parts.push(removed[0]);
// }
// if (removed.length > 1) parts.push(`and ${removed.length - 1} others`);
return parts.join(" ") + ".";
}
// return parts.join(" ") + ".";
// }
function resolveReference(ref: NotebookReference): string | undefined {
const notebook = db.notebooks.notebook(ref.id);
if (!notebook) return undefined;
// function resolveReference(ref: NotebookReference): string | undefined {
// const notebook = db.notebooks.notebook(ref.id);
// if (!notebook) return undefined;
if (ref.topic) {
return notebook.topics.topic(ref.topic)?._topic?.title;
} else {
return notebook.title;
}
}
// // if (ref.topic) {
// // return notebook.topics.topic(ref.topic)?._topic?.title;
// // } else {
// return notebook.title;
// // }
// }

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Flex, Text } from "@theme-ui/components";
import { User } from "../../../components/icons";
import { useStore as useUserStore } from "../../../stores/user-store";
import ObjectID from "@notesnook/core/dist/utils/object-id";
import { getObjectIdTimestamp } from "@notesnook/core/dist/utils/object-id";
import { getFormattedDate } from "@notesnook/common";
import { SUBSCRIPTION_STATUS } from "../../../common/constants";
import dayjs from "dayjs";
@@ -121,8 +121,7 @@ export function UserProfile() {
</Text>
<Text variant={"title"}>{user.email}</Text>
<Text variant={"subBody"}>
Member since{" "}
{getFormattedDate(new ObjectID(user.id).getTimestamp(), "date")}
Member since {getFormattedDate(getObjectIdTimestamp(user.id), "date")}
</Text>
</Flex>
</Flex>

View File

@@ -147,10 +147,7 @@ export class NNStorage implements IStorage {
return NNCrypto.encryptMulti(key, items, "text", "base64");
}
decrypt(
key: SerializedKey,
cipherData: Cipher<"base64">
): Promise<string | undefined> {
decrypt(key: SerializedKey, cipherData: Cipher<"base64">): Promise<string> {
cipherData.format = "base64";
return NNCrypto.decrypt(key, cipherData, "text");
}
@@ -158,7 +155,7 @@ export class NNStorage implements IStorage {
decryptMulti(
key: SerializedKey,
items: Cipher<"base64">[]
): Promise<string[] | undefined> {
): Promise<string[]> {
items.forEach((c) => (c.format = "base64"));
return NNCrypto.decryptMulti(key, items, "text");
}

View File

@@ -48,9 +48,7 @@ import { defineRoutes } from "./types";
const hashroutes = defineRoutes({
"/": () => {
return (
!editorStore.get().session.state && <Editor noteId={0} nonce={"-1"} />
);
return !editorStore.get().session.state && <Editor nonce={"-1"} />;
},
"/email/verify": () => {
showEmailVerificationDialog().then(afterAction);
@@ -91,7 +89,7 @@ const hashroutes = defineRoutes({
},
"/notes/create/:nonce": ({ nonce }) => {
closeOpenedDialog();
return <Editor noteId={0} nonce={nonce} />;
return <Editor nonce={nonce} />;
},
"/notes/:noteId/edit": ({ noteId }) => {
closeOpenedDialog();

View File

@@ -31,21 +31,32 @@ import { store as announcementStore } from "./announcement-store";
import { store as settingStore } from "./setting-store";
import BaseStore from "./index";
import { showToast } from "../utils/toast";
import { resetNotices } from "../common/notices";
import { Notice, resetNotices } from "../common/notices";
import { EV, EVENTS, SYNC_CHECK_IDS } from "@notesnook/core/dist/common";
import { logger } from "../utils/logger";
import Config from "../utils/config";
import { onPageVisibilityChanged } from "../utils/page-visibility";
import { NetworkCheck } from "../utils/network-check";
import { Color, Notebook, Tag } from "@notesnook/core";
type SyncState =
| "synced"
| "syncing"
| "conflicts"
| "failed"
| "completed"
| "offline"
| "disabled";
type SyncStatus = {
key: SyncState;
progress: number | null;
type: "download" | "upload" | null;
};
const networkCheck = new NetworkCheck();
var syncStatusTimeout = 0;
let pendingSync = false;
let syncStatusTimeout = 0;
let pendingSync: { full: boolean } | undefined = undefined;
/**
* @extends {BaseStore<AppStore>}
*/
class AppStore extends BaseStore {
class AppStore extends BaseStore<AppStore> {
// default state
isSideMenuOpen = false;
isFocusMode = false;
@@ -54,7 +65,7 @@ class AppStore extends BaseStore {
isAutoSyncEnabled = Config.get("autoSyncEnabled", true);
isSyncEnabled = Config.get("syncEnabled", true);
isRealtimeSyncEnabled = Config.get("isRealtimeSyncEnabled", true);
syncStatus = {
syncStatus: SyncStatus = {
key: navigator.onLine
? Config.get("syncEnabled", true)
? "synced"
@@ -63,13 +74,9 @@ class AppStore extends BaseStore {
progress: null,
type: null
};
colors = [];
globalMenu = { items: [], data: {} };
/**
* @type {import("../common/notices").Notice[]}
*/
notices = [];
shortcuts = [];
colors: Color[] = [];
notices: Notice[] = [];
shortcuts: (Notebook | Tag)[] = [];
lastSynced = 0;
init = () => {
@@ -141,7 +148,7 @@ class AppStore extends BaseStore {
await this.updateLastSynced();
await resetNotices();
noteStore.refresh();
await noteStore.refresh();
notebookStore.refresh();
reminderStore.refresh();
trashStore.refresh();
@@ -151,15 +158,17 @@ class AppStore extends BaseStore {
settingStore.refresh();
await editorstore.refresh();
this.refreshNavItems();
await this.refreshNavItems();
logger.measure("refreshing app");
};
refreshNavItems = () => {
refreshNavItems = async () => {
const shortcuts = await db.shortcuts.resolved();
const colors = await db.colors.all.items();
this.set((state) => {
state.shortcuts = db.shortcuts.resolved;
state.colors = db.colors.all;
state.shortcuts = shortcuts;
state.colors = colors;
});
};
@@ -190,7 +199,7 @@ class AppStore extends BaseStore {
);
};
toggleSideMenu = (toggleState) => {
toggleSideMenu = (toggleState: boolean) => {
this.set(
(state) =>
(state.isSideMenuOpen =
@@ -198,23 +207,15 @@ class AppStore extends BaseStore {
);
};
setGlobalMenu = (items, data) => {
this.set((state) => (state.globalMenu = { items, data }));
};
setIsEditorOpen = (toggleState) => {
setIsEditorOpen = (toggleState: boolean) => {
this.set((state) => (state.isEditorOpen = toggleState));
};
setIsVaultCreated = (toggleState) => {
setIsVaultCreated = (toggleState: boolean) => {
this.set((state) => (state.isVaultCreated = toggleState));
};
/**
*
* @param {import("../common/notices").Notice[]} notices
*/
setNotices = (...notices) => {
setNotices = (...notices: Notice[]) => {
this.set((state) => {
for (const notice of notices) {
const oldIndex = state.notices.findIndex((a) => a.type === notice.type);
@@ -224,26 +225,23 @@ class AppStore extends BaseStore {
});
};
dismissNotices = (...reminders) => {
dismissNotices = (...notices: Notice[]) => {
this.set((state) => {
for (let reminder of reminders) {
state.notices.splice(state.notices.indexOf(reminder), 1);
for (const notice of notices) {
state.notices.splice(state.notices.indexOf(notice), 1);
}
});
};
addToShortcuts = async (item) => {
if (db.shortcuts.exists(item.id)) {
addToShortcuts = async (item: { type: "tag" | "notebook"; id: string }) => {
if (await db.shortcuts.exists(item.id)) {
await db.shortcuts.remove(item.id);
this.refreshNavItems();
showToast("success", `Shortcut removed!`);
} else {
await db.shortcuts.add({
item: {
type: item.type,
id: item.id,
notebookId: item.notebookId
}
itemType: item.type,
itemId: item.id
});
this.refreshNavItems();
showToast("success", `Shortcut created!`);
@@ -255,10 +253,6 @@ class AppStore extends BaseStore {
notebookStore.refresh();
break;
}
case "topic": {
notebookStore.setSelectedNotebook(item.notebookId);
break;
}
case "tag": {
tagStore.refresh();
break;
@@ -273,7 +267,7 @@ class AppStore extends BaseStore {
this.set((state) => (state.lastSynced = lastSynced));
};
sync = async (full = true, force = false, lastSynced = null) => {
sync = async (full = true, force = false, lastSynced?: number) => {
if (
this.isSyncing() ||
!this.get().isSyncEnabled ||
@@ -291,7 +285,6 @@ class AppStore extends BaseStore {
if (this.isSyncing()) pendingSync = { full };
return;
}
pendingSync = false;
clearTimeout(syncStatusTimeout);
this.updateLastSynced();
@@ -311,13 +304,20 @@ class AppStore extends BaseStore {
if (pendingSync) {
logger.info("Running pending sync", pendingSync);
await this.get().sync(pendingSync.full, false);
const isFullSync = pendingSync.full;
pendingSync = undefined;
await this.get().sync(isFullSync, false);
}
} catch (err) {
if (!(err instanceof Error)) {
console.error(err);
return;
}
logger.error(err);
if (err.cause === "MERGE_CONFLICT") {
if (editorstore.get().session.id)
editorstore.openSession(editorstore.get().session.id, true);
const sessionId = editorstore.get().session.id;
if (sessionId) await editorstore.openSession(sessionId, true);
await this.refresh();
this.updateSyncStatus("conflicts");
} else {
@@ -334,26 +334,24 @@ class AppStore extends BaseStore {
}
};
abortSync = async (status = "offline") => {
abortSync = async (status: SyncState = "offline") => {
if (this.isSyncing()) this.updateSyncStatus("failed");
else this.updateSyncStatus(status);
await db.syncer.stop();
};
/**
*
* @param {"synced" | "syncing" | "conflicts" | "failed" | "completed" | "offline" | "disabled"} key
*/
updateSyncStatus = (key, reset = false) => {
updateSyncStatus = (key: SyncState, reset = false) => {
logger.info(`Sync status updated: ${key}`);
this.set((state) => (state.syncStatus = { key }));
this.set((state) => {
state.syncStatus = { key, progress: null, type: null };
});
if (reset) {
syncStatusTimeout = setTimeout(() => {
if (this.get().syncStatus.key === key)
this.updateSyncStatus("synced", false);
}, 3000);
}, 3000) as unknown as number;
}
};

View File

@@ -21,7 +21,6 @@ import createStore from "../common/store";
import { store as noteStore } from "./note-store";
import { store as attachmentStore } from "./attachment-store";
import { store as appStore } from "./app-store";
import { store as tagStore } from "./tag-store";
import { store as settingStore } from "./setting-store";
import { db } from "../common/db";
import BaseStore from ".";
@@ -30,59 +29,49 @@ import { hashNavigate } from "../navigation";
import { logger } from "../utils/logger";
import Config from "../utils/config";
import { setDocumentTitle } from "../utils/dom";
import { Note, Tag } from "@notesnook/core";
import { NoteContent } from "@notesnook/core/dist/collections/session-content";
import { Context } from "../components/list-container/types";
const SESSION_STATES = {
stale: "stale",
new: "new",
locked: "locked",
unlocked: "unlocked",
opening: "opening"
};
enum SaveState {
NotSaved = -1,
Saving = 0,
Saved = 1
}
enum SESSION_STATES {
stale = "stale",
new = "new",
locked = "locked",
unlocked = "unlocked",
opening = "opening"
}
export const getDefaultSession = (sessionId = Date.now()) => {
type EditorSession = {
sessionType: "default" | "locked";
content?: NoteContent<false>;
isDeleted: boolean;
attachmentsLength: number;
saveState: SaveState;
sessionId: string;
state: SESSION_STATES;
context?: Context;
nonce?: string;
} & Partial<Note>;
export const getDefaultSession = (sessionId?: string): EditorSession => {
return {
sessionType: "default",
readonly: false,
state: undefined,
saveState: 1, // -1 = not saved, 0 = saving, 1 = saved
sessionId,
/**
* @type {string | undefined}
*/
contentId: undefined,
/**
* @type {any[]}
*/
notebooks: undefined,
title: "",
/**
* @type {string | undefined}
*/
id: undefined,
pinned: false,
localOnly: false,
favorite: false,
locked: false,
// tags: [],
context: undefined,
// color: undefined,
dateEdited: 0,
state: SESSION_STATES.new,
saveState: SaveState.Saved, // -1 = not saved, 0 = saving, 1 = saved
sessionId: sessionId || Date.now().toString(),
attachmentsLength: 0,
isDeleted: false,
/**
* @type {{data: string; type: "tiptap"} | undefined}
*/
content: undefined
isDeleted: false
};
};
/**
* @extends {BaseStore<EditorStore>}
*/
class EditorStore extends BaseStore {
class EditorStore extends BaseStore<EditorStore> {
session = getDefaultSession();
tags = [];
tags: Tag[] = [];
color = undefined;
arePropertiesVisible = false;
editorMargins = Config.get("editor:margins", true);
@@ -98,16 +87,13 @@ class EditorStore extends BaseStore {
});
};
refreshTags = () => {
this.set((state) => {
if (!state.session.id) return;
console.log(
db.relations.to({ id: state.session.id, type: "note" }, "tag")
);
state.tags = db.relations.to(
{ id: state.session.id, type: "note" },
"tag"
);
refreshTags = async () => {
const { session } = this.get();
if (!session.id) return;
this.set({
tags: await db.relations
.to({ id: session.id, type: "note" }, "tag")
.resolve()
});
};
@@ -116,7 +102,7 @@ class EditorStore extends BaseStore {
if (sessionId && !db.notes.note(sessionId)) await this.clearSession();
}
updateSession = async (item) => {
updateSession = async (item: Note) => {
this.set((state) => {
state.session.title = item.title;
state.session.pinned = item.pinned;
@@ -129,10 +115,10 @@ class EditorStore extends BaseStore {
this.refreshTags();
};
openLockedSession = async (note) => {
openLockedSession = async (note: Note) => {
this.set((state) => {
state.session = {
...getDefaultSession(note.dateEdited),
...getDefaultSession(note.dateEdited.toString()),
...note,
sessionType: "locked",
id: undefined, // NOTE: we give a session id only after the note is opened.
@@ -143,10 +129,10 @@ class EditorStore extends BaseStore {
hashNavigate(`/notes/${note.id}/edit`, { replace: true });
};
openSession = async (noteId, force) => {
openSession = async (noteId: string, force = false) => {
const session = this.get().session;
if (session.id) await db.fs().cancel(session.id);
if (session.id) await db.fs().cancel(session.id, "download");
if (session.id === noteId && !force) return;
if (session.state === SESSION_STATES.unlocked) {
@@ -157,7 +143,7 @@ class EditorStore extends BaseStore {
return;
}
const note = db.notes.note(noteId)?.data || db.notes.trashed(noteId);
const note = await db.notes.note(noteId); // TODO: || db.notes.trashed(noteId);
if (!note) return;
noteStore.setSelectedNote(note.id);
@@ -168,28 +154,36 @@ class EditorStore extends BaseStore {
if (note.conflicted)
return hashNavigate(`/notes/${noteId}/conflict`, { replace: true });
const content = await db.content.raw(note.contentId);
const content = note.contentId
? await db.content.get(note.contentId)
: undefined;
if (content && content.locked)
return hashNavigate(`/notes/${noteId}/unlock`, { replace: true });
this.set((state) => {
const defaultSession = getDefaultSession(note.dateEdited);
const defaultSession = getDefaultSession(note.dateEdited.toString());
state.session = {
...defaultSession,
...note,
content,
state: SESSION_STATES.new,
attachmentsLength: db.attachments.ofNote(note.id, "all")?.length || 0
attachmentsLength: 0 // TODO: db.attachments.ofNote(note.id, "all")?.length || 0
};
const isDeleted = note.type === "trash";
if (isDeleted) {
state.session.isDeleted = true;
state.session.readonly = true;
}
// TODO: const isDeleted = note.type === "trash";
// if (isDeleted) {
// state.session.isDeleted = true;
// state.session.readonly = true;
// }
});
appStore.setIsEditorOpen(true);
this.toggleProperties(false);
};
saveSession = async (sessionId, session) => {
saveSession = async (
sessionId: string | undefined,
session: Partial<EditorSession>
) => {
if (!session) {
logger.warn("Session cannot be undefined", { sessionId, session });
return;
@@ -204,32 +198,43 @@ class EditorStore extends BaseStore {
try {
if (session.content) this.get().session.content = session.content;
const id = await this._getSaveFn()({ ...session, id: sessionId });
const id =
currentSession.locked && sessionId
? await db.vault.save({ ...session, id: sessionId })
: await db.notes.add({ ...session, id: sessionId });
if (currentSession && currentSession.id !== sessionId) {
noteStore.refresh();
throw new Error("Aborting save operation: old session.");
}
if (!id) throw new Error("Note not saved.");
let note = db.notes.note(id)?.data;
if (!note) throw new Error("Note not saved.");
// let note = await db.notes.note(id);
// if (!note) throw new Error("Note not saved.");
if (!sessionId) {
noteStore.setSelectedNote(id);
hashNavigate(`/notes/${id}/edit`, { replace: true, notify: false });
}
const defaultNotebook = db.settings.getDefaultNotebook();
if (currentSession.context) {
const { type, value } = currentSession.context;
if (type === "topic" || type === "notebook")
await db.notes.addToNotebook(value, id);
const { type } = currentSession.context;
if (type === "notebook")
await db.notes.addToNotebook(currentSession.context.id, id);
else if (type === "color" || type === "tag")
await db.relations.add({ type, id: value }, { id, type: "note" });
// update the note.
note = db.notes.note(id)?.data;
} else if (!sessionId && db.settings.getDefaultNotebook()) {
await db.notes.addToNotebook(db.settings.getDefaultNotebook(), id);
await db.relations.add(
{ type, id: currentSession.context.id },
{ id, type: "note" }
);
} else if (!sessionId && defaultNotebook) {
await db.notes.addToNotebook(defaultNotebook, id);
}
console.log("getting note");
const note = await db.notes.note(id);
if (!note) throw new Error("Note not saved.");
const shouldRefreshNotes =
currentSession.context ||
!sessionId ||
@@ -237,7 +242,7 @@ class EditorStore extends BaseStore {
note.headline !== currentSession.headline;
if (shouldRefreshNotes) noteStore.refresh();
const attachments = db.attachments.ofNote(id, "all");
const attachments = await db.attachments.ofNote(id, "all");
if (attachments.length !== currentSession.attachmentsLength) {
attachmentStore.refresh();
}
@@ -245,13 +250,14 @@ class EditorStore extends BaseStore {
this.set((state) => {
if (!!state.session.id && state.session.id !== note.id) return;
for (let key in session) {
for (const key in session) {
if (key === "content") continue;
state.session[key] = session[key];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
state.session[key] = session[key as keyof EditorSession];
}
state.session.notebooks = note.notebooks;
state.session.context = null;
state.session.context = undefined;
state.session.id = note.id;
state.session.title = note.title;
state.session.dateEdited = note.dateEdited;
@@ -264,17 +270,18 @@ class EditorStore extends BaseStore {
this.setSaveState(1);
} catch (err) {
this.setSaveState(-1);
logger.error(err);
console.error(err);
if (err instanceof Error) logger.error(err);
if (currentSession.locked) {
hashNavigate(`/notes/${session.id}/unlock`, { replace: true });
}
}
};
newSession = async (nonce) => {
let context = noteStore.get().context;
newSession = async (nonce?: string) => {
const context = noteStore.get().context;
const session = this.get().session;
if (session.id) await db.fs().cancel(session.id);
if (session.id) await db.fs().cancel(session.id, "download");
this.set((state) => {
state.session = {
@@ -284,14 +291,14 @@ class EditorStore extends BaseStore {
state: SESSION_STATES.new
};
});
noteStore.setSelectedNote(0);
noteStore.setSelectedNote();
appStore.setIsEditorOpen(true);
setDocumentTitle();
};
clearSession = async (shouldNavigate = true) => {
const session = this.get().session;
if (session.id) await db.fs().cancel(session.id);
if (session.id) await db.fs().cancel(session.id, "download");
this.set((state) => {
state.session = {
@@ -300,7 +307,7 @@ class EditorStore extends BaseStore {
};
});
setTimeout(() => {
noteStore.setSelectedNote(0);
noteStore.setSelectedNote();
this.toggleProperties(false);
if (shouldNavigate)
hashNavigate(`/notes/create`, { replace: true, addNonce: true });
@@ -309,38 +316,39 @@ class EditorStore extends BaseStore {
}, 100);
};
setTitle = (noteId, title) => {
setTitle = (noteId: string | undefined, title: string) => {
return this.saveSession(noteId, { title });
};
toggle = (noteId, name, value) => {
toggle = (
noteId: string,
name: "favorite" | "pinned" | "readonly" | "localOnly" | "color",
value: boolean | string
) => {
return this.saveSession(noteId, { [name]: value });
};
/**
*
* @param {*} noteId
* @param {*} sessionId
* @param {boolean} ignoreEdit if this is set to true, we save the content but do not update the dateEdited. Useful for metadata only changes in content.
* @param {*} content
* @returns
*/
saveSessionContent = (noteId, sessionId, ignoreEdit, content) => {
saveSessionContent = (
noteId: string | undefined,
sessionId: string,
ignoreEdit: boolean,
content: NoteContent<false>
) => {
const dateEdited = ignoreEdit ? this.get().session.dateEdited : undefined;
return this.saveSession(noteId, { sessionId, content, dateEdited });
};
setTag = (tag) => {
setTag = (tag: string) => {
return this._setTag(tag);
};
setSaveState = (saveState) => {
setSaveState = (saveState: SaveState) => {
this.set((state) => {
state.session.saveState = saveState;
});
};
toggleProperties = (toggleState) => {
toggleProperties = (toggleState: boolean) => {
this.set(
(state) =>
(state.arePropertiesVisible =
@@ -348,7 +356,7 @@ class EditorStore extends BaseStore {
);
};
toggleEditorMargins = (toggleState) => {
toggleEditorMargins = (toggleState: boolean) => {
this.set((state) => {
state.editorMargins =
toggleState !== undefined ? toggleState : !state.editorMargins;
@@ -356,38 +364,30 @@ class EditorStore extends BaseStore {
});
};
/**
* @private internal
* @param {Boolean} isLocked
* @returns {(note: any) => Promise<string>}
*/
_getSaveFn = () => {
return this.get().session.locked
? db.vault.save.bind(db.vault)
: db.notes.add.bind(db.notes);
};
// _getSaveFn = () => {
// return this.get().session.locked
// ? db.vault.save.bind(db.vault)
// : db.notes.add.bind(db.notes);
// };
async _setTag(value) {
const {
tags,
session: { id }
} = this.get();
let note = db.notes.note(id);
if (!note) return;
let tag = tags.find((t) => t.title === value);
if (tag) {
await db.relations.unlink(tag, note._note);
appStore.refreshNavItems();
} else {
const id = await db.tags.add({ title: value });
await db.relations.add({ id, type: "tag" }, note._note);
}
this.refreshTags();
tagStore.refresh();
noteStore.refresh();
async _setTag(value: string) {
// const {
// tags,
// session: { id }
// } = this.get();
// let note = db.notes.note(id);
// if (!note) return;
// let tag = tags.find((t) => t.title === value);
// if (tag) {
// await db.relations.unlink(tag, note._note);
// appStore.refreshNavItems();
// } else {
// const id = await db.tags.add({ title: value });
// await db.relations.add({ id, type: "tag" }, note._note);
// }
// this.refreshTags();
// tagStore.refresh();
// noteStore.refresh();
}
}

View File

@@ -1,230 +0,0 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
import { db } from "../common/db";
import createStore from "../common/store";
import { store as editorStore } from "./editor-store";
import { store as appStore } from "./app-store";
import { store as selectionStore } from "./selection-store";
import Vault from "../common/vault";
import BaseStore from ".";
import Config from "../utils/config";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
/**
* @extends {BaseStore<NoteStore>}
*/
class NoteStore extends BaseStore {
notes = [];
/**
* @type {import("../components/list-container/types").Context | undefined}
*/
context = undefined;
selectedNote = "";
nonce = 0;
viewMode = Config.get("notes:viewMode", "detailed");
setViewMode = (viewMode) => {
this.set((state) => (state.viewMode = viewMode));
Config.set("notes:viewMode", viewMode);
};
setSelectedNote = (id) => {
if (!id) selectionStore.get().toggleSelectionMode(false);
this.set((state) => (state.selectedNote = id));
};
refresh = () => {
this.get().notes = groupArray(
db.notes.all,
db.settings.getGroupOptions("home")
);
this._forceUpdate();
this.refreshContext();
};
refreshItem = (id, newNote) => {
const notes = this.get().notes;
const index = notes.findIndex((n) => n.id === id);
if (index <= -1) return;
newNote = newNote || db.notes.note(id).data;
notes[index] = newNote;
this._forceUpdate();
};
refreshContext = () => {
const context = this.get().context;
if (!context) return;
this.setContext(context);
};
clearContext = () => {
this.set((state) => {
state.context = undefined;
});
};
setContext = (context) => {
this.get().context = { ...context, notes: notesFromContext(context) };
this._forceUpdate();
};
delete = async (...ids) => {
const { session, clearSession } = editorStore.get();
for (let id of ids) {
if (session && session.id === id) {
await clearSession();
}
}
await db.notes.delete(...ids);
this.refresh();
appStore.refreshNavItems();
};
pin = async (id) => {
const note = db.notes.note(id);
await note.pin();
this._syncEditor(note.id, "pinned", !note.data.pinned);
this.refresh();
};
favorite = async (id) => {
const note = db.notes.note(id);
await note.favorite();
this._syncEditor(note.id, "favorite", !note.data.favorite);
this.refreshItem(id);
};
unlock = async (id) => {
return await Vault.unlockNote(id).then(async (res) => {
if (editorStore.get().session.id === id)
await editorStore.clearSession(true);
this.refreshItem(id);
return res;
});
};
lock = async (id) => {
if (!(await Vault.lockNote(id))) return false;
this.refreshItem(id);
if (editorStore.get().session.id === id)
await editorStore.openSession(id, true);
return true;
};
readonly = async (id) => {
const note = db.notes.note(id);
await note.readonly();
this._syncEditor(note.id, "readonly", !note.data.readonly);
this.refreshItem(id);
};
duplicate = async (note) => {
const id = await db.notes.note(note).duplicate();
this.refresh();
return id;
};
localOnly = async (id) => {
const note = db.notes.note(id);
await note.localOnly();
this._syncEditor(note.id, "localOnly", !note.data.localOnly);
this.refreshItem(id);
};
setColor = async (id, color) => {
try {
let note = db.notes.note(id);
if (!note) return;
const colorId =
db.tags.find(color)?.id || (await db.colors.add({ title: color }));
const isColored =
db.relations.from({ type: "color", id: colorId }, "note").length > 0;
if (isColored)
await db.relations.unlink({ type: "color", id: colorId }, note._note);
else await db.relations.add({ type: "color", id: colorId }, note._note);
appStore.refreshNavItems();
this._syncEditor(note.id, "color", color);
this.refreshItem(id);
} catch (e) {
console.error(e);
}
};
/**
* @private
*/
_syncEditor = (noteId, action, value) => {
const { session, toggle } = editorStore.get();
if (session.id !== noteId) return false;
toggle(session.id, action, value);
return true;
};
/**
* @private
*/
_forceUpdate = () => {
this.set((state) => {
state.nonce++;
});
};
}
const [useStore, store] = createStore(NoteStore);
export { useStore, store };
function notesFromContext(context) {
let notes = [];
switch (context.type) {
case "tag":
case "color":
notes = db.relations
.from({ type: context.type, id: context.value }, "note")
.resolved();
break;
case "notebook": {
const notebook = db.notebooks.notebook(context?.value?.id);
if (!notebook) break;
notes = db.relations.from(notebook.data, "note").resolved();
break;
}
case "topic": {
const notebook = db.notebooks.notebook(context?.value?.id);
if (!notebook) break;
const topic = notebook.topics?.topic(context?.value?.topic);
if (!topic) break;
notes = topic.all;
break;
}
case "favorite":
notes = db.notes.favorites;
break;
case "monographs":
notes = db.monographs.all;
break;
default:
return [];
}
return notes;
}

View File

@@ -0,0 +1,179 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
import { db } from "../common/db";
import createStore from "../common/store";
import { store as editorStore } from "./editor-store";
import { store as appStore } from "./app-store";
import { store as selectionStore } from "./selection-store";
import Vault from "../common/vault";
import BaseStore from ".";
import Config from "../utils/config";
import { Note, VirtualizedGrouping } from "@notesnook/core";
import { Context } from "../components/list-container/types";
type ViewMode = "detailed" | "compact";
class NoteStore extends BaseStore<NoteStore> {
notes: VirtualizedGrouping<Note> | undefined = undefined;
contextNotes: VirtualizedGrouping<Note> | undefined = undefined;
context: Context | undefined = undefined;
selectedNote?: string;
// nonce = 0;
viewMode: ViewMode = Config.get("notes:viewMode", "detailed");
setViewMode = (viewMode: ViewMode) => {
this.set((state) => (state.viewMode = viewMode));
Config.set("notes:viewMode", viewMode);
};
setSelectedNote = (id?: string) => {
if (!id) selectionStore.get().toggleSelectionMode(false);
this.set({ selectedNote: id });
};
refresh = async () => {
const grouping = await db.notes.all.grouped(
db.settings.getGroupOptions("home")
);
this.set((state) => {
state.notes = grouping;
});
await this.refreshContext();
};
refreshContext = async () => {
const context = this.get().context;
if (!context) return;
await this.setContext(context);
};
setContext = async (context?: Context) => {
this.set({
context,
contextNotes: context
? await notesFromContext(context).grouped(
db.settings.getGroupOptions(
context.type === "favorite" ? "favorites" : "notes"
)
)
: undefined
});
};
delete = async (...ids: string[]) => {
const { session, clearSession } = editorStore.get();
if (session.id && ids.indexOf(session.id) > -1) await clearSession();
await db.notes.moveToTrash(...ids);
await this.refresh();
};
pin = async (state: boolean, ...ids: string[]) => {
await db.notes.pin(state, ...ids);
this.syncNoteWithEditor(ids, "pinned", state);
await this.refresh();
};
favorite = async (state: boolean, ...ids: string[]) => {
await db.notes.favorite(state, ...ids);
this.syncNoteWithEditor(ids, "favorite", state);
await this.refresh();
};
unlock = async (id: string) => {
return await Vault.unlockNote(id).then(async (res) => {
if (editorStore.get().session.id === id)
await editorStore.openSession(id);
await this.refresh();
return res;
});
};
lock = async (id: string) => {
if (!(await Vault.lockNote(id))) return false;
await this.refresh();
if (editorStore.get().session.id === id)
await editorStore.openSession(id, true);
return true;
};
readonly = async (state: boolean, ...ids: string[]) => {
await db.notes.readonly(state, ...ids);
this.syncNoteWithEditor(ids, "readonly", state);
await this.refresh();
};
duplicate = async (...ids: string[]) => {
await db.notes.duplicate(...ids);
await this.refresh();
};
localOnly = async (state: boolean, ...ids: string[]) => {
await db.notes.localOnly(state, ...ids);
this.syncNoteWithEditor(ids, "localOnly", state);
await this.refresh();
};
setColor = async (color: string, ...ids: string[]) => {
// try {
// let note = db.notes.note(id);
// if (!note) return;
// const colorId =
// db.tags.find(color)?.id || (await db.colors.add({ title: color }));
// const isColored =
// db.relations.from({ type: "color", id: colorId }, "note").length > 0;
// if (isColored)
// await db.relations.unlink({ type: "color", id: colorId }, note._note);
// else
// await db.relations.add({ type: "color", id: colorId }, note._note);
// const notes = await db.notes.all.items(ids)
// TODO:
await appStore.refreshNavItems();
this.syncNoteWithEditor(ids, "color", color);
await this.refresh();
};
private syncNoteWithEditor = (
noteIds: string[],
action: "favorite" | "pinned" | "readonly" | "localOnly" | "color",
value: boolean | string
) => {
const { session, toggle } = editorStore.get();
if (!session.id || !noteIds.includes(session.id)) return false;
toggle(session.id, action, value);
return true;
};
}
const [useStore, store] = createStore(NoteStore);
export { useStore, store };
function notesFromContext(context: Context) {
switch (context.type) {
case "notebook":
case "tag":
case "color":
return db.relations.from({ type: context.type, id: context.id }, "note")
.selector;
case "favorite":
return db.notes.favorites;
case "monographs":
return db.monographs.all;
}
}

View File

@@ -1,84 +0,0 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
import { db } from "../common/db";
import createStore from "../common/store";
import { store as appStore } from "./app-store";
import { store as noteStore } from "./note-store";
import BaseStore from "./index";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
import Config from "../utils/config";
/**
* @extends {BaseStore<NotebookStore>}
*/
class NotebookStore extends BaseStore {
notebooks = [];
/**
* @type {any | undefined}
*/
selectedNotebook = undefined;
selectedNotebookTopics = [];
viewMode = Config.get("notebooks:viewMode", "detailed");
setViewMode = (viewMode) => {
this.set((state) => (state.viewMode = viewMode));
Config.set("notebooks:viewMode", viewMode);
};
refresh = () => {
this.set((state) => {
state.notebooks = groupArray(
db.notebooks.all,
db.settings.getGroupOptions("notebooks")
);
});
this.setSelectedNotebook(this.get().selectedNotebook?.id);
};
delete = async (...ids) => {
await db.notebooks.delete(...ids);
this.refresh();
appStore.refreshNavItems();
noteStore.refresh();
};
pin = async (notebookId) => {
const notebook = db.notebooks.notebook(notebookId);
await notebook.pin();
this.refresh();
};
setSelectedNotebook = (id) => {
if (!id) return;
const notebook = db.notebooks.notebook(id)?.data;
if (!notebook) return;
this.set((state) => {
state.selectedNotebook = notebook;
state.selectedNotebookTopics = groupArray(
notebook.topics,
db.settings.getGroupOptions("topics")
);
});
};
}
const [useStore, store] = createStore(NotebookStore);
export { useStore, store };

View File

@@ -0,0 +1,75 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
import { db } from "../common/db";
import createStore from "../common/store";
import { store as appStore } from "./app-store";
import { store as noteStore } from "./note-store";
import BaseStore from "./index";
import Config from "../utils/config";
import { Notebook, VirtualizedGrouping } from "@notesnook/core";
type ViewMode = "detailed" | "compact";
class NotebookStore extends BaseStore<NotebookStore> {
notebooks: VirtualizedGrouping<Notebook> | undefined = undefined;
// selectedNotebook = undefined;
// selectedNotebookTopics = [];
viewMode = Config.get<ViewMode>("notebooks:viewMode", "detailed");
setViewMode = (viewMode: ViewMode) => {
this.set((state) => (state.viewMode = viewMode));
Config.set("notebooks:viewMode", viewMode);
};
refresh = async () => {
const notebooks = await db.notebooks.all.grouped(
db.settings.getGroupOptions("notebooks")
);
this.set({ notebooks });
};
delete = async (...ids: string[]) => {
await db.notebooks.moveToTrash(...ids);
await this.refresh();
await appStore.refreshNavItems();
await noteStore.refresh();
};
pin = async (state: boolean, ...ids: string[]) => {
await db.notebooks.pin(state, ...ids);
await this.refresh();
};
// setSelectedNotebook = (id) => {
// if (!id) return;
// const notebook = db.notebooks.notebook(id)?.data;
// if (!notebook) return;
// this.set((state) => {
// state.selectedNotebook = notebook;
// state.selectedNotebookTopics = groupArray(
// notebook.topics,
// db.settings.getGroupOptions("topics")
// );
// });
// };
}
const [useStore, store] = createStore(NotebookStore);
export { useStore, store };

View File

@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import createStore from "../common/store";
import { db } from "../common/db";
import BaseStore from "./index";
import { groupReminders } from "@notesnook/core/dist/utils/grouping";
import { TaskScheduler } from "../utils/task-scheduler";
import { showReminderPreviewDialog } from "../common/dialog-controller";
import dayjs from "dayjs";
@@ -36,18 +35,12 @@ class ReminderStore extends BaseStore {
reminders = [];
refresh = (reset = true) => {
const reminders = db.reminders.all;
this.set(
(state) =>
(state.reminders = groupReminders(
reminders,
db.settings.getGroupOptions("reminders")
))
);
if (reset) {
resetReminders(reminders);
notestore.refresh();
}
// const reminders = db.reminders.all;
// this.set((state) => (state.reminders = groupReminders(reminders)));
// if (reset) {
// resetReminders(reminders);
// notestore.refresh();
// }
};
delete = async (...ids) => {

View File

@@ -39,7 +39,8 @@ class SelectionStore extends BaseStore {
};
selectItem = (item) => {
const index = this.get().selectedItems.findIndex((v) => item.id === v.id);
console.log(this.get().selectedItems, item);
const index = this.get().selectedItems.findIndex((v) => item === v);
this.set((state) => {
if (index <= -1) {
state.selectedItems.push(item);
@@ -49,7 +50,7 @@ class SelectionStore extends BaseStore {
deselectItem = (item) => {
this.set((state) => {
const index = state.selectedItems.findIndex((v) => item.id === v.id);
const index = state.selectedItems.findIndex((v) => item === v);
if (index >= 0) {
state.selectedItems.splice(index, 1);
}

View File

@@ -20,20 +20,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import createStore from "../common/store";
import { db } from "../common/db";
import BaseStore from "./index";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
import { GroupedItems, Tag } from "@notesnook/core/dist/types";
import { Tag } from "@notesnook/core/dist/types";
import { VirtualizedGrouping } from "@notesnook/core";
class TagStore extends BaseStore<TagStore> {
tags: GroupedItems<Tag> = [];
tags?: VirtualizedGrouping<Tag>;
refresh = () => {
this.set(
(state) =>
(state.tags = groupArray(
db.tags.all || [],
db.settings.getGroupOptions("tags")
))
);
refresh = async () => {
this.set({
tags: await db.tags.all.grouped(db.settings.getGroupOptions("tags"))
});
};
}

View File

@@ -17,35 +17,43 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import CompressorWorker from "./compressor.worker.ts?worker";
import type { Compressor as CompressorWorkerType } from "./compressor.worker";
import { wrap, Remote } from "comlink";
import { desktop } from "../common/desktop-bridge";
import { ICompressor } from "@notesnook/core/dist/interfaces";
import { Foras, gzip, gunzip, Memory } from "@hazae41/foras";
export class Compressor implements ICompressor {
private worker!: globalThis.Worker;
private compressor!: Remote<CompressorWorkerType>;
constructor() {
if (!IS_DESKTOP_APP) {
this.worker = new CompressorWorker();
this.compressor = wrap<CompressorWorkerType>(this.worker);
}
private inititalized = false;
private async init() {
if (this.inititalized) return;
await Foras.initBundledOnce();
this.inititalized = true;
}
async compress(data: string) {
if (IS_DESKTOP_APP && desktop)
return await desktop.compress.gzip.query({ data, level: 6 });
return await this.compressor.gzip({ data, level: 6 });
await this.init();
const bytes = new Memory(new TextEncoder().encode(data));
const res = gzip(bytes, 6);
const base64 = Buffer.from(res.bytes).toString("base64");
res.free();
return base64;
}
async decompress(data: string) {
if (IS_DESKTOP_APP && desktop)
return await desktop.compress.gunzip.query(data);
return await this.compressor.gunzip({ data });
await this.init();
return gunzip(Buffer.from(data, "base64"))
.copyAndDispose()
.toString("utf-8");
// return new Promise<string>((resolve, reject) => {
// gunzip(Buffer.from(data, "base64"), (err, data) =>
// err ? reject(err) : resolve(Buffer.from(data.buffer).toString("utf-8"))
// );
// });
}
}

View File

@@ -25,16 +25,16 @@ import useNavigate from "../hooks/use-navigate";
import Placeholder from "../components/placeholders";
function Home() {
useStore((store) => store.nonce);
const notes = useStore((store) => store.notes);
const isCompact = useStore((store) => store.viewMode === "compact");
const refresh = useStore((store) => store.refresh);
const clearContext = useStore((store) => store.clearContext);
const setContext = useStore((store) => store.setContext);
useNavigate("home", clearContext);
useNavigate("home", setContext);
useEffect(() => {
(async function () {
await refresh();
// const note = db.notes.note("62bc3f28a1a1a10000707077").data;
// const data = await db.content.raw(note.contentId);
// const note2 = db.notes.note("62bc3f1ca1a1a10000707075").data;
@@ -44,8 +44,9 @@ function Home() {
// await db.notes.add({ id: note.id, conflicted: true, resolved: false });
// console.log(data3);
})();
}, []);
}, [refresh]);
if (!notes) return <Placeholder context="notes" />;
return (
<ListContainer
group="home"

View File

@@ -20,14 +20,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import ListContainer from "../components/list-container";
import { useStore, store } from "../stores/notebook-store";
import { hashNavigate } from "../navigation";
import useNavigate from "../hooks/use-navigate";
import Placeholder from "../components/placeholders";
import { useEffect } from "react";
function Notebooks() {
useNavigate("notebooks", () => store.refresh());
// useNavigate("notebooks", () => store.refresh());
const notebooks = useStore((state) => state.notebooks);
const refresh = useStore((state) => state.refresh);
useEffect(() => {
store.get().refresh();
}, []);
if (!notebooks) return <Placeholder context="notebooks" />;
return (
<>
<ListContainer

View File

@@ -21,12 +21,11 @@ import { useEffect } from "react";
import ListContainer from "../components/list-container";
import { useStore as useNotesStore } from "../stores/note-store";
import { hashNavigate, navigate } from "../navigation";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
import { db } from "../common/db";
import Placeholder from "../components/placeholders";
function Notes() {
const context = useNotesStore((store) => store.context);
const contextNotes = useNotesStore((store) => store.contextNotes);
const refreshContext = useNotesStore((store) => store.refreshContext);
const type = context?.type === "favorite" ? "favorites" : "notes";
const isCompact = useNotesStore((store) => store.viewMode === "compact");
@@ -34,25 +33,21 @@ function Notes() {
useEffect(() => {
if (
context?.type === "color" &&
context.notes &&
context.notes.length <= 0
contextNotes &&
contextNotes.ids.length <= 0
) {
navigate("/", true);
}
}, [context]);
}, [context, contextNotes]);
if (!context) return null;
if (!context || !contextNotes) return <Placeholder context="notes" />;
return (
<ListContainer
group={type}
refresh={refreshContext}
compact={isCompact}
context={{ ...context, notes: undefined }}
items={
context.notes
? groupArray(context.notes, db.settings.getGroupOptions(type))
: []
}
context={context}
items={contextNotes}
placeholder={
<Placeholder
context={

View File

@@ -26,6 +26,8 @@ function Tags() {
useNavigate("tags", () => store.refresh());
const tags = useStore((store) => store.tags);
const refresh = useStore((store) => store.refresh);
if (!tags) return <Placeholder context="tags" />;
return (
<ListContainer
group="tags"

View File

@@ -31,7 +31,6 @@ import {
ShortcutLink,
SortAsc
} from "../components/icons";
import { getTotalNotes } from "@notesnook/common";
import { pluralize } from "@notesnook/common";
import { Allotment } from "allotment";
import { Plus } from "../components/icons";
@@ -272,7 +271,7 @@ function NotebookHeader({ notebook }) {
const [isShortcut, setIsShortcut] = useState(false);
const shortcuts = useAppStore((store) => store.shortcuts);
const addToShortcuts = useAppStore((store) => store.addToShortcuts);
const totalNotes = getTotalNotes(notebook);
const totalNotes = 0; // getTotalNotes(notebook);
useEffect(() => {
setIsShortcut(shortcuts.findIndex((p) => p.id === notebook.id) > -1);