mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-02-23 12:38:47 +00:00
Compare commits
16 Commits
0.25.30-de
...
0.25.35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55ffeeda10 | ||
|
|
65f18b4160 | ||
|
|
b0c1d6a1bf | ||
|
|
ca19f2f2ed | ||
|
|
ac0378ca4b | ||
|
|
f06f8d1eb6 | ||
|
|
77074cb92f | ||
|
|
1274b6f683 | ||
|
|
c54ae58c0f | ||
|
|
3f54921e90 | ||
|
|
1b070c2dd4 | ||
|
|
0e5846b670 | ||
|
|
f81e71802b | ||
|
|
4c761eebff | ||
|
|
bf754d6e07 | ||
|
|
1c49acd5b5 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,3 +21,6 @@ data.json
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
|
||||
# local config files
|
||||
*.local
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"printWidth": 120,
|
||||
"semi": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
20
.prettierrc.mjs
Normal file
20
.prettierrc.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
import { readFileSync } from "fs";
|
||||
let localPrettierConfig = {};
|
||||
|
||||
try {
|
||||
const localConfig = readFileSync(".prettierrc.local", "utf-8");
|
||||
localPrettierConfig = JSON.parse(localConfig);
|
||||
console.log("Using local Prettier config from .prettierrc.local");
|
||||
} catch (e) {
|
||||
// no local config
|
||||
}
|
||||
const prettierConfig = {
|
||||
trailingComma: "es5",
|
||||
tabWidth: 4,
|
||||
printWidth: 120,
|
||||
semi: true,
|
||||
endOfLine: "cr",
|
||||
...localPrettierConfig,
|
||||
};
|
||||
|
||||
export default prettierConfig;
|
||||
@@ -40,7 +40,10 @@ export default [
|
||||
"src/lib/test",
|
||||
"src/lib/src/cli",
|
||||
"**/main.js",
|
||||
"src/lib/apps/webpeer/*"
|
||||
"src/lib/apps/webpeer/*",
|
||||
".prettierrc.*.mjs",
|
||||
".prettierrc.mjs",
|
||||
"*.config.mjs"
|
||||
],
|
||||
},
|
||||
...compat.extends(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.30",
|
||||
"version": "0.25.35",
|
||||
"minAppVersion": "0.9.12",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
241
package-lock.json
generated
241
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.30",
|
||||
"version": "0.25.35",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.30",
|
||||
"version": "0.25.35",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
@@ -1793,6 +1793,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.4.tgz",
|
||||
"integrity": "sha512-pUxEGmR+uu21OG/icAovjlu1fcYJzyVhhT0rsCrn+zi+nHtrS43Bp9KPn9KGa4NMspCUE++nkyiqziuIvJdwzw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.7.0",
|
||||
"@firebase/logger": "0.5.0",
|
||||
@@ -1859,6 +1860,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.4.tgz",
|
||||
"integrity": "sha512-T7ifGmb+awJEcp542Ek4HtNfBxcBrnuk1ggUdqyFEdsXHdq7+wVlhvE6YukTL7NS8hIkEfL7TMAPx/uCNqt30g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@firebase/app": "0.14.4",
|
||||
"@firebase/component": "0.7.0",
|
||||
@@ -1874,7 +1876,8 @@
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
|
||||
"integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
|
||||
"license": "Apache-2.0"
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@firebase/app/node_modules/idb": {
|
||||
"version": "7.1.1",
|
||||
@@ -2343,6 +2346,7 @@
|
||||
"integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
@@ -3200,8 +3204,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.5",
|
||||
@@ -3215,8 +3218,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.5",
|
||||
@@ -3230,8 +3232,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.5",
|
||||
@@ -3245,8 +3246,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.5",
|
||||
@@ -3260,8 +3260,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.5",
|
||||
@@ -3275,8 +3274,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.5",
|
||||
@@ -3290,8 +3288,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.5",
|
||||
@@ -3305,8 +3302,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.5",
|
||||
@@ -3320,8 +3316,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.5",
|
||||
@@ -3335,8 +3330,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.5",
|
||||
@@ -3350,8 +3344,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.5",
|
||||
@@ -3365,8 +3358,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.5",
|
||||
@@ -3380,8 +3372,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.5",
|
||||
@@ -3395,8 +3386,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.5",
|
||||
@@ -3410,8 +3400,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.5",
|
||||
@@ -3425,8 +3414,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.5",
|
||||
@@ -3440,8 +3428,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.5",
|
||||
@@ -3455,8 +3442,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.5",
|
||||
@@ -3470,8 +3456,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.5",
|
||||
@@ -3485,8 +3470,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.5",
|
||||
@@ -3500,8 +3484,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.5",
|
||||
@@ -3515,8 +3498,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
@@ -4378,6 +4360,7 @@
|
||||
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||
"debug": "^4.4.1",
|
||||
@@ -4774,6 +4757,7 @@
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
@@ -5338,6 +5322,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6416,6 +6401,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@@ -6504,6 +6490,7 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -7391,15 +7378,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
@@ -8631,6 +8618,7 @@
|
||||
"resolved": "https://registry.npmjs.org/libp2p/-/libp2p-2.8.11.tgz",
|
||||
"integrity": "sha512-EjkyN0CI6uP+e4OOkEcZvhbZtlwFl4Y0rkkMvDbXmcfILX4E4n/jKE4Ppoc1qhNufxToxVWCMDS2ipniQgiYaw==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@chainsafe/is-ip": "^2.1.0",
|
||||
"@chainsafe/netmask": "^2.0.0",
|
||||
@@ -9520,6 +9508,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -9544,6 +9533,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lilconfig": "^3.1.1"
|
||||
},
|
||||
@@ -10235,7 +10225,6 @@
|
||||
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -10806,8 +10795,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
|
||||
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/sublevel-pouchdb": {
|
||||
"version": "9.0.0",
|
||||
@@ -10878,6 +10866,7 @@
|
||||
"integrity": "sha512-0a/huwc8e2es+7KFi70esqsReRfRbrT8h1cJSY/+z1lF0yKM6TT+//HYu28Yxstr50H7ifaqZRDGd0KuKDxP7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -10940,21 +10929,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-eslint-parser": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.4.0.tgz",
|
||||
@@ -11060,6 +11034,7 @@
|
||||
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.8.2",
|
||||
@@ -11105,7 +11080,6 @@
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
@@ -11123,7 +11097,6 @@
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -11251,6 +11224,7 @@
|
||||
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -11376,6 +11350,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -11563,7 +11538,6 @@
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -11620,8 +11594,7 @@
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
|
||||
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/weald": {
|
||||
"version": "1.1.1",
|
||||
@@ -11950,6 +11923,7 @@
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
@@ -13174,6 +13148,7 @@
|
||||
"version": "0.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.4.tgz",
|
||||
"integrity": "sha512-pUxEGmR+uu21OG/icAovjlu1fcYJzyVhhT0rsCrn+zi+nHtrS43Bp9KPn9KGa4NMspCUE++nkyiqziuIvJdwzw==",
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@firebase/component": "0.7.0",
|
||||
"@firebase/logger": "0.5.0",
|
||||
@@ -13227,6 +13202,7 @@
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.4.tgz",
|
||||
"integrity": "sha512-T7ifGmb+awJEcp542Ek4HtNfBxcBrnuk1ggUdqyFEdsXHdq7+wVlhvE6YukTL7NS8hIkEfL7TMAPx/uCNqt30g==",
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@firebase/app": "0.14.4",
|
||||
"@firebase/component": "0.7.0",
|
||||
@@ -13238,7 +13214,8 @@
|
||||
"@firebase/app-types": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
|
||||
"integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw=="
|
||||
"integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
|
||||
"peer": true
|
||||
},
|
||||
"@firebase/auth": {
|
||||
"version": "1.11.0",
|
||||
@@ -13566,6 +13543,7 @@
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz",
|
||||
"integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==",
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -14224,176 +14202,154 @@
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
||||
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
|
||||
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
|
||||
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
|
||||
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
|
||||
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
|
||||
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
|
||||
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
|
||||
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
|
||||
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
|
||||
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
|
||||
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
@@ -15023,6 +14979,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.1.tgz",
|
||||
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||
"debug": "^4.4.1",
|
||||
@@ -15375,6 +15332,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz",
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
@@ -15747,7 +15705,8 @@
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"acorn-jsx": {
|
||||
"version": "5.3.2",
|
||||
@@ -16475,6 +16434,7 @@
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@esbuild/aix-ppc64": "0.25.0",
|
||||
"@esbuild/android-arm": "0.25.0",
|
||||
@@ -16538,6 +16498,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -17153,14 +17114,14 @@
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
@@ -17978,6 +17939,7 @@
|
||||
"version": "2.8.11",
|
||||
"resolved": "https://registry.npmjs.org/libp2p/-/libp2p-2.8.11.tgz",
|
||||
"integrity": "sha512-EjkyN0CI6uP+e4OOkEcZvhbZtlwFl4Y0rkkMvDbXmcfILX4E4n/jKE4Ppoc1qhNufxToxVWCMDS2ipniQgiYaw==",
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@chainsafe/is-ip": "^2.1.0",
|
||||
"@chainsafe/netmask": "^2.0.0",
|
||||
@@ -18580,6 +18542,7 @@
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -18591,6 +18554,7 @@
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
||||
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"lilconfig": "^3.1.1"
|
||||
}
|
||||
@@ -19104,7 +19068,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
||||
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.52.5",
|
||||
"@rollup/rollup-android-arm64": "4.52.5",
|
||||
@@ -19469,8 +19432,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
|
||||
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"sublevel-pouchdb": {
|
||||
"version": "9.0.0",
|
||||
@@ -19529,6 +19491,7 @@
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.41.1.tgz",
|
||||
"integrity": "sha512-0a/huwc8e2es+7KFi70esqsReRfRbrT8h1cJSY/+z1lF0yKM6TT+//HYu28Yxstr50H7ifaqZRDGd0KuKDxP7w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -19565,14 +19528,6 @@
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -19610,6 +19565,7 @@
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
|
||||
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.8.2",
|
||||
@@ -19645,7 +19601,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
@@ -19656,7 +19611,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {}
|
||||
},
|
||||
"picomatch": {
|
||||
@@ -19751,6 +19705,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
|
||||
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"esbuild": "~0.25.0",
|
||||
"fsevents": "~2.3.3",
|
||||
@@ -19833,7 +19788,8 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"uint8-varint": {
|
||||
"version": "2.0.4",
|
||||
@@ -19934,7 +19890,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {}
|
||||
},
|
||||
"picomatch": {
|
||||
@@ -19963,8 +19918,7 @@
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
|
||||
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"weald": {
|
||||
"version": "1.1.1",
|
||||
@@ -20189,7 +20143,8 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "17.7.2",
|
||||
|
||||
10
package.json
10
package.json
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.30",
|
||||
"version": "0.25.35",
|
||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"bakei18n": "npx tsx ./src/lib/_tools/bakei18n.ts",
|
||||
"bakei18n": "npm run i18n:yaml2json && npm run i18n:bakejson",
|
||||
"i18n:bakejson": "npx tsx ./src/lib/_tools/bakei18n.ts",
|
||||
"i18n:yaml2json": "npx tsx ./src/lib/_tools/yaml2json.ts",
|
||||
"i18n:json2yaml": "npx tsx ./src/lib/_tools/json2yaml.ts",
|
||||
"prettyjson": "prettier --config ./.prettierrc ./src/lib/src/common/messagesJson/*.json --write --log-level error",
|
||||
"postbakei18n": "prettier --config ./.prettierrc ./src/lib/src/common/messages/*.ts --write --log-level error",
|
||||
"prettyjson": "prettier --config ./.prettierrc.mjs ./src/lib/src/common/messagesJson/*.json --write --log-level error",
|
||||
"postbakei18n": "prettier --config ./.prettierrc.mjs ./src/lib/src/common/messages/*.ts --write --log-level error",
|
||||
"posti18n:yaml2json": "npm run prettyjson",
|
||||
"predev": "npm run bakei18n",
|
||||
"dev": "node --env-file=.env esbuild.config.mjs",
|
||||
@@ -22,7 +22,7 @@
|
||||
"tsc-check": "tsc --noEmit",
|
||||
"pretty": "npm run prettyNoWrite -- --write --log-level error",
|
||||
"prettyCheck": "npm run prettyNoWrite -- --check",
|
||||
"prettyNoWrite": "prettier --config ./.prettierrc \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
|
||||
"prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
|
||||
"check": "npm run lint && npm run svelte-check",
|
||||
"unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/"
|
||||
},
|
||||
|
||||
@@ -23,6 +23,8 @@ export const EVENT_REQUEST_RUN_DOCTOR = "request-run-doctor";
|
||||
export const EVENT_REQUEST_RUN_FIX_INCOMPLETE = "request-run-fix-incomplete";
|
||||
export const EVENT_ON_UNRESOLVED_ERROR = "on-unresolved-error";
|
||||
|
||||
export const EVENT_ANALYSE_DB_USAGE = "analyse-db-usage";
|
||||
export const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size";
|
||||
// export const EVENT_FILE_CHANGED = "file-changed";
|
||||
|
||||
declare global {
|
||||
@@ -42,6 +44,8 @@ declare global {
|
||||
[EVENT_REQUEST_RUN_DOCTOR]: string;
|
||||
[EVENT_REQUEST_RUN_FIX_INCOMPLETE]: undefined;
|
||||
[EVENT_ON_UNRESOLVED_ERROR]: undefined;
|
||||
[EVENT_ANALYSE_DB_USAGE]: undefined;
|
||||
[EVENT_REQUEST_CHECK_REMOTE_SIZE]: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ import {
|
||||
type DocumentID,
|
||||
type EntryDoc,
|
||||
type EntryLeaf,
|
||||
type FilePathWithPrefix,
|
||||
type MetaEntry,
|
||||
} from "../../lib/src/common/types";
|
||||
import { getNoFromRev } from "../../lib/src/pouchdb/LiveSyncLocalDB";
|
||||
import { LiveSyncCommands } from "../LiveSyncCommands";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock_v2";
|
||||
import { arrayToChunkedArray } from "octagonal-wheels/collection";
|
||||
import { EVENT_ANALYSE_DB_USAGE, eventHub } from "@/common/events";
|
||||
const DB_KEY_SEQ = "gc-seq";
|
||||
const DB_KEY_CHUNK_SET = "chunk-set";
|
||||
const DB_KEY_DOC_USAGE_MAP = "doc-usage-map";
|
||||
@@ -27,6 +29,15 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands {
|
||||
}
|
||||
onload(): void | Promise<void> {
|
||||
// NO OP.
|
||||
this.plugin.addCommand({
|
||||
id: "analyse-database",
|
||||
name: "Analyse Database Usage (advanced)",
|
||||
icon: "database-search",
|
||||
callback: async () => {
|
||||
await this.analyseDatabase();
|
||||
},
|
||||
});
|
||||
eventHub.onEvent(EVENT_ANALYSE_DB_USAGE, () => this.analyseDatabase());
|
||||
}
|
||||
async allChunks(includeDeleted: boolean = false) {
|
||||
const p = this._progress("", LOG_LEVEL_NOTICE);
|
||||
@@ -485,4 +496,216 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
const kvDB = this.plugin.kvDB;
|
||||
await kvDB.set(DB_KEY_CHUNK_SET, chunkSet);
|
||||
}
|
||||
|
||||
// Analyse the database and report chunk usage.
|
||||
async analyseDatabase() {
|
||||
if (!this.isAvailable()) return;
|
||||
const db = this.localDatabase.localDatabase;
|
||||
// Map of chunk ID to its info
|
||||
type ChunkInfo = {
|
||||
id: DocumentID;
|
||||
refCount: number;
|
||||
length: number;
|
||||
};
|
||||
const chunkMap = new Map<DocumentID, Set<ChunkInfo>>();
|
||||
// Map of document ID to its info
|
||||
type DocumentInfo = {
|
||||
id: DocumentID;
|
||||
rev: Rev;
|
||||
chunks: Set<ChunkID>;
|
||||
uniqueChunks: Set<ChunkID>;
|
||||
sharedChunks: Set<ChunkID>;
|
||||
path: FilePathWithPrefix;
|
||||
};
|
||||
const docMap = new Map<DocumentID, Set<DocumentInfo>>();
|
||||
const info = await db.info();
|
||||
// Total number of revisions to process (approximate)
|
||||
const maxSeq = new Number(info.update_seq);
|
||||
let processed = 0;
|
||||
let read = 0;
|
||||
let errored = 0;
|
||||
// Fetch Tasks
|
||||
const ft = [] as ReturnType<typeof fetchRevision>[];
|
||||
// Fetch a specific revision of a document and make note of its chunks, or add chunk info.
|
||||
const fetchRevision = async (id: DocumentID, rev: Rev, seq: string | number) => {
|
||||
try {
|
||||
processed++;
|
||||
const doc = await db.get(id, { rev: rev });
|
||||
if (doc) {
|
||||
if ("children" in doc) {
|
||||
const id = doc._id;
|
||||
const rev = doc._rev;
|
||||
const children = (doc.children || []) as DocumentID[];
|
||||
const set = docMap.get(id) || new Set();
|
||||
set.add({
|
||||
id,
|
||||
rev,
|
||||
chunks: new Set(children),
|
||||
uniqueChunks: new Set(),
|
||||
sharedChunks: new Set(),
|
||||
path: doc.path,
|
||||
});
|
||||
docMap.set(id, set);
|
||||
} else if (doc.type === EntryTypes.CHUNK) {
|
||||
const id = doc._id as DocumentID;
|
||||
if (chunkMap.has(id)) {
|
||||
return;
|
||||
}
|
||||
if (doc._deleted) {
|
||||
// Deleted chunk, skip (possibly resurrected later)
|
||||
return;
|
||||
}
|
||||
const length = doc.data.length;
|
||||
const set = chunkMap.get(id) || new Set();
|
||||
set.add({ id, length, refCount: 0 });
|
||||
chunkMap.set(id, set);
|
||||
}
|
||||
read++;
|
||||
} else {
|
||||
this._log(`Analysing Database: not found: ${id} / ${rev}`);
|
||||
errored++;
|
||||
}
|
||||
} catch (error) {
|
||||
this._log(`Error fetching document ${id} / ${rev}: $`, LOG_LEVEL_NOTICE);
|
||||
this._log(error, LOG_LEVEL_VERBOSE);
|
||||
errored++;
|
||||
}
|
||||
if (processed % 100 == 0) {
|
||||
this._log(`Analysing database: ${read} (${errored}) / ${maxSeq} `, LOG_LEVEL_NOTICE, "db-analyse");
|
||||
}
|
||||
};
|
||||
|
||||
// Enumerate all documents and their revisions.
|
||||
const IDs = this.localDatabase.findEntryNames("", "", {});
|
||||
for await (const id of IDs) {
|
||||
const revList = await this.localDatabase.getRaw(id as DocumentID, {
|
||||
revs: true,
|
||||
revs_info: true,
|
||||
conflicts: true,
|
||||
});
|
||||
const revInfos = revList._revs_info || [];
|
||||
for (const revInfo of revInfos) {
|
||||
// All available revisions should be processed.
|
||||
// If the revision is not available, it means the revision is already tombstoned.
|
||||
if (revInfo.status == "available") {
|
||||
// Schedule fetch task
|
||||
ft.push(fetchRevision(id as DocumentID, revInfo.rev, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Wait for all fetch tasks to complete.
|
||||
await Promise.all(ft);
|
||||
// Reference count marking and unique/shared chunk classification.
|
||||
for (const [, docRevs] of docMap) {
|
||||
for (const docRev of docRevs) {
|
||||
for (const chunkId of docRev.chunks) {
|
||||
const chunkInfos = chunkMap.get(chunkId);
|
||||
if (chunkInfos) {
|
||||
for (const chunkInfo of chunkInfos) {
|
||||
if (chunkInfo.refCount === 0) {
|
||||
docRev.uniqueChunks.add(chunkId);
|
||||
} else {
|
||||
docRev.sharedChunks.add(chunkId);
|
||||
}
|
||||
chunkInfo.refCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Prepare results
|
||||
const result = [];
|
||||
// Calculate total size of chunks in the given set.
|
||||
const getTotalSize = (ids: Set<DocumentID>) => {
|
||||
return [...ids].reduce((acc, chunkId) => {
|
||||
const chunkInfos = chunkMap.get(chunkId);
|
||||
if (chunkInfos) {
|
||||
for (const chunkInfo of chunkInfos) {
|
||||
acc += chunkInfo.length;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Compile results for each document revision
|
||||
for (const doc of docMap.values()) {
|
||||
for (const rev of doc) {
|
||||
const title = `${rev.path} (${rev.rev})`;
|
||||
const id = rev.id;
|
||||
const revStr = `${getNoFromRev(rev.rev)}`;
|
||||
const revHash = rev.rev.split("-")[1].substring(0, 6);
|
||||
const path = rev.path;
|
||||
const uniqueChunkCount = rev.uniqueChunks.size;
|
||||
const sharedChunkCount = rev.sharedChunks.size;
|
||||
const uniqueChunkSize = getTotalSize(rev.uniqueChunks);
|
||||
const sharedChunkSize = getTotalSize(rev.sharedChunks);
|
||||
result.push({
|
||||
title,
|
||||
path,
|
||||
rev: revStr,
|
||||
revHash,
|
||||
id,
|
||||
uniqueChunkCount: uniqueChunkCount,
|
||||
sharedChunkCount,
|
||||
uniqueChunkSize: uniqueChunkSize,
|
||||
sharedChunkSize: sharedChunkSize,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const titleMap = {
|
||||
title: "Title",
|
||||
id: "Document ID",
|
||||
path: "Path",
|
||||
rev: "Revision No",
|
||||
revHash: "Revision Hash",
|
||||
uniqueChunkCount: "Unique Chunk Count",
|
||||
sharedChunkCount: "Shared Chunk Count",
|
||||
uniqueChunkSize: "Unique Chunk Size",
|
||||
sharedChunkSize: "Shared Chunk Size",
|
||||
} as const;
|
||||
// Enumerate orphan chunks (not referenced by any document)
|
||||
const orphanChunks = [...chunkMap.entries()].filter(([chunkId, infos]) => {
|
||||
const totalRefCount = [...infos].reduce((acc, info) => acc + info.refCount, 0);
|
||||
return totalRefCount === 0;
|
||||
});
|
||||
const orphanChunkSize = orphanChunks.reduce((acc, [chunkId, infos]) => {
|
||||
for (const info of infos) {
|
||||
acc += info.length;
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
result.push({
|
||||
title: "__orphan",
|
||||
id: "__orphan",
|
||||
path: "__orphan",
|
||||
rev: "1",
|
||||
revHash: "xxxxx",
|
||||
uniqueChunkCount: orphanChunks.length,
|
||||
sharedChunkCount: 0,
|
||||
uniqueChunkSize: orphanChunkSize,
|
||||
sharedChunkSize: 0,
|
||||
} as any);
|
||||
|
||||
const csvSrc = result.map((e) => {
|
||||
return [
|
||||
`${e.title.replace(/"/g, '""')}"`,
|
||||
`${e.id}`,
|
||||
`${e.path}`,
|
||||
`${e.rev}`,
|
||||
`${e.revHash}`,
|
||||
`${e.uniqueChunkCount}`,
|
||||
`${e.sharedChunkCount}`,
|
||||
`${e.uniqueChunkSize}`,
|
||||
`${e.sharedChunkSize}`,
|
||||
].join("\t");
|
||||
});
|
||||
// Add title row
|
||||
csvSrc.unshift(Object.values(titleMap).join("\t"));
|
||||
const csv = csvSrc.join("\n");
|
||||
|
||||
// Prompt to copy to clipboard
|
||||
await this.services.UI.promptCopyToClipboard("Database Analysis data (TSV):", csv);
|
||||
}
|
||||
}
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 86b0a95d56...da14678633
@@ -26,7 +26,7 @@ import { ModuleFileAccessObsidian } from "./modules/coreObsidian/ModuleFileAcces
|
||||
import { ModuleInputUIObsidian } from "./modules/coreObsidian/ModuleInputUIObsidian.ts";
|
||||
import { ModuleMigration } from "./modules/essential/ModuleMigration.ts";
|
||||
|
||||
import { ModuleCheckRemoteSize } from "./modules/coreFeatures/ModuleCheckRemoteSize.ts";
|
||||
import { ModuleCheckRemoteSize } from "./modules/essentialObsidian/ModuleCheckRemoteSize.ts";
|
||||
import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver.ts";
|
||||
import { ModuleInteractiveConflictResolver } from "./modules/features/ModuleInteractiveConflictResolver.ts";
|
||||
import { ModuleLog } from "./modules/features/ModuleLog.ts";
|
||||
@@ -170,9 +170,7 @@ export default class ObsidianLiveSyncPlugin
|
||||
new ModuleRedFlag(this),
|
||||
new ModuleInteractiveConflictResolver(this, this),
|
||||
new ModuleObsidianGlobalHistory(this, this),
|
||||
// Common modules
|
||||
// Note: Platform-dependent functions are not entirely dependent on the core only, as they are from platform-dependent modules. Stubbing is sometimes required.
|
||||
new ModuleCheckRemoteSize(this),
|
||||
new ModuleCheckRemoteSize(this, this),
|
||||
// Test and Dev Modules
|
||||
new ModuleDev(this, this),
|
||||
new ModuleReplicateTest(this, this),
|
||||
|
||||
@@ -76,11 +76,11 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements Database
|
||||
async checkIsTargetFile(file: UXFileInfoStub | FilePathWithPrefix): Promise<boolean> {
|
||||
const path = getStoragePathFromUXFileInfo(file);
|
||||
if (!(await this.services.vault.isTargetFile(path))) {
|
||||
this._log(`File is not target`, LOG_LEVEL_VERBOSE);
|
||||
this._log(`File is not target: ${path}`, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
if (shouldBeIgnored(path)) {
|
||||
this._log(`File should be ignored`, LOG_LEVEL_VERBOSE);
|
||||
this._log(`File should be ignored: ${path}`, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { delay } from "octagonal-wheels/promises";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
FLAGMD_REDFLAG2_HR,
|
||||
FLAGMD_REDFLAG3_HR,
|
||||
LOG_LEVEL_NOTICE,
|
||||
@@ -58,7 +59,7 @@ Please enable them from the settings screen after setup is complete.`,
|
||||
async rebuildRemote() {
|
||||
await this.services.setting.suspendExtraSync();
|
||||
this.core.settings.isConfigured = true;
|
||||
|
||||
this.core.settings.notifyThresholdOfRemoteStorageSize = DEFAULT_SETTINGS.notifyThresholdOfRemoteStorageSize;
|
||||
await this.services.setting.realiseSetting();
|
||||
await this.services.remote.markLocked();
|
||||
await this.services.remote.tryResetDatabase();
|
||||
@@ -77,8 +78,9 @@ Please enable them from the settings screen after setup is complete.`,
|
||||
|
||||
async rebuildEverything() {
|
||||
await this.services.setting.suspendExtraSync();
|
||||
await this.askUseNewAdapter();
|
||||
// await this.askUseNewAdapter();
|
||||
this.core.settings.isConfigured = true;
|
||||
this.core.settings.notifyThresholdOfRemoteStorageSize = DEFAULT_SETTINGS.notifyThresholdOfRemoteStorageSize;
|
||||
await this.services.setting.realiseSetting();
|
||||
await this.resetLocalDatabase();
|
||||
await delay(1000);
|
||||
@@ -167,29 +169,31 @@ Please enable them from the settings screen after setup is complete.`,
|
||||
await this.services.replication.onBeforeReplicate(false); //TODO: Check actual need of this.
|
||||
await this.core.saveSettings();
|
||||
}
|
||||
async askUseNewAdapter() {
|
||||
if (!this.core.settings.useIndexedDBAdapter) {
|
||||
const message = `Now this core has been configured to use the old database adapter for keeping compatibility. Do you want to deactivate it?`;
|
||||
const CHOICE_YES = "Yes, disable and use latest";
|
||||
const CHOICE_NO = "No, keep compatibility";
|
||||
const choices = [CHOICE_YES, CHOICE_NO];
|
||||
|
||||
const ret = await this.core.confirm.confirmWithMessage(
|
||||
"Database adapter",
|
||||
message,
|
||||
choices,
|
||||
CHOICE_YES,
|
||||
10
|
||||
);
|
||||
if (ret == CHOICE_YES) {
|
||||
this.core.settings.useIndexedDBAdapter = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// No longer needed, both adapters have each advantages and disadvantages.
|
||||
// async askUseNewAdapter() {
|
||||
// if (!this.core.settings.useIndexedDBAdapter) {
|
||||
// const message = `Now this core has been configured to use the old database adapter for keeping compatibility. Do you want to deactivate it?`;
|
||||
// const CHOICE_YES = "Yes, disable and use latest";
|
||||
// const CHOICE_NO = "No, keep compatibility";
|
||||
// const choices = [CHOICE_YES, CHOICE_NO];
|
||||
//
|
||||
// const ret = await this.core.confirm.confirmWithMessage(
|
||||
// "Database adapter",
|
||||
// message,
|
||||
// choices,
|
||||
// CHOICE_YES,
|
||||
// 10
|
||||
// );
|
||||
// if (ret == CHOICE_YES) {
|
||||
// this.core.settings.useIndexedDBAdapter = true;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
async fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean) {
|
||||
await this.services.setting.suspendExtraSync();
|
||||
await this.askUseNewAdapter();
|
||||
// await this.askUseNewAdapter();
|
||||
this.core.settings.isConfigured = true;
|
||||
this.core.settings.notifyThresholdOfRemoteStorageSize = DEFAULT_SETTINGS.notifyThresholdOfRemoteStorageSize;
|
||||
await this.suspendReflectingDatabase();
|
||||
await this.services.setting.realiseSetting();
|
||||
await this.resetLocalDatabase();
|
||||
@@ -236,7 +240,7 @@ Please enable them from the settings screen after setup is complete.`,
|
||||
async fetchRemoteChunks() {
|
||||
if (
|
||||
!this.core.settings.doNotSuspendOnFetching &&
|
||||
this.core.settings.readChunksOnline &&
|
||||
!this.core.settings.useOnlyLocalChunk &&
|
||||
this.core.settings.remoteType == REMOTE_COUCHDB
|
||||
) {
|
||||
this._log(`Fetching chunks`, LOG_LEVEL_NOTICE);
|
||||
|
||||
@@ -14,34 +14,15 @@ import { isLockAcquired, shareRunningResult, skipIfDuplicated } from "octagonal-
|
||||
import { balanceChunkPurgedDBs } from "@/lib/src/pouchdb/chunks";
|
||||
import { purgeUnreferencedChunks } from "@/lib/src/pouchdb/chunks";
|
||||
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
|
||||
import { throttle } from "octagonal-wheels/function";
|
||||
import { arrayToChunkedArray } from "octagonal-wheels/collection";
|
||||
import {
|
||||
SYNCINFO_ID,
|
||||
VER,
|
||||
type EntryBody,
|
||||
type EntryDoc,
|
||||
type EntryLeaf,
|
||||
type LoadedEntry,
|
||||
type MetaEntry,
|
||||
type RemoteType,
|
||||
} from "../../lib/src/common/types";
|
||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||
import {
|
||||
getPath,
|
||||
isChunk,
|
||||
isValidPath,
|
||||
rateLimitedSharedExecution,
|
||||
scheduleTask,
|
||||
updatePreviousExecutionTime,
|
||||
} from "../../common/utils";
|
||||
import { isAnyNote } from "../../lib/src/common/utils";
|
||||
import { type EntryDoc, type RemoteType } from "../../lib/src/common/types";
|
||||
import { rateLimitedSharedExecution, scheduleTask, updatePreviousExecutionTime } from "../../common/utils";
|
||||
import { EVENT_FILE_SAVED, EVENT_ON_UNRESOLVED_ERROR, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
|
||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
||||
|
||||
import { $msg } from "../../lib/src/common/i18n";
|
||||
import { clearHandlers } from "../../lib/src/replication/SyncParamsHandler";
|
||||
import type { LiveSyncCore } from "../../main";
|
||||
import { ReplicateResultProcessor } from "./ReplicateResultProcessor";
|
||||
|
||||
const KEY_REPLICATION_ON_EVENT = "replicationOnEvent";
|
||||
const REPLICATION_ON_EVENT_FORECASTED_TIME = 5000;
|
||||
@@ -49,6 +30,7 @@ const REPLICATION_ON_EVENT_FORECASTED_TIME = 5000;
|
||||
export class ModuleReplicator extends AbstractModule {
|
||||
_replicatorType?: RemoteType;
|
||||
_previousErrors = new Set<string>();
|
||||
processor: ReplicateResultProcessor = new ReplicateResultProcessor(this);
|
||||
|
||||
showError(msg: string, max_log_level: LOG_LEVEL = LEVEL_NOTICE) {
|
||||
const level = this._previousErrors.has(msg) ? LEVEL_INFO : max_log_level;
|
||||
@@ -73,6 +55,11 @@ export class ModuleReplicator extends AbstractModule {
|
||||
if (this._replicatorType !== setting.remoteType) {
|
||||
void this.setReplicator();
|
||||
}
|
||||
if (this.core.settings.suspendParseReplicationResult) {
|
||||
this.processor.suspend();
|
||||
} else {
|
||||
this.processor.resume();
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve(true);
|
||||
@@ -103,6 +90,10 @@ export class ModuleReplicator extends AbstractModule {
|
||||
_everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
||||
return this.setReplicator();
|
||||
}
|
||||
_everyOnDatabaseInitialized(showNotice: boolean): Promise<boolean> {
|
||||
fireAndForget(() => this.processor.restoreFromSnapshotOnce());
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
_everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
||||
return this.setReplicator();
|
||||
@@ -128,7 +119,7 @@ export class ModuleReplicator extends AbstractModule {
|
||||
this.showError("Failed to initialise the encryption key, preventing replication.");
|
||||
return false;
|
||||
}
|
||||
await this.loadQueuedFiles();
|
||||
await this.processor.restoreFromSnapshotOnce();
|
||||
this.clearErrors();
|
||||
return true;
|
||||
}
|
||||
@@ -287,249 +278,10 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
||||
}
|
||||
return await shareRunningResult(`replication`, () => this.services.replication.replicate());
|
||||
}
|
||||
|
||||
_parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): void {
|
||||
if (this.settings.suspendParseReplicationResult && !this.replicationResultProcessor.isSuspended) {
|
||||
this.replicationResultProcessor.suspend();
|
||||
}
|
||||
this.replicationResultProcessor.enqueueAll(docs);
|
||||
if (!this.settings.suspendParseReplicationResult && this.replicationResultProcessor.isSuspended) {
|
||||
this.replicationResultProcessor.resume();
|
||||
}
|
||||
this.processor.enqueueAll(docs);
|
||||
}
|
||||
_saveQueuedFiles = throttle(() => {
|
||||
const saveData = this.replicationResultProcessor._queue
|
||||
.filter((e) => e !== undefined && e !== null)
|
||||
.map((e) => e?._id ?? ("" as string)) as string[];
|
||||
const kvDBKey = "queued-files";
|
||||
// localStorage.setItem(lsKey, saveData);
|
||||
fireAndForget(() => this.core.kvDB.set(kvDBKey, saveData));
|
||||
}, 100);
|
||||
saveQueuedFiles() {
|
||||
this._saveQueuedFiles();
|
||||
}
|
||||
async loadQueuedFiles() {
|
||||
if (this.settings.suspendParseReplicationResult) return;
|
||||
if (!this.settings.isConfigured) return;
|
||||
try {
|
||||
const kvDBKey = "queued-files";
|
||||
// const ids = [...new Set(JSON.parse(localStorage.getItem(lsKey) || "[]"))] as string[];
|
||||
const ids = [...new Set((await this.core.kvDB.get<string[]>(kvDBKey)) ?? [])];
|
||||
const batchSize = 100;
|
||||
const chunkedIds = arrayToChunkedArray(ids, batchSize);
|
||||
|
||||
// suspendParseReplicationResult is true, so we have to resume it if it is suspended.
|
||||
if (this.replicationResultProcessor.isSuspended) {
|
||||
this.replicationResultProcessor.resume();
|
||||
}
|
||||
for await (const idsBatch of chunkedIds) {
|
||||
const ret = await this.localDatabase.allDocsRaw<EntryDoc>({
|
||||
keys: idsBatch,
|
||||
include_docs: true,
|
||||
limit: 100,
|
||||
});
|
||||
const docs = ret.rows
|
||||
.filter((e) => e.doc)
|
||||
.map((e) => e.doc) as PouchDB.Core.ExistingDocument<EntryDoc>[];
|
||||
const errors = ret.rows.filter((e) => !e.doc && !e.value.deleted);
|
||||
if (errors.length > 0) {
|
||||
Logger("Some queued processes were not resurrected");
|
||||
Logger(JSON.stringify(errors), LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
this.replicationResultProcessor.enqueueAll(docs);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger(`Failed to load queued files.`, LOG_LEVEL_NOTICE);
|
||||
Logger(e, LOG_LEVEL_VERBOSE);
|
||||
} finally {
|
||||
// Check again before awaiting,
|
||||
if (this.replicationResultProcessor.isSuspended) {
|
||||
this.replicationResultProcessor.resume();
|
||||
}
|
||||
}
|
||||
// Wait for all queued files to be processed.
|
||||
try {
|
||||
await this.replicationResultProcessor.waitForAllProcessed();
|
||||
} catch (e) {
|
||||
Logger(`Failed to wait for all queued files to be processed.`, LOG_LEVEL_NOTICE);
|
||||
Logger(e, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
|
||||
replicationResultProcessor = new QueueProcessor(
|
||||
async (docs: PouchDB.Core.ExistingDocument<EntryDoc>[]) => {
|
||||
if (this.settings.suspendParseReplicationResult) return;
|
||||
const change = docs[0];
|
||||
if (!change) return;
|
||||
if (isChunk(change._id)) {
|
||||
this.localDatabase.onNewLeaf(change as EntryLeaf);
|
||||
return;
|
||||
}
|
||||
if (await this.services.replication.processVirtualDocument(change)) return;
|
||||
// any addon needs this item?
|
||||
// for (const proc of this.core.addOns) {
|
||||
// if (await proc.parseReplicationResultItem(change)) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
if (change.type == "versioninfo") {
|
||||
if (change.version > VER) {
|
||||
this.core.replicator.closeReplication();
|
||||
Logger(
|
||||
`Remote database updated to incompatible version. update your Self-hosted LiveSync plugin.`,
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
change._id == SYNCINFO_ID || // Synchronisation information data
|
||||
change._id.startsWith("_design") //design document
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (isAnyNote(change)) {
|
||||
const docPath = getPath(change);
|
||||
if (!(await this.services.vault.isTargetFile(docPath))) {
|
||||
Logger(`Skipped: ${docPath}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
if (this.databaseQueuedProcessor._isSuspended) {
|
||||
Logger(`Processing scheduled: ${docPath}`, LOG_LEVEL_INFO);
|
||||
}
|
||||
const size = change.size;
|
||||
if (this.services.vault.isFileSizeTooLarge(size)) {
|
||||
Logger(
|
||||
`Processing ${docPath} has been skipped due to file size exceeding the limit`,
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.databaseQueuedProcessor.enqueue(change);
|
||||
}
|
||||
return;
|
||||
},
|
||||
{
|
||||
batchSize: 1,
|
||||
suspended: true,
|
||||
concurrentLimit: 100,
|
||||
delay: 0,
|
||||
totalRemainingReactiveSource: this.core.replicationResultCount,
|
||||
}
|
||||
)
|
||||
.replaceEnqueueProcessor((queue, newItem) => {
|
||||
const q = queue.filter((e) => e._id != newItem._id);
|
||||
return [...q, newItem];
|
||||
})
|
||||
.startPipeline()
|
||||
.onUpdateProgress(() => {
|
||||
this.saveQueuedFiles();
|
||||
});
|
||||
|
||||
async checkIsChangeRequiredForDatabaseProcessing(dbDoc: LoadedEntry): Promise<boolean> {
|
||||
const path = getPath(dbDoc);
|
||||
try {
|
||||
const savedDoc = await this.localDatabase.getRaw<LoadedEntry>(dbDoc._id, {
|
||||
conflicts: true,
|
||||
revs_info: true,
|
||||
});
|
||||
const newRev = dbDoc._rev ?? "";
|
||||
const latestRev = savedDoc._rev ?? "";
|
||||
const revisions = savedDoc._revs_info?.map((e) => e.rev) ?? [];
|
||||
if (savedDoc._conflicts && savedDoc._conflicts.length > 0) {
|
||||
// There are conflicts, so we have to process it.
|
||||
return true;
|
||||
}
|
||||
if (newRev == latestRev) {
|
||||
// The latest revision. We need to process it.
|
||||
return true;
|
||||
}
|
||||
const index = revisions.indexOf(newRev);
|
||||
if (index >= 0) {
|
||||
// the revision has been inserted before.
|
||||
return false; // Already processed.
|
||||
}
|
||||
return true; // This mostly should not happen, but we have to process it just in case.
|
||||
} catch (e: any) {
|
||||
if ("status" in e && e.status == 404) {
|
||||
return true;
|
||||
// Not existing, so we have to process it.
|
||||
} else {
|
||||
Logger(
|
||||
`Failed to get existing document for ${path} (${dbDoc._id.substring(0, 8)}, ${dbDoc._rev?.substring(0, 10)}) `,
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
Logger(e, LOG_LEVEL_VERBOSE);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
databaseQueuedProcessor = new QueueProcessor(
|
||||
async (docs: EntryBody[]) => {
|
||||
const dbDoc = docs[0] as LoadedEntry; // It has no `data`
|
||||
const path = getPath(dbDoc);
|
||||
// If the document is existing with any revision, confirm that we have to process it.
|
||||
const isRequired = await this.checkIsChangeRequiredForDatabaseProcessing(dbDoc);
|
||||
if (!isRequired) {
|
||||
Logger(`Skipped (Not latest): ${path} (${dbDoc._id.substring(0, 8)})`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
// If `Read chunks online` is disabled, chunks should be transferred before here.
|
||||
// However, in some cases, chunks are after that. So, if missing chunks exist, we have to wait for them.
|
||||
const doc = await this.localDatabase.getDBEntryFromMeta({ ...dbDoc }, false, true);
|
||||
if (!doc) {
|
||||
Logger(
|
||||
`Something went wrong while gathering content of ${path} (${dbDoc._id.substring(0, 8)}, ${dbDoc._rev?.substring(0, 10)}) `,
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.services.replication.processOptionalSynchroniseResult(dbDoc)) {
|
||||
// Already processed
|
||||
} else if (isValidPath(getPath(doc))) {
|
||||
this.storageApplyingProcessor.enqueue(doc as MetaEntry);
|
||||
} else {
|
||||
Logger(`Skipped: ${path} (${doc._id.substring(0, 8)})`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
return;
|
||||
},
|
||||
{
|
||||
suspended: true,
|
||||
batchSize: 1,
|
||||
concurrentLimit: 10,
|
||||
yieldThreshold: 1,
|
||||
delay: 0,
|
||||
totalRemainingReactiveSource: this.core.databaseQueueCount,
|
||||
}
|
||||
)
|
||||
.replaceEnqueueProcessor((queue, newItem) => {
|
||||
const q = queue.filter((e) => e._id != newItem._id);
|
||||
return [...q, newItem];
|
||||
})
|
||||
.startPipeline();
|
||||
|
||||
storageApplyingProcessor = new QueueProcessor(
|
||||
async (docs: MetaEntry[]) => {
|
||||
const entry = docs[0];
|
||||
await this.services.replication.processSynchroniseResult(entry);
|
||||
return;
|
||||
},
|
||||
{
|
||||
suspended: true,
|
||||
batchSize: 1,
|
||||
concurrentLimit: 6,
|
||||
yieldThreshold: 1,
|
||||
delay: 0,
|
||||
totalRemainingReactiveSource: this.core.storageApplyingCount,
|
||||
}
|
||||
)
|
||||
.replaceEnqueueProcessor((queue, newItem) => {
|
||||
const q = queue.filter((e) => e._id != newItem._id);
|
||||
return [...q, newItem];
|
||||
})
|
||||
.startPipeline();
|
||||
|
||||
_everyBeforeSuspendProcess(): Promise<boolean> {
|
||||
this.core.replicator?.closeReplication();
|
||||
@@ -579,6 +331,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
||||
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||
services.replicator.handleGetActiveReplicator(this._getReplicator.bind(this));
|
||||
services.databaseEvents.handleOnDatabaseInitialisation(this._everyOnInitializeDatabase.bind(this));
|
||||
services.databaseEvents.handleDatabaseInitialised(this._everyOnDatabaseInitialized.bind(this));
|
||||
services.databaseEvents.handleOnResetDatabase(this._everyOnResetDatabase.bind(this));
|
||||
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
|
||||
services.replication.handleParseSynchroniseResult(this._parseReplicationResult.bind(this));
|
||||
|
||||
466
src/modules/core/ReplicateResultProcessor.ts
Normal file
466
src/modules/core/ReplicateResultProcessor.ts
Normal file
@@ -0,0 +1,466 @@
|
||||
import {
|
||||
SYNCINFO_ID,
|
||||
VER,
|
||||
type AnyEntry,
|
||||
type EntryDoc,
|
||||
type EntryLeaf,
|
||||
type LoadedEntry,
|
||||
type MetaEntry,
|
||||
} from "@/lib/src/common/types";
|
||||
import type { ModuleReplicator } from "./ModuleReplicator";
|
||||
import { getPath, isChunk, isValidPath } from "@/common/utils";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import {
|
||||
LOG_LEVEL_DEBUG,
|
||||
LOG_LEVEL_INFO,
|
||||
LOG_LEVEL_NOTICE,
|
||||
LOG_LEVEL_VERBOSE,
|
||||
Logger,
|
||||
type LOG_LEVEL,
|
||||
} from "@/lib/src/common/logger";
|
||||
import { fireAndForget, isAnyNote, throttle } from "@/lib/src/common/utils";
|
||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore_v2";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
||||
|
||||
const KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT = "replicationResultProcessorSnapshot";
|
||||
type ReplicateResultProcessorState = {
|
||||
queued: PouchDB.Core.ExistingDocument<EntryDoc>[];
|
||||
processing: PouchDB.Core.ExistingDocument<EntryDoc>[];
|
||||
};
|
||||
function shortenId(id: string): string {
|
||||
return id.length > 10 ? id.substring(0, 10) : id;
|
||||
}
|
||||
function shortenRev(rev: string | undefined): string {
|
||||
if (!rev) return "undefined";
|
||||
return rev.length > 10 ? rev.substring(0, 10) : rev;
|
||||
}
|
||||
export class ReplicateResultProcessor {
|
||||
private log(message: string, level: LOG_LEVEL = LOG_LEVEL_INFO) {
|
||||
Logger(`[ReplicateResultProcessor] ${message}`, level);
|
||||
}
|
||||
private logError(e: any) {
|
||||
Logger(e, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
private replicator: ModuleReplicator;
|
||||
|
||||
constructor(replicator: ModuleReplicator) {
|
||||
this.replicator = replicator;
|
||||
}
|
||||
|
||||
get localDatabase() {
|
||||
return this.replicator.core.localDatabase;
|
||||
}
|
||||
get services() {
|
||||
return this.replicator.core.services;
|
||||
}
|
||||
get core(): LiveSyncCore {
|
||||
return this.replicator.core;
|
||||
}
|
||||
|
||||
public suspend() {
|
||||
this._suspended = true;
|
||||
}
|
||||
public resume() {
|
||||
this._suspended = false;
|
||||
fireAndForget(() => this.runProcessQueue());
|
||||
}
|
||||
|
||||
// Whether the processing is suspended
|
||||
// If true, the processing queue processor bails the loop.
|
||||
private _suspended: boolean = false;
|
||||
|
||||
public get isSuspended() {
|
||||
return (
|
||||
this._suspended ||
|
||||
!this.core.services.appLifecycle.isReady ||
|
||||
this.replicator.settings.suspendParseReplicationResult ||
|
||||
this.core.services.appLifecycle.isSuspended()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a snapshot of the current processing state.
|
||||
* This snapshot is stored in the KV database for recovery on restart.
|
||||
*/
|
||||
protected async _takeSnapshot() {
|
||||
const snapshot = {
|
||||
queued: this._queuedChanges.slice(),
|
||||
processing: this._processingChanges.slice(),
|
||||
} satisfies ReplicateResultProcessorState;
|
||||
await this.core.kvDB.set(KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT, snapshot);
|
||||
this.log(
|
||||
`Snapshot taken. Queued: ${snapshot.queued.length}, Processing: ${snapshot.processing.length}`,
|
||||
LOG_LEVEL_DEBUG
|
||||
);
|
||||
this.reportStatus();
|
||||
}
|
||||
/**
|
||||
* Trigger taking a snapshot.
|
||||
*/
|
||||
protected _triggerTakeSnapshot() {
|
||||
fireAndForget(() => this._takeSnapshot());
|
||||
}
|
||||
/**
|
||||
* Throttled version of triggerTakeSnapshot.
|
||||
*/
|
||||
protected triggerTakeSnapshot = throttle(() => this._triggerTakeSnapshot(), 50);
|
||||
|
||||
/**
|
||||
* Restore from snapshot.
|
||||
*/
|
||||
public async restoreFromSnapshot() {
|
||||
const snapshot = await this.core.kvDB.get<ReplicateResultProcessorState>(
|
||||
KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT
|
||||
);
|
||||
if (snapshot) {
|
||||
// Restoring the snapshot re-runs processing for both queued and processing items.
|
||||
const newQueue = [...snapshot.processing, ...snapshot.queued, ...this._queuedChanges];
|
||||
this._queuedChanges = [];
|
||||
this.enqueueAll(newQueue);
|
||||
this.log(
|
||||
`Restored from snapshot (${snapshot.processing.length + snapshot.queued.length} items)`,
|
||||
LOG_LEVEL_INFO
|
||||
);
|
||||
// await this._takeSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
private _restoreFromSnapshot: Promise<void> | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Restore from snapshot only once.
|
||||
* @returns Promise that resolves when restoration is complete.
|
||||
*/
|
||||
public restoreFromSnapshotOnce() {
|
||||
if (!this._restoreFromSnapshot) {
|
||||
this._restoreFromSnapshot = this.restoreFromSnapshot();
|
||||
}
|
||||
return this._restoreFromSnapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the given procedure while counting the concurrency.
|
||||
* @param proc async procedure to perform
|
||||
* @param countValue reactive source to count concurrency
|
||||
* @returns result of the procedure
|
||||
*/
|
||||
async withCounting<T>(proc: () => Promise<T>, countValue: ReactiveSource<number>) {
|
||||
countValue.value++;
|
||||
try {
|
||||
return await proc();
|
||||
} finally {
|
||||
countValue.value--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the current status.
|
||||
*/
|
||||
protected reportStatus() {
|
||||
this.core.replicationResultCount.value = this._queuedChanges.length + this._processingChanges.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue all the given changes for processing.
|
||||
* @param changes Changes to enqueue
|
||||
*/
|
||||
|
||||
public enqueueAll(changes: PouchDB.Core.ExistingDocument<EntryDoc>[]) {
|
||||
for (const change of changes) {
|
||||
// Check if the change is not a document change (e.g., chunk, versioninfo, syncinfo), and processed it directly.
|
||||
const isProcessed = this.processIfNonDocumentChange(change);
|
||||
if (!isProcessed) {
|
||||
this.enqueueChange(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Process the change if it is not a document change.
|
||||
* @param change Change to process
|
||||
* @returns True if the change was processed; false otherwise
|
||||
*/
|
||||
protected processIfNonDocumentChange(change: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
||||
if (!change) {
|
||||
this.log(`Received empty change`, LOG_LEVEL_VERBOSE);
|
||||
return true;
|
||||
}
|
||||
if (isChunk(change._id)) {
|
||||
// Emit event for new chunk
|
||||
this.localDatabase.onNewLeaf(change as EntryLeaf);
|
||||
this.log(`Processed chunk: ${shortenId(change._id)}`, LOG_LEVEL_DEBUG);
|
||||
return true;
|
||||
}
|
||||
if (change.type == "versioninfo") {
|
||||
this.log(`Version info document received: ${change._id}`, LOG_LEVEL_VERBOSE);
|
||||
if (change.version > VER) {
|
||||
// Incompatible version, stop replication.
|
||||
this.core.replicator.closeReplication();
|
||||
this.log(
|
||||
`Remote database updated to incompatible version. update your Self-hosted LiveSync plugin.`,
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
change._id == SYNCINFO_ID || // Synchronisation information data
|
||||
change._id.startsWith("_design") //design document
|
||||
) {
|
||||
this.log(`Skipped system document: ${change._id}`, LOG_LEVEL_VERBOSE);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue of changes to be processed.
|
||||
*/
|
||||
private _queuedChanges: PouchDB.Core.ExistingDocument<EntryDoc>[] = [];
|
||||
|
||||
/**
|
||||
* List of changes being processed.
|
||||
*/
|
||||
private _processingChanges: PouchDB.Core.ExistingDocument<EntryDoc>[] = [];
|
||||
|
||||
/**
|
||||
* Enqueue the given document change for processing.
|
||||
* @param doc Document change to enqueue
|
||||
* @returns
|
||||
*/
|
||||
protected enqueueChange(doc: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
||||
const old = this._queuedChanges.find((e) => e._id == doc._id);
|
||||
const path = "path" in doc ? getPath(doc) : "<unknown>";
|
||||
const docNote = `${path} (${shortenId(doc._id)}, ${shortenRev(doc._rev)})`;
|
||||
if (old) {
|
||||
if (old._rev == doc._rev) {
|
||||
this.log(`[Enqueue] skipped (Already queued): ${docNote}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
|
||||
const oldRev = old._rev ?? "";
|
||||
const isDeletedBefore = old._deleted === true || ("deleted" in old && old.deleted === true);
|
||||
const isDeletedNow = doc._deleted === true || ("deleted" in doc && doc.deleted === true);
|
||||
|
||||
// Replace the old queued change (This may performed batched updates, actually process performed always with the latest version, hence we can simply replace it if the change is the same type).
|
||||
if (isDeletedBefore === isDeletedNow) {
|
||||
this._queuedChanges = this._queuedChanges.filter((e) => e._id != doc._id);
|
||||
this.log(`[Enqueue] requeued: ${docNote} (from rev: ${shortenRev(oldRev)})`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
// Enqueue the change
|
||||
this._queuedChanges.push(doc);
|
||||
this.triggerTakeSnapshot();
|
||||
this.triggerProcessQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger processing of the queued changes.
|
||||
*/
|
||||
protected triggerProcessQueue() {
|
||||
fireAndForget(() => this.runProcessQueue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Semaphore to limit concurrent processing.
|
||||
* This is the per-id semaphore + concurrency-control (max 10 concurrent = 10 documents being processed at the same time).
|
||||
*/
|
||||
private _semaphore = Semaphore(10);
|
||||
|
||||
/**
|
||||
* Flag indicating whether the process queue is currently running.
|
||||
*/
|
||||
private _isRunningProcessQueue: boolean = false;
|
||||
|
||||
/**
|
||||
* Process the queued changes.
|
||||
*/
|
||||
private async runProcessQueue() {
|
||||
// Avoid re-entrance, suspend processing, or empty queue loop consumption.
|
||||
if (this._isRunningProcessQueue) return;
|
||||
if (this.isSuspended) return;
|
||||
if (this._queuedChanges.length == 0) return;
|
||||
try {
|
||||
this._isRunningProcessQueue = true;
|
||||
while (this._queuedChanges.length > 0) {
|
||||
// If getting suspended, bail the loop. Some concurrent tasks may still be running.
|
||||
if (this.isSuspended) {
|
||||
this.log(
|
||||
`Processing has got suspended. Remaining items in queue: ${this._queuedChanges.length}`,
|
||||
LOG_LEVEL_INFO
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Acquire semaphore for new processing slot
|
||||
// (per-document serialisation caps concurrency).
|
||||
const releaser = await this._semaphore.acquire();
|
||||
releaser();
|
||||
// Dequeue the next change
|
||||
const doc = this._queuedChanges.shift();
|
||||
if (doc) {
|
||||
this._processingChanges.push(doc);
|
||||
void this.parseDocumentChange(doc);
|
||||
}
|
||||
// Take snapshot (to be restored on next startup if needed)
|
||||
this.triggerTakeSnapshot();
|
||||
}
|
||||
} finally {
|
||||
this._isRunningProcessQueue = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 1: parse replication result
|
||||
/**
|
||||
* Parse the given document change.
|
||||
* @param change
|
||||
* @returns
|
||||
*/
|
||||
async parseDocumentChange(change: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
||||
try {
|
||||
// If the document is a virtual document, process it in the virtual document processor.
|
||||
if (await this.services.replication.processVirtualDocument(change)) return;
|
||||
// If the document is version info, check compatibility and return.
|
||||
if (isAnyNote(change)) {
|
||||
const docPath = getPath(change);
|
||||
if (!(await this.services.vault.isTargetFile(docPath))) {
|
||||
this.log(`Skipped: ${docPath}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const size = change.size;
|
||||
// Note that this size check depends size that in metadata, not the actual content size.
|
||||
if (this.services.vault.isFileSizeTooLarge(size)) {
|
||||
this.log(
|
||||
`Processing ${docPath} has been skipped due to file size exceeding the limit`,
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
return;
|
||||
}
|
||||
return await this.applyToDatabase(change);
|
||||
}
|
||||
this.log(`Skipped unexpected non-note document: ${change._id}`, LOG_LEVEL_INFO);
|
||||
return;
|
||||
} finally {
|
||||
// Remove from processing queue
|
||||
this._processingChanges.remove(change);
|
||||
this.triggerTakeSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: apply the document to database
|
||||
protected applyToDatabase(doc: PouchDB.Core.ExistingDocument<AnyEntry>) {
|
||||
return this.withCounting(async () => {
|
||||
let releaser: Awaited<ReturnType<typeof this._semaphore.acquire>> | undefined = undefined;
|
||||
try {
|
||||
releaser = await this._semaphore.acquire();
|
||||
await this._applyToDatabase(doc);
|
||||
} catch (e) {
|
||||
this.log(`Error while processing replication result`, LOG_LEVEL_NOTICE);
|
||||
this.logError(e);
|
||||
} finally {
|
||||
// Remove from processing queue (To remove from "in-progress" list, and snapshot will not include it)
|
||||
if (releaser) {
|
||||
releaser();
|
||||
}
|
||||
}
|
||||
}, this.replicator.core.databaseQueueCount);
|
||||
}
|
||||
// Phase 2.1: process the document and apply to storage
|
||||
// This function is serialized per document to avoid race-condition for the same document.
|
||||
private _applyToDatabase(doc_: PouchDB.Core.ExistingDocument<AnyEntry>) {
|
||||
const dbDoc = doc_ as LoadedEntry; // It has no `data`
|
||||
const path = getPath(dbDoc);
|
||||
return serialized(`replication-process:${dbDoc._id}`, async () => {
|
||||
const docNote = `${path} (${shortenId(dbDoc._id)}, ${shortenRev(dbDoc._rev)})`;
|
||||
const isRequired = await this.checkIsChangeRequiredForDatabaseProcessing(dbDoc);
|
||||
if (!isRequired) {
|
||||
this.log(`Skipped (Not latest): ${docNote}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
// If `Read chunks online` is disabled, chunks should be transferred before here.
|
||||
// However, in some cases, chunks are after that. So, if missing chunks exist, we have to wait for them.
|
||||
// (If `Use Only Local Chunks` is enabled, we should not attempt to fetch chunks online automatically).
|
||||
|
||||
const isDeleted = dbDoc._deleted === true || ("deleted" in dbDoc && dbDoc.deleted === true);
|
||||
// Gather full document if not deleted
|
||||
const doc = isDeleted
|
||||
? { ...dbDoc, data: "" }
|
||||
: await this.localDatabase.getDBEntryFromMeta({ ...dbDoc }, false, true);
|
||||
if (!doc) {
|
||||
// Failed to gather content
|
||||
this.log(`Failed to gather content of ${docNote}`, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
// Check if other processor wants to process this document, if so, skip processing here.
|
||||
if (await this.services.replication.processOptionalSynchroniseResult(dbDoc)) {
|
||||
// Already processed
|
||||
this.log(`Processed by other processor: ${docNote}`, LOG_LEVEL_DEBUG);
|
||||
} else if (isValidPath(getPath(doc))) {
|
||||
// Apply to storage if the path is valid
|
||||
await this.applyToStorage(doc as MetaEntry);
|
||||
this.log(`Processed: ${docNote}`, LOG_LEVEL_DEBUG);
|
||||
} else {
|
||||
// Should process, but have an invalid path
|
||||
this.log(`Unprocessed (Invalid path): ${docNote}`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Phase 3: Apply the given entry to storage.
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
protected applyToStorage(entry: MetaEntry) {
|
||||
return this.withCounting(async () => {
|
||||
await this.services.replication.processSynchroniseResult(entry);
|
||||
}, this.replicator.core.storageApplyingCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether processing is required for the given document.
|
||||
* @param dbDoc Document to check
|
||||
* @returns True if processing is required; false otherwise
|
||||
*/
|
||||
protected async checkIsChangeRequiredForDatabaseProcessing(dbDoc: LoadedEntry): Promise<boolean> {
|
||||
const path = getPath(dbDoc);
|
||||
try {
|
||||
const savedDoc = await this.localDatabase.getRaw<LoadedEntry>(dbDoc._id, {
|
||||
conflicts: true,
|
||||
revs_info: true,
|
||||
});
|
||||
const newRev = dbDoc._rev ?? "";
|
||||
const latestRev = savedDoc._rev ?? "";
|
||||
const revisions = savedDoc._revs_info?.map((e) => e.rev) ?? [];
|
||||
if (savedDoc._conflicts && savedDoc._conflicts.length > 0) {
|
||||
// There are conflicts, so we have to process it.
|
||||
// (May auto-resolve or user intervention will be occurred).
|
||||
return true;
|
||||
}
|
||||
if (newRev == latestRev) {
|
||||
// The latest revision. Simply we can process it.
|
||||
return true;
|
||||
}
|
||||
const index = revisions.indexOf(newRev);
|
||||
if (index >= 0) {
|
||||
// The revision has been inserted before.
|
||||
return false; // This means that the document already processed (While no conflict existed).
|
||||
}
|
||||
return true; // This mostly should not happen, but we have to process it just in case.
|
||||
} catch (e: any) {
|
||||
if ("status" in e && e.status == 404) {
|
||||
// getRaw failed due to not existing, it may not be happened normally especially on replication.
|
||||
// If the process caused by some other reason, we **probably** have to process it.
|
||||
// Note that this is not a common case.
|
||||
return true;
|
||||
} else {
|
||||
this.log(
|
||||
`Failed to get existing document for ${path} (${shortenId(dbDoc._id)}, ${shortenRev(dbDoc._rev)}) `,
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
this.logError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { normalizePath } from "../../deps.ts";
|
||||
import {
|
||||
FlagFilesHumanReadable,
|
||||
FlagFilesOriginal,
|
||||
REMOTE_MINIO,
|
||||
TweakValuesShouldMatchedTemplate,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "../../lib/src/common/types.ts";
|
||||
@@ -86,7 +87,7 @@ export class ModuleRedFlag extends AbstractModule {
|
||||
const remoteTweaks = await this.services.tweakValue.fetchRemotePreferred(config);
|
||||
if (!remoteTweaks) {
|
||||
const choice = await this.core.confirm.askSelectStringDialogue(
|
||||
"Could not fetch remote configuration. What do you want to do?",
|
||||
"Could not fetch configuration from remote. If you are new to the Self-hosted LiveSync, this might be expected. If not, you should check your network or server settings.",
|
||||
[SKIP_FETCH, RETRY_FETCH] as const,
|
||||
{
|
||||
defaultAction: RETRY_FETCH,
|
||||
@@ -203,12 +204,13 @@ export class ModuleRedFlag extends AbstractModule {
|
||||
return false;
|
||||
}
|
||||
const { vault, extra } = method;
|
||||
|
||||
// If remote is MinIO, makeLocalChunkBeforeSync is not available. (because no-deduplication on sending).
|
||||
const makeLocalChunkBeforeSyncAvailable = this.settings.remoteType !== REMOTE_MINIO;
|
||||
const mapVaultStateToAction = {
|
||||
identical: {
|
||||
// If both are identical, no need to make local files/chunks before sync,
|
||||
// Just for the efficiency, chunks should be made before sync.
|
||||
makeLocalChunkBeforeSync: true,
|
||||
makeLocalChunkBeforeSync: makeLocalChunkBeforeSyncAvailable,
|
||||
makeLocalFilesBeforeSync: false,
|
||||
},
|
||||
independent: {
|
||||
|
||||
@@ -52,12 +52,16 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
|
||||
}
|
||||
vaultAccess!: SerializedFileAccess;
|
||||
vaultManager: StorageEventManager = new StorageEventManagerObsidian(this.plugin, this.core, this);
|
||||
|
||||
restoreState() {
|
||||
return this.vaultManager.restoreState();
|
||||
}
|
||||
private _everyOnload(): Promise<boolean> {
|
||||
this.core.storageAccess = this;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
_everyOnFirstInitialize(): Promise<boolean> {
|
||||
this.vaultManager.beginWatch();
|
||||
async _everyOnFirstInitialize(): Promise<boolean> {
|
||||
await this.vaultManager.beginWatch();
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
@@ -65,8 +69,8 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
|
||||
// this.vaultManager.flushQueue();
|
||||
// }
|
||||
|
||||
_everyCommitPendingFileEvent(): Promise<boolean> {
|
||||
this.vaultManager.flushQueue();
|
||||
async _everyCommitPendingFileEvent(): Promise<boolean> {
|
||||
await this.vaultManager.waitForIdle();
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,25 +9,20 @@ import {
|
||||
LOG_LEVEL_VERBOSE,
|
||||
type FileEventType,
|
||||
type FilePath,
|
||||
type FilePathWithPrefix,
|
||||
type UXFileInfoStub,
|
||||
type UXInternalFileInfoStub,
|
||||
} from "../../../lib/src/common/types.ts";
|
||||
import { delay, fireAndForget } from "../../../lib/src/common/utils.ts";
|
||||
import { delay, fireAndForget, throttle } from "../../../lib/src/common/utils.ts";
|
||||
import { type FileEventItem } from "../../../common/types.ts";
|
||||
import { serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
||||
import {
|
||||
finishAllWaitingForTimeout,
|
||||
finishWaitingForTimeout,
|
||||
isWaitingForTimeout,
|
||||
waitForTimeout,
|
||||
} from "octagonal-wheels/concurrency/task";
|
||||
import { isWaitingForTimeout } from "octagonal-wheels/concurrency/task";
|
||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
import type { LiveSyncCore } from "../../../main.ts";
|
||||
import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "./utilObsidian.ts";
|
||||
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||
import type { StorageAccess } from "../../interfaces/StorageAccess.ts";
|
||||
import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||
import { promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises";
|
||||
// import { InternalFileToUXFileInfo } from "../platforms/obsidian.ts";
|
||||
|
||||
export type FileEvent = {
|
||||
@@ -36,14 +31,29 @@ export type FileEvent = {
|
||||
oldPath?: string;
|
||||
cachedData?: string;
|
||||
skipBatchWait?: boolean;
|
||||
cancelled?: boolean;
|
||||
};
|
||||
type WaitInfo = {
|
||||
since: number;
|
||||
type: FileEventType;
|
||||
canProceed: PromiseWithResolvers<boolean>;
|
||||
timerHandler: ReturnType<typeof setTimeout>;
|
||||
event: FileEventItem;
|
||||
};
|
||||
const TYPE_SENTINEL_FLUSH = "SENTINEL_FLUSH";
|
||||
type FileEventItemSentinelFlush = {
|
||||
type: typeof TYPE_SENTINEL_FLUSH;
|
||||
};
|
||||
type FileEventItemSentinel = FileEventItemSentinelFlush;
|
||||
|
||||
export abstract class StorageEventManager {
|
||||
abstract beginWatch(): void;
|
||||
abstract flushQueue(): void;
|
||||
abstract beginWatch(): Promise<void>;
|
||||
|
||||
abstract appendQueue(items: FileEvent[], ctx?: any): Promise<void>;
|
||||
abstract cancelQueue(key: string): void;
|
||||
|
||||
abstract isWaiting(filename: FilePath): boolean;
|
||||
abstract waitForIdle(): Promise<void>;
|
||||
abstract restoreState(): Promise<void>;
|
||||
}
|
||||
|
||||
export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
@@ -66,6 +76,13 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
// Necessary evil.
|
||||
cmdHiddenFileSync: HiddenFileSync;
|
||||
|
||||
/**
|
||||
* Snapshot restoration promise.
|
||||
* Snapshot will be restored before starting to watch vault changes.
|
||||
* In designed time, this has been called from Initialisation process, which has been implemented on `ModuleInitializerFile.ts`.
|
||||
*/
|
||||
snapShotRestored: Promise<void> | null = null;
|
||||
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, storageAccess: StorageAccess) {
|
||||
super();
|
||||
this.storageAccess = storageAccess;
|
||||
@@ -73,7 +90,18 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
this.core = core;
|
||||
this.cmdHiddenFileSync = this.plugin.getAddOn(HiddenFileSync.name) as HiddenFileSync;
|
||||
}
|
||||
beginWatch() {
|
||||
|
||||
/**
|
||||
* Restore the previous snapshot if exists.
|
||||
* @returns
|
||||
*/
|
||||
restoreState(): Promise<void> {
|
||||
this.snapShotRestored = this._restoreFromSnapshot();
|
||||
return this.snapShotRestored;
|
||||
}
|
||||
|
||||
async beginWatch() {
|
||||
await this.snapShotRestored;
|
||||
const plugin = this.plugin;
|
||||
this.watchVaultChange = this.watchVaultChange.bind(this);
|
||||
this.watchVaultCreate = this.watchVaultCreate.bind(this);
|
||||
@@ -88,8 +116,6 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
//@ts-ignore : Internal API
|
||||
plugin.registerEvent(plugin.app.vault.on("raw", this.watchVaultRawEvents));
|
||||
plugin.registerEvent(plugin.app.workspace.on("editor-change", this.watchEditorChange));
|
||||
|
||||
// plugin.fileEventQueue.startPipeline();
|
||||
}
|
||||
watchEditorChange(editor: any, info: any) {
|
||||
if (!("path" in info)) {
|
||||
@@ -212,13 +238,13 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
// Cache file and waiting to can be proceed.
|
||||
async appendQueue(params: FileEvent[], ctx?: any) {
|
||||
if (!this.core.settings.isConfigured) return;
|
||||
if (this.core.settings.suspendFileWatching) return;
|
||||
this.core.services.vault.markFileListPossiblyChanged();
|
||||
// Flag up to be reload
|
||||
const processFiles = new Set<FilePath>();
|
||||
for (const param of params) {
|
||||
if (shouldBeIgnored(param.file.path)) {
|
||||
continue;
|
||||
@@ -261,7 +287,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
if (param.cachedData) {
|
||||
cache = param.cachedData;
|
||||
}
|
||||
this.enqueue({
|
||||
void this.enqueue({
|
||||
type,
|
||||
args: {
|
||||
file: file,
|
||||
@@ -272,123 +298,291 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
skipBatchWait: param.skipBatchWait,
|
||||
key: atomicKey,
|
||||
});
|
||||
processFiles.add(file.path as FilePath);
|
||||
if (oldPath) {
|
||||
processFiles.add(oldPath as FilePath);
|
||||
}
|
||||
}
|
||||
for (const path of processFiles) {
|
||||
fireAndForget(() => this.startStandingBy(path));
|
||||
}
|
||||
}
|
||||
bufferedQueuedItems = [] as FileEventItem[];
|
||||
private bufferedQueuedItems = [] as (FileEventItem | FileEventItemSentinel)[];
|
||||
|
||||
/**
|
||||
* Immediately take snapshot.
|
||||
*/
|
||||
private _triggerTakeSnapshot() {
|
||||
void this._takeSnapshot();
|
||||
}
|
||||
/**
|
||||
* Trigger taking snapshot after throttled period.
|
||||
*/
|
||||
triggerTakeSnapshot = throttle(() => this._triggerTakeSnapshot(), 100);
|
||||
|
||||
enqueue(newItem: FileEventItem) {
|
||||
const filename = newItem.args.file.path;
|
||||
if (this.shouldBatchSave) {
|
||||
Logger(`Request cancel for waiting of previous ${filename}`, LOG_LEVEL_DEBUG);
|
||||
finishWaitingForTimeout(`storage-event-manager-batchsave-${filename}`);
|
||||
}
|
||||
this.bufferedQueuedItems.push(newItem);
|
||||
// When deleting or renaming, the queue must be flushed once before processing subsequent processes to prevent unexpected race condition.
|
||||
if (newItem.type == "DELETE") {
|
||||
return this.flushQueue();
|
||||
// If the sentinel pushed, the runQueuedEvents will wait for idle before processing delete.
|
||||
this.bufferedQueuedItems.push({
|
||||
type: TYPE_SENTINEL_FLUSH,
|
||||
});
|
||||
}
|
||||
this.updateStatus();
|
||||
this.bufferedQueuedItems.push(newItem);
|
||||
|
||||
fireAndForget(() => this._takeSnapshot().then(() => this.runQueuedEvents()));
|
||||
}
|
||||
|
||||
// Limit concurrent processing to reduce the IO load. file-processing + scheduler (1), so file events can be processed in 4 slots.
|
||||
concurrentProcessing = Semaphore(5);
|
||||
|
||||
private _waitingMap = new Map<string, WaitInfo>();
|
||||
private _waitForIdle: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* Wait until all queued events are processed.
|
||||
* Subsequent new events will not be waited, but new events will not be added.
|
||||
* @returns
|
||||
*/
|
||||
waitForIdle(): Promise<void> {
|
||||
if (this._waitingMap.size === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (this._waitForIdle) {
|
||||
return this._waitForIdle;
|
||||
}
|
||||
const promises = [...this._waitingMap.entries()].map(([key, waitInfo]) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
waitInfo.canProceed.promise
|
||||
.then(() => {
|
||||
Logger(`Processing ${key}: Wait for idle completed`, LOG_LEVEL_DEBUG);
|
||||
// No op
|
||||
})
|
||||
.catch((e) => {
|
||||
Logger(`Processing ${key}: Wait for idle error`, LOG_LEVEL_INFO);
|
||||
Logger(e, LOG_LEVEL_VERBOSE);
|
||||
//no op
|
||||
})
|
||||
.finally(() => {
|
||||
resolve();
|
||||
});
|
||||
this._proceedWaiting(key);
|
||||
});
|
||||
});
|
||||
const waitPromise = Promise.all(promises).then(() => {
|
||||
this._waitForIdle = null;
|
||||
Logger(`All wait for idle completed`, LOG_LEVEL_VERBOSE);
|
||||
});
|
||||
this._waitForIdle = waitPromise;
|
||||
return waitPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceed waiting for the given key immediately.
|
||||
*/
|
||||
private _proceedWaiting(key: string) {
|
||||
const waitInfo = this._waitingMap.get(key);
|
||||
if (waitInfo) {
|
||||
waitInfo.canProceed.resolve(true);
|
||||
clearTimeout(waitInfo.timerHandler);
|
||||
this._waitingMap.delete(key);
|
||||
}
|
||||
this.triggerTakeSnapshot();
|
||||
}
|
||||
/**
|
||||
* Cancel waiting for the given key.
|
||||
*/
|
||||
private _cancelWaiting(key: string) {
|
||||
const waitInfo = this._waitingMap.get(key);
|
||||
if (waitInfo) {
|
||||
waitInfo.canProceed.resolve(false);
|
||||
clearTimeout(waitInfo.timerHandler);
|
||||
this._waitingMap.delete(key);
|
||||
}
|
||||
this.triggerTakeSnapshot();
|
||||
}
|
||||
/**
|
||||
* Add waiting for the given key.
|
||||
* @param key
|
||||
* @param event
|
||||
* @param waitedSince Optional waited since timestamp to calculate the remaining delay.
|
||||
*/
|
||||
private _addWaiting(key: string, event: FileEventItem, waitedSince?: number): WaitInfo {
|
||||
if (this._waitingMap.has(key)) {
|
||||
// Already waiting
|
||||
throw new Error(`Already waiting for key: ${key}`);
|
||||
}
|
||||
const resolver = promiseWithResolvers<boolean>();
|
||||
const now = Date.now();
|
||||
const since = waitedSince ?? now;
|
||||
const elapsed = now - since;
|
||||
const maxDelay = this.batchSaveMaximumDelay * 1000;
|
||||
const remainingDelay = Math.max(0, maxDelay - elapsed);
|
||||
const nextDelay = Math.min(remainingDelay, this.batchSaveMinimumDelay * 1000);
|
||||
// x*<------- maxDelay --------->*
|
||||
// x*<-- minDelay -->*
|
||||
// x* x<-- nextDelay -->*
|
||||
// x* x<-- Capped-->*
|
||||
// x* x.......*
|
||||
// x: event
|
||||
// *: save
|
||||
// When at event (x) At least, save (*) within maxDelay, but maintain minimum delay between saves.
|
||||
|
||||
if (elapsed >= maxDelay) {
|
||||
// Already exceeded maximum delay, do not wait.
|
||||
Logger(`Processing ${key}: Batch save maximum delay already exceeded: ${event.type}`, LOG_LEVEL_DEBUG);
|
||||
} else {
|
||||
Logger(`Processing ${key}: Adding waiting for batch save: ${event.type} (${nextDelay}ms)`, LOG_LEVEL_DEBUG);
|
||||
}
|
||||
const waitInfo: WaitInfo = {
|
||||
since: since,
|
||||
type: event.type,
|
||||
event: event,
|
||||
canProceed: resolver,
|
||||
timerHandler: setTimeout(() => {
|
||||
Logger(`Processing ${key}: Batch save timeout reached: ${event.type}`, LOG_LEVEL_DEBUG);
|
||||
this._proceedWaiting(key);
|
||||
}, nextDelay),
|
||||
};
|
||||
this._waitingMap.set(key, waitInfo);
|
||||
this.triggerTakeSnapshot();
|
||||
return waitInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given file event.
|
||||
*/
|
||||
async processFileEvent(fei: FileEventItem) {
|
||||
const releaser = await this.concurrentProcessing.acquire();
|
||||
try {
|
||||
this.updateStatus();
|
||||
const filename = fei.args.file.path;
|
||||
const waitingKey = `${filename}`;
|
||||
const previous = this._waitingMap.get(waitingKey);
|
||||
let isShouldBeCancelled = fei.skipBatchWait || false;
|
||||
let previousPromise: Promise<boolean> = Promise.resolve(true);
|
||||
let waitPromise: Promise<boolean> = Promise.resolve(true);
|
||||
// 1. Check if there is previous waiting for the same file
|
||||
if (previous) {
|
||||
previousPromise = previous.canProceed.promise;
|
||||
if (isShouldBeCancelled) {
|
||||
Logger(
|
||||
`Processing ${filename}: Requested to perform immediately, cancelling previous waiting: ${fei.type}`,
|
||||
LOG_LEVEL_DEBUG
|
||||
);
|
||||
}
|
||||
if (!isShouldBeCancelled && fei.type === "DELETE") {
|
||||
// For DELETE, cancel any previous waiting and proceed immediately
|
||||
// That because when deleting, we cannot read the file anymore.
|
||||
Logger(
|
||||
`Processing ${filename}: DELETE requested, cancelling previous waiting: ${fei.type}`,
|
||||
LOG_LEVEL_DEBUG
|
||||
);
|
||||
isShouldBeCancelled = true;
|
||||
}
|
||||
if (!isShouldBeCancelled && previous.type === fei.type) {
|
||||
// For the same type, we can cancel the previous waiting and proceed immediately.
|
||||
Logger(`Processing ${filename}: Cancelling previous waiting: ${fei.type}`, LOG_LEVEL_DEBUG);
|
||||
isShouldBeCancelled = true;
|
||||
}
|
||||
// 2. wait for the previous to complete
|
||||
if (isShouldBeCancelled) {
|
||||
this._cancelWaiting(waitingKey);
|
||||
Logger(`Processing ${filename}: Previous cancelled: ${fei.type}`, LOG_LEVEL_DEBUG);
|
||||
isShouldBeCancelled = true;
|
||||
}
|
||||
if (!isShouldBeCancelled) {
|
||||
Logger(`Processing ${filename}: Waiting for previous to complete: ${fei.type}`, LOG_LEVEL_DEBUG);
|
||||
this._proceedWaiting(waitingKey);
|
||||
Logger(`Processing ${filename}: Previous completed: ${fei.type}`, LOG_LEVEL_DEBUG);
|
||||
}
|
||||
}
|
||||
await previousPromise;
|
||||
// 3. Check if shouldBatchSave is true
|
||||
if (this.shouldBatchSave && !fei.skipBatchWait) {
|
||||
// if type is CREATE or CHANGED, set waiting
|
||||
if (fei.type == "CREATE" || fei.type == "CHANGED") {
|
||||
// 3.2. If true, set the queue, and wait for the waiting, or until timeout
|
||||
// (since is copied from previous waiting if exists to limit the maximum wait time)
|
||||
console.warn(`Since:`, previous?.since);
|
||||
const info = this._addWaiting(waitingKey, fei, previous?.since);
|
||||
waitPromise = info.canProceed.promise;
|
||||
} else if (fei.type == "DELETE") {
|
||||
// For DELETE, cancel any previous waiting and proceed immediately
|
||||
}
|
||||
Logger(`Processing ${filename}: Waiting for batch save: ${fei.type}`, LOG_LEVEL_DEBUG);
|
||||
const canProceed = await waitPromise;
|
||||
if (!canProceed) {
|
||||
// 3.2.1. If cancelled by new queue, cancel subsequent process.
|
||||
Logger(`Processing ${filename}: Cancelled by new queue: ${fei.type}`, LOG_LEVEL_DEBUG);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// await this.handleFileEvent(fei);
|
||||
await this.requestProcessQueue(fei);
|
||||
} finally {
|
||||
await this._takeSnapshot();
|
||||
releaser();
|
||||
}
|
||||
}
|
||||
concurrentProcessing = Semaphore(5);
|
||||
waitedSince = new Map<FilePath | FilePathWithPrefix, number>();
|
||||
async startStandingBy(filename: FilePath) {
|
||||
// If waited, no need to start again (looping inside the function)
|
||||
await skipIfDuplicated(`storage-event-manager-${filename}`, async () => {
|
||||
Logger(`Processing ${filename}: Starting`, LOG_LEVEL_DEBUG);
|
||||
const release = await this.concurrentProcessing.acquire();
|
||||
try {
|
||||
Logger(`Processing ${filename}: Started`, LOG_LEVEL_DEBUG);
|
||||
let noMoreFiles = false;
|
||||
do {
|
||||
const target = this.bufferedQueuedItems.find((e) => e.args.file.path == filename);
|
||||
if (target === undefined) {
|
||||
noMoreFiles = true;
|
||||
break;
|
||||
}
|
||||
const operationType = target.type;
|
||||
async _takeSnapshot() {
|
||||
const processingEvents = [...this._waitingMap.values()].map((e) => e.event);
|
||||
const waitingEvents = this.bufferedQueuedItems;
|
||||
const snapShot = [...processingEvents, ...waitingEvents];
|
||||
await this.core.kvDB.set("storage-event-manager-snapshot", snapShot);
|
||||
Logger(`Storage operation snapshot taken: ${snapShot.length} items`, LOG_LEVEL_DEBUG);
|
||||
this.updateStatus();
|
||||
}
|
||||
async _restoreFromSnapshot() {
|
||||
const snapShot = await this.core.kvDB.get<(FileEventItem | FileEventItemSentinel)[]>(
|
||||
"storage-event-manager-snapshot"
|
||||
);
|
||||
if (snapShot && Array.isArray(snapShot) && snapShot.length > 0) {
|
||||
console.warn(`Restoring snapshot: ${snapShot.length} items`);
|
||||
Logger(`Restoring storage operation snapshot: ${snapShot.length} items`, LOG_LEVEL_VERBOSE);
|
||||
// Restore the snapshot
|
||||
// Note: Mark all items as skipBatchWait to prevent apply the off-line batch saving.
|
||||
this.bufferedQueuedItems = snapShot.map((e) => ({ ...e, skipBatchWait: true }));
|
||||
this.updateStatus();
|
||||
await this.runQueuedEvents();
|
||||
} else {
|
||||
Logger(`No snapshot to restore`, LOG_LEVEL_VERBOSE);
|
||||
// console.warn(`No snapshot to restore`);
|
||||
}
|
||||
}
|
||||
runQueuedEvents() {
|
||||
return skipIfDuplicated("storage-event-manager-run-queued-events", async () => {
|
||||
do {
|
||||
if (this.bufferedQueuedItems.length === 0) {
|
||||
break;
|
||||
}
|
||||
// 1. Get the first queued item
|
||||
|
||||
// if (target.waitedFrom + this.batchSaveMaximumDelay > now) {
|
||||
// this.requestProcessQueue(target);
|
||||
// continue;
|
||||
// }
|
||||
const type = target.type;
|
||||
// If already cancelled by other operation, skip this.
|
||||
if (target.cancelled) {
|
||||
Logger(`Processing ${filename}: Cancelled (scheduled): ${operationType}`, LOG_LEVEL_DEBUG);
|
||||
this.cancelStandingBy(target);
|
||||
continue;
|
||||
}
|
||||
if (!target.skipBatchWait) {
|
||||
if (this.shouldBatchSave && (type == "CREATE" || type == "CHANGED")) {
|
||||
const waitedSince = this.waitedSince.get(filename);
|
||||
let canWait = true;
|
||||
const now = Date.now();
|
||||
if (waitedSince !== undefined) {
|
||||
if (waitedSince + this.batchSaveMaximumDelay * 1000 < now) {
|
||||
Logger(
|
||||
`Processing ${filename}: Could not wait no more: ${operationType}`,
|
||||
LOG_LEVEL_INFO
|
||||
);
|
||||
canWait = false;
|
||||
}
|
||||
}
|
||||
if (canWait) {
|
||||
if (waitedSince === undefined) this.waitedSince.set(filename, now);
|
||||
target.batched = true;
|
||||
Logger(
|
||||
`Processing ${filename}: Waiting for batch save delay: ${operationType}`,
|
||||
LOG_LEVEL_DEBUG
|
||||
);
|
||||
this.updateStatus();
|
||||
const result = await waitForTimeout(
|
||||
`storage-event-manager-batchsave-${filename}`,
|
||||
this.batchSaveMinimumDelay * 1000
|
||||
);
|
||||
if (!result) {
|
||||
Logger(
|
||||
`Processing ${filename}: Cancelled by new queue: ${operationType}`,
|
||||
LOG_LEVEL_DEBUG
|
||||
);
|
||||
// If could not wait for the timeout, possibly we got a new queue. therefore, currently processing one should be cancelled
|
||||
this.cancelStandingBy(target);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger(
|
||||
`Processing ${filename}:Requested to perform immediately ${filename}: ${operationType}`,
|
||||
LOG_LEVEL_DEBUG
|
||||
);
|
||||
}
|
||||
Logger(`Processing ${filename}: Request main to process: ${operationType}`, LOG_LEVEL_DEBUG);
|
||||
await this.requestProcessQueue(target);
|
||||
} while (!noMoreFiles);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
Logger(`Processing ${filename}: Finished`, LOG_LEVEL_DEBUG);
|
||||
const fei = this.bufferedQueuedItems.shift()!;
|
||||
await this._takeSnapshot();
|
||||
this.updateStatus();
|
||||
// 2. Consume 1 semaphore slot to enqueue processing. Then release immediately.
|
||||
// (Just to limit the total concurrent processing count, because skipping batch handles at processFileEvent).
|
||||
const releaser = await this.concurrentProcessing.acquire();
|
||||
releaser();
|
||||
this.updateStatus();
|
||||
// 3. Check if sentinel flush
|
||||
// If sentinel, wait for idle and continue.
|
||||
if (fei.type === TYPE_SENTINEL_FLUSH) {
|
||||
Logger(`Waiting for idle`, LOG_LEVEL_VERBOSE);
|
||||
// Flush all waiting batch queues
|
||||
await this.waitForIdle();
|
||||
this.updateStatus();
|
||||
continue;
|
||||
}
|
||||
// 4. Process the event, this should be fire-and-forget to not block the queue processing in each file.
|
||||
fireAndForget(() => this.processFileEvent(fei));
|
||||
} while (this.bufferedQueuedItems.length > 0);
|
||||
});
|
||||
}
|
||||
|
||||
cancelStandingBy(fei: FileEventItem) {
|
||||
this.bufferedQueuedItems.remove(fei);
|
||||
this.updateStatus();
|
||||
}
|
||||
processingCount = 0;
|
||||
async requestProcessQueue(fei: FileEventItem) {
|
||||
try {
|
||||
this.processingCount++;
|
||||
this.bufferedQueuedItems.remove(fei);
|
||||
// this.bufferedQueuedItems.remove(fei);
|
||||
this.updateStatus();
|
||||
this.waitedSince.delete(fei.args.file.path);
|
||||
// this.waitedSince.delete(fei.args.file.path);
|
||||
await this.handleFileEvent(fei);
|
||||
await this._takeSnapshot();
|
||||
} finally {
|
||||
this.processingCount--;
|
||||
this.updateStatus();
|
||||
@@ -397,27 +591,26 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
isWaiting(filename: FilePath) {
|
||||
return isWaitingForTimeout(`storage-event-manager-batchsave-${filename}`);
|
||||
}
|
||||
flushQueue() {
|
||||
this.bufferedQueuedItems.forEach((e) => (e.skipBatchWait = true));
|
||||
finishAllWaitingForTimeout("storage-event-manager-batchsave-", true);
|
||||
}
|
||||
cancelQueue(key: string) {
|
||||
this.bufferedQueuedItems.forEach((e) => {
|
||||
if (e.key === key) e.skipBatchWait = true;
|
||||
});
|
||||
}
|
||||
|
||||
updateStatus() {
|
||||
const allItems = this.bufferedQueuedItems.filter((e) => !e.cancelled);
|
||||
const batchedCount = allItems.filter((e) => e.batched && !e.skipBatchWait).length;
|
||||
const allFileEventItems = this.bufferedQueuedItems.filter((e): e is FileEventItem => "args" in e);
|
||||
const allItems = allFileEventItems.filter((e) => !e.cancelled);
|
||||
const totalItems = allItems.length + this.concurrentProcessing.waiting;
|
||||
const processing = this.processingCount;
|
||||
const batchedCount = this._waitingMap.size;
|
||||
this.core.batched.value = batchedCount;
|
||||
this.core.processing.value = this.processingCount;
|
||||
this.core.totalQueued.value = allItems.length - batchedCount;
|
||||
this.core.processing.value = processing;
|
||||
this.core.totalQueued.value = totalItems + batchedCount + processing;
|
||||
}
|
||||
|
||||
async handleFileEvent(queue: FileEventItem): Promise<any> {
|
||||
const file = queue.args.file;
|
||||
const lockKey = `handleFile:${file.path}`;
|
||||
return await serialized(lockKey, async () => {
|
||||
const ret = await serialized(lockKey, async () => {
|
||||
if (queue.cancelled) {
|
||||
Logger(`File event cancelled before processing: ${file.path}`, LOG_LEVEL_INFO);
|
||||
return;
|
||||
}
|
||||
if (queue.type == "INTERNAL" || file.isInternal) {
|
||||
await this.core.services.fileProcessing.processOptionalFileEvent(file.path as unknown as FilePath);
|
||||
} else {
|
||||
@@ -444,9 +637,11 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
}
|
||||
}
|
||||
});
|
||||
this.updateStatus();
|
||||
return ret;
|
||||
}
|
||||
|
||||
cancelRelativeEvent(item: FileEventItem): void {
|
||||
this.cancelQueue(item.key);
|
||||
this._cancelWaiting(item.args.file.path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ export class ModuleInitializerFile extends AbstractModule {
|
||||
if (showingNotice) {
|
||||
this._log("Initializing", LOG_LEVEL_NOTICE, "syncAll");
|
||||
}
|
||||
if (isInitialized) {
|
||||
this._log("Restoring storage state", LOG_LEVEL_VERBOSE);
|
||||
await this.core.storageAccess.restoreState();
|
||||
}
|
||||
|
||||
this._log("Initialize and checking database files");
|
||||
this._log("Checking deleted files");
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import { sizeToHumanReadable } from "octagonal-wheels/number";
|
||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||
import { EVENT_REQUEST_CHECK_REMOTE_SIZE, eventHub } from "@/common/events.ts";
|
||||
|
||||
export class ModuleCheckRemoteSize extends AbstractModule {
|
||||
async _allScanStat(): Promise<boolean> {
|
||||
export class ModuleCheckRemoteSize extends AbstractObsidianModule {
|
||||
checkRemoteSize(): Promise<boolean> {
|
||||
this.settings.notifyThresholdOfRemoteStorageSize = 1;
|
||||
return this._allScanStat();
|
||||
}
|
||||
|
||||
private async _allScanStat(): Promise<boolean> {
|
||||
if (this.core.managers.networkManager.isOnline === false) {
|
||||
this._log("Network is offline, skipping remote size check.", LOG_LEVEL_INFO);
|
||||
return true;
|
||||
@@ -109,7 +115,20 @@ export class ModuleCheckRemoteSize extends AbstractModule {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _everyOnloadStart(): Promise<boolean> {
|
||||
this.addCommand({
|
||||
id: "livesync-reset-remote-size-threshold-and-check",
|
||||
name: "Reset notification threshold and check the remote database usage",
|
||||
callback: async () => {
|
||||
await this.checkRemoteSize();
|
||||
},
|
||||
});
|
||||
eventHub.onEvent(EVENT_REQUEST_CHECK_REMOTE_SIZE, () => this.checkRemoteSize());
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||
services.appLifecycle.handleOnScanningStartupIssues(this._allScanStat.bind(this));
|
||||
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,20 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
async __fetchByAPI(url: string, authHeader: string, opts?: RequestInit): Promise<Response> {
|
||||
const body = opts?.body as string;
|
||||
|
||||
const transformedHeaders = { ...(opts?.headers as Record<string, string>) };
|
||||
const optHeaders = {} as Record<string, string>;
|
||||
if (opts && "headers" in opts) {
|
||||
if (opts.headers instanceof Headers) {
|
||||
// For Compatibility, mostly headers.entries() is supported, but not all environments.
|
||||
opts.headers.forEach((value, key) => {
|
||||
optHeaders[key] = value;
|
||||
});
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(opts.headers as Record<string, string>)) {
|
||||
optHeaders[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
const transformedHeaders = { ...optHeaders };
|
||||
if (authHeader != "") transformedHeaders["authorization"] = authHeader;
|
||||
delete transformedHeaders["host"];
|
||||
delete transformedHeaders["Host"];
|
||||
@@ -132,7 +145,6 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
return "Network is offline";
|
||||
}
|
||||
// let authHeader = await this._authHeader.getAuthorizationHeader(auth);
|
||||
|
||||
const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = {
|
||||
adapter: "http",
|
||||
auth: "username" in auth ? auth : undefined,
|
||||
@@ -166,7 +178,6 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
if (!("username" in auth)) {
|
||||
headers.append("authorization", authHeader);
|
||||
}
|
||||
|
||||
try {
|
||||
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
|
||||
const response: Response = await (useRequestAPI
|
||||
|
||||
@@ -90,10 +90,8 @@ export class ConflictResolveModal extends Modal {
|
||||
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
|
||||
const date2 =
|
||||
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
|
||||
div2.setHTMLUnsafe(`
|
||||
<span class='deleted'><span class='conflict-dev-name'>${this.localName}</span>: ${date1}</span><br>
|
||||
<span class='added'><span class='conflict-dev-name'>${this.remoteName}</span>: ${date2}</span><br>
|
||||
`);
|
||||
div2.innerHTML = `<span class='deleted'><span class='conflict-dev-name'>${this.localName}</span>: ${date1}</span><br>
|
||||
<span class='added'><span class='conflict-dev-name'>${this.remoteName}</span>: ${date2}</span><br>`;
|
||||
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) =>
|
||||
e.addEventListener("click", () => this.sendResponse(this.result.right.rev))
|
||||
).style.marginRight = "4px";
|
||||
@@ -109,11 +107,10 @@ export class ConflictResolveModal extends Modal {
|
||||
e.addEventListener("click", () => this.sendResponse(CANCELLED))
|
||||
).style.marginRight = "4px";
|
||||
diff = diff.replace(/\n/g, "<br>");
|
||||
// div.innerHTML = diff;
|
||||
if (diff.length > 100 * 1024) {
|
||||
div.setText("(Too large diff to display)");
|
||||
div.innerText = "(Too large diff to display)";
|
||||
} else {
|
||||
div.setHTMLUnsafe(diff);
|
||||
div.innerHTML = diff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,12 @@ import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
import { P2PLogCollector } from "../../lib/src/replication/trystero/P2PReplicatorCore.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import { LiveSyncError } from "@/lib/src/common/LSError.ts";
|
||||
import { isValidPath } from "@/common/utils.ts";
|
||||
import {
|
||||
isValidFilenameInAndroid,
|
||||
isValidFilenameInDarwin,
|
||||
isValidFilenameInWidows,
|
||||
} from "@/lib/src/string_and_binary/path.ts";
|
||||
|
||||
// This module cannot be a core module because it depends on the Obsidian UI.
|
||||
|
||||
@@ -229,19 +235,45 @@ export class ModuleLog extends AbstractObsidianModule {
|
||||
}
|
||||
|
||||
async getActiveFileStatus() {
|
||||
const reason = [] as string[];
|
||||
const reasonWarn = [] as string[];
|
||||
const thisFile = this.app.workspace.getActiveFile();
|
||||
if (!thisFile) return "";
|
||||
const validPath = isValidPath(thisFile.path);
|
||||
if (!validPath) {
|
||||
reason.push("This file has an invalid path under the current settings");
|
||||
} else {
|
||||
// The most narrow check: Filename validity on Windows
|
||||
const validOnWindows = isValidFilenameInWidows(thisFile.name);
|
||||
const validOnDarwin = isValidFilenameInDarwin(thisFile.name);
|
||||
const validOnAndroid = isValidFilenameInAndroid(thisFile.name);
|
||||
const labels = [];
|
||||
if (!validOnWindows) labels.push("🪟");
|
||||
if (!validOnDarwin) labels.push("🍎");
|
||||
if (!validOnAndroid) labels.push("🤖");
|
||||
if (labels.length > 0) {
|
||||
reasonWarn.push("Some platforms may be unable to process this file correctly: " + labels.join(" "));
|
||||
}
|
||||
}
|
||||
// Case Sensitivity
|
||||
if (this.services.setting.shouldCheckCaseInsensitively()) {
|
||||
const f = this.core.storageAccess
|
||||
.getFiles()
|
||||
.map((e) => e.path)
|
||||
.filter((e) => e.toLowerCase() == thisFile.path.toLowerCase());
|
||||
if (f.length > 1) return "Not synchronised: There are multiple files with the same name";
|
||||
if (f.length > 1) {
|
||||
reason.push("There are multiple files with the same name (case-insensitive match)");
|
||||
}
|
||||
}
|
||||
if (!(await this.services.vault.isTargetFile(thisFile.path))) return "Not synchronised: not a target file";
|
||||
if (this.services.vault.isFileSizeTooLarge(thisFile.stat.size)) return "Not synchronised: File size exceeded";
|
||||
return "";
|
||||
if (!(await this.services.vault.isTargetFile(thisFile.path))) {
|
||||
reason.push("This file is ignored by the ignore rules");
|
||||
}
|
||||
if (this.services.vault.isFileSizeTooLarge(thisFile.stat.size)) {
|
||||
reason.push("This file size exceeds the configured limit");
|
||||
}
|
||||
const result = reason.length > 0 ? "Not synchronised: " + reason.join(", ") : "";
|
||||
const warnResult = reasonWarn.length > 0 ? "Warning: " + reasonWarn.join(", ") : "";
|
||||
return [result, warnResult].filter((e) => e).join("\n");
|
||||
}
|
||||
async setFileStatus() {
|
||||
const fileStatus = await this.getActiveFileStatus();
|
||||
|
||||
@@ -22,6 +22,9 @@ export function paneAdvanced(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
|
||||
new Setting(paneEl)
|
||||
.setClass("wizardHidden")
|
||||
.autoWireToggle("readChunksOnline", { onUpdate: this.onlyOnCouchDB });
|
||||
new Setting(paneEl)
|
||||
.setClass("wizardHidden")
|
||||
.autoWireToggle("useOnlyLocalChunk", { onUpdate: this.onlyOnCouchDB });
|
||||
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("concurrencyOfReadChunksOnline", {
|
||||
clampMin: 10,
|
||||
|
||||
@@ -26,7 +26,13 @@ import { addPrefix, shouldBeIgnored, stripAllPrefixes } from "../../../lib/src/s
|
||||
import { $msg } from "../../../lib/src/common/i18n.ts";
|
||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import { EVENT_REQUEST_RUN_DOCTOR, EVENT_REQUEST_RUN_FIX_INCOMPLETE, eventHub } from "../../../common/events.ts";
|
||||
import {
|
||||
EVENT_ANALYSE_DB_USAGE,
|
||||
EVENT_REQUEST_CHECK_REMOTE_SIZE,
|
||||
EVENT_REQUEST_RUN_DOCTOR,
|
||||
EVENT_REQUEST_RUN_FIX_INCOMPLETE,
|
||||
eventHub,
|
||||
} from "../../../common/events.ts";
|
||||
import { ICHeader, ICXHeader, PSCHeader } from "../../../common/types.ts";
|
||||
import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||
import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts";
|
||||
@@ -182,6 +188,24 @@ ${stringifyYaml({
|
||||
}
|
||||
})
|
||||
);
|
||||
new Setting(paneEl)
|
||||
.setName("Analyse database usage")
|
||||
.setDesc(
|
||||
"Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like."
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText("Analyse").onClick(() => {
|
||||
eventHub.emitEvent(EVENT_ANALYSE_DB_USAGE);
|
||||
})
|
||||
);
|
||||
new Setting(paneEl)
|
||||
.setName("Reset notification threshold and check the remote database usage")
|
||||
.setDesc("Reset the remote storage size threshold and check the remote storage size again.")
|
||||
.addButton((button) =>
|
||||
button.setButtonText("Check").onClick(() => {
|
||||
eventHub.emitEvent(EVENT_REQUEST_CHECK_REMOTE_SIZE);
|
||||
})
|
||||
);
|
||||
new Setting(paneEl).autoWireToggle("writeLogToTheFile");
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
import type { CustomRegExp } from "../../lib/src/common/utils";
|
||||
|
||||
export interface StorageAccess {
|
||||
restoreState(): Promise<void>;
|
||||
processWriteFile<T>(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise<T>): Promise<T>;
|
||||
processReadFile<T>(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise<T>): Promise<T>;
|
||||
isFileProcessing(file: UXFileInfoStub | FilePathWithPrefix): boolean;
|
||||
|
||||
@@ -4,7 +4,7 @@ if you want to view the source, please visit the github repository of this plugi
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
const prod = process.argv[2] === "production" || process.env?.BUILD_MODE === "production";
|
||||
/***
|
||||
* @type import("terser").MinifyOptions
|
||||
*/
|
||||
|
||||
87
updates.md
87
updates.md
@@ -1,9 +1,88 @@
|
||||
# 0.25
|
||||
|
||||
Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
|
||||
|
||||
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
||||
|
||||
## 0.25.35
|
||||
|
||||
24th December, 2025
|
||||
|
||||
Sorry for a small release! I would like to keep things moving along like this if possible. After all, the holidays seem to be starting soon. I will be doubled by my business until the 27th though, indeed.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Now the conflict resolution dialogue shows correctly which device only has older APIs (#764).
|
||||
|
||||
## 0.25.34
|
||||
|
||||
10th December, 2025
|
||||
|
||||
### Behaviour change
|
||||
|
||||
- The plug-in automatically fetches the missing chunks even if `Fetch chunks on demand` is disabled.
|
||||
- This change is to avoid loss of data when receiving a bulk of revisions.
|
||||
- This can be prevented by enabling `Use Only Local Chunks` in the settings.
|
||||
- Storage application now saved during each event and restored on startup.
|
||||
- Synchronisation result application is also now saved during each event and restored on startup.
|
||||
- These may avoid some unexpected loss of data when the editor crashes.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Now the plug-in waits for the application of pended batch changes before the synchronisation starts.
|
||||
- This may avoid some unexpected loss or unexpected conflicts.
|
||||
Plug-in sends custom headers correctly when RequestAPI is used.
|
||||
- No longer causing unexpected chunk creation during `Reset synchronisation on This Device` with bucket sync.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Synchronisation result application process has been refactored.
|
||||
- Storage application process has been refactored.
|
||||
- Please report if you find any unexpected behaviour after this update. A bit of large refactoring.
|
||||
|
||||
## 0.25.33
|
||||
|
||||
05th December, 2025
|
||||
|
||||
### New feature
|
||||
|
||||
- We can analyse the local database with the `Analyse database usage` command.
|
||||
- This command makes a TSV-style report of the database usage, which can be pasted into spreadsheet applications.
|
||||
- The report contains the number of unique chunks and shared chunks for each document revision.
|
||||
- Unique chunks indicate the actual consumption.
|
||||
- Shared chunks indicate the reference counts from other chunks with no consumption.
|
||||
- We can find which notes or files are using large amounts of storage in the database. Or which notes cannot share chunks effectively.
|
||||
- This command is useful when optimising the database size or investigating an unexpectedly large database size.
|
||||
- We can reset the notification threshold and check the remote usage at once with the `Reset notification threshold and check the remote database usage` command.
|
||||
- Commands are available from the Command Palette, or `Hatch` pane in the settings dialogue.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Now the plug-in resets the remote size notification threshold after rebuild.
|
||||
|
||||
## 0.25.32
|
||||
|
||||
02nd December, 2025
|
||||
|
||||
Now I am back from a short (?) break! Thank you all for your patience. (It is nothing major, but the first half of the year has finally come to an end).
|
||||
Anyway, I will release the things a bit by bit. I think that we need a rehabilitation or getting gears in again.
|
||||
|
||||
### Improved
|
||||
|
||||
- Now the plugin warns when we are in several file-related situations that may cause unexpected behaviour (#300).
|
||||
- These errors are displayed alongside issues such as file size exceeding limits.
|
||||
- Such situations include:
|
||||
- When the document has a name which is not supported by some file systems.
|
||||
- When the vault has the same file names with different letter cases.
|
||||
|
||||
## 0.25.31
|
||||
|
||||
18th November, 2025
|
||||
|
||||
### Fixed
|
||||
|
||||
- Now fetching configuration from the server can handle the empty remote correctly (reported on #756).
|
||||
- No longer asking to switch adapters during rebuilding.
|
||||
|
||||
## 0.25.30
|
||||
|
||||
17th November, 2025
|
||||
@@ -14,19 +93,22 @@ So sorry for the quick follow-up release, due to a humble mistake in a quick cau
|
||||
|
||||
- Now we can save settings correctly again (#756).
|
||||
|
||||
|
||||
## ~~0.25.28~~ 0.25.29
|
||||
|
||||
(0.25.28 was skipped due to a packaging issue.)
|
||||
|
||||
17th November, 2025
|
||||
|
||||
### New feature
|
||||
|
||||
- We can now configure hidden file synchronisation to always overwrite with the latest version (#579).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Timing dependency issues during initialisation have been mitigated (#714)
|
||||
|
||||
### Improved
|
||||
|
||||
- Error logs now contain stack-traces for better inspection.
|
||||
|
||||
## 0.25.27
|
||||
@@ -47,7 +129,6 @@ So sorry for the quick follow-up release, due to a humble mistake in a quick cau
|
||||
|
||||
- Reverted QR code library to v1.4.4 (To make sure #752).
|
||||
|
||||
|
||||
## 0.25.26
|
||||
|
||||
07th November, 2025
|
||||
|
||||
Reference in New Issue
Block a user