mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
web: start initial migration to sqlite
This commit is contained in:
443
apps/web/package-lock.json
generated
443
apps/web/package-lock.json
generated
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -83,10 +83,10 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
|
||||
|
||||
initStore();
|
||||
initAttachments();
|
||||
refreshNavItems();
|
||||
initEditorStore();
|
||||
|
||||
(async function () {
|
||||
await refreshNavItems();
|
||||
await updateLastSynced();
|
||||
if (await initUser()) {
|
||||
showUpgradeReminderDialogs();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
462
apps/web/src/common/sqlite/AccessHandlePoolVFS.js
Normal file
462
apps/web/src/common/sqlite/AccessHandlePoolVFS.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
855
apps/web/src/common/sqlite/IDBBatchAtomicVFS.js
Normal file
855
apps/web/src/common/sqlite/IDBBatchAtomicVFS.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
281
apps/web/src/common/sqlite/IDBContext.js
Normal file
281
apps/web/src/common/sqlite/IDBContext.js
Normal 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);
|
||||
}
|
||||
}
|
||||
191
apps/web/src/common/sqlite/VFS.js
Normal file
191
apps/web/src/common/sqlite/VFS.js
Normal 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);
|
||||
364
apps/web/src/common/sqlite/WebLocks.js
Normal file
364
apps/web/src/common/sqlite/WebLocks.js
Normal 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
85
apps/web/src/common/sqlite/globals.d.ts
vendored
Normal 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
1726
apps/web/src/common/sqlite/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
958
apps/web/src/common/sqlite/sqlite-api.js
Normal file
958
apps/web/src/common/sqlite/sqlite-api.js
Normal 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;
|
||||
}
|
||||
288
apps/web/src/common/sqlite/sqlite-constants.js
Normal file
288
apps/web/src/common/sqlite/sqlite-constants.js
Normal 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;
|
||||
124
apps/web/src/common/sqlite/sqlite.kysely.ts
Normal file
124
apps/web/src/common/sqlite/sqlite.kysely.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
133
apps/web/src/common/sqlite/sqlite.worker.ts
Normal file
133
apps/web/src/common/sqlite/sqlite.worker.ts
Normal 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);
|
||||
@@ -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;
|
||||
114
apps/web/src/common/sqlite/wa-sqlite-async.js
Normal file
114
apps/web/src/common/sqlite/wa-sqlite-async.js
Normal 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;
|
||||
BIN
apps/web/src/common/sqlite/wa-sqlite-async.wasm
Normal file
BIN
apps/web/src/common/sqlite/wa-sqlite-async.wasm
Normal file
Binary file not shown.
110
apps/web/src/common/sqlite/wa-sqlite.js
Normal file
110
apps/web/src/common/sqlite/wa-sqlite.js
Normal 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;
|
||||
BIN
apps/web/src/common/sqlite/wa-sqlite.wasm
Normal file
BIN
apps/web/src/common/sqlite/wa-sqlite.wasm
Normal file
Binary file not shown.
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 = "";
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
// // }
|
||||
// }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
179
apps/web/src/stores/note-store.ts
Normal file
179
apps/web/src/stores/note-store.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
75
apps/web/src/stores/notebook-store.ts
Normal file
75
apps/web/src/stores/notebook-store.ts
Normal 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 };
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))
|
||||
// );
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user