diff --git a/.eslintignore b/.eslintignore index 7b703be..a6115c3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,8 @@ node_modules build .eslintrc.js.bak src/lib/src/patches/pouchdb-utils -esbuild.config.mjs \ No newline at end of file +esbuild.config.mjs +rollup.config.js +src/lib/test +src/lib/src/cli +main.js diff --git a/.eslintrc b/.eslintrc index 08e619d..64c653b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,20 +1,13 @@ { "root": true, "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], + "plugins": ["@typescript-eslint"], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended"], "parserOptions": { "sourceType": "module", - "project": [ - "tsconfig.json" - ] + "project": ["tsconfig.json"] }, + "ignorePatterns": [], "rules": { "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ @@ -28,9 +21,17 @@ "no-prototype-builtins": "off", "@typescript-eslint/no-empty-function": "off", "require-await": "warn", - "no-async-promise-executor": "off", + "@typescript-eslint/require-await": "warn", + "@typescript-eslint/no-misused-promises": "warn", + "@typescript-eslint/no-floating-promises": "warn", + "no-async-promise-executor": "warn", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unnecessary-type-assertion": "error", - "no-constant-condition": ["error", { "checkLoops": false }] - } + "no-constant-condition": [ + "error", + { + "checkLoops": false + } + ] } +} diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 1bfcd9e..495c887 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -12,7 +12,7 @@ import inlineWorkerPlugin from "esbuild-plugin-inline-worker"; import { terserOption } from "./terser.config.mjs"; const prod = process.argv[2] === "production"; -const keepTest = !prod; +const keepTest = true; //!prod; const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + ""); const packageJson = JSON.parse(fs.readFileSync("./package.json") + ""); diff --git a/package-lock.json b/package-lock.json index 919fc63..7eaa799 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.23.23", + "version": "0.24.0.dev-rc8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.23.23", + "version": "0.24.0.dev-rc8", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.645.0", @@ -19,6 +19,7 @@ "idb": "^8.0.0", "minimatch": "^10.0.1", "octagonal-wheels": "^0.1.15", + "svelte-check": "^4.0.4", "xxhash-wasm": "0.4.2", "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" }, @@ -78,7 +79,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -1578,7 +1578,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -1592,7 +1591,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -1601,7 +1599,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -1619,14 +1616,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2408,8 +2403,7 @@ "node_modules/@types/estree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", - "dev": true + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -2848,7 +2842,6 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2915,7 +2908,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, "dependencies": { "dequal": "^2.0.3" } @@ -3055,7 +3047,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", - "dev": true, "dependencies": { "dequal": "^2.0.3" } @@ -3154,11 +3145,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/code-red": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.3.tgz", "integrity": "sha512-kVwJELqiILQyG5aeuyKFbdsI1fmQy1Cmf7dQ8eGmVuJoaRVdwey7WaMknr2ZFeVSYSKT0rExsa8EGw0aoI/1QQ==", - "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.14", "@types/estree": "^1.0.0", @@ -3226,7 +3230,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" @@ -3350,7 +3353,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } @@ -3916,7 +3918,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, "dependencies": { "@types/estree": "^1.0.0" } @@ -4622,7 +4623,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.1.tgz", "integrity": "sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==", - "dev": true, "dependencies": { "@types/estree": "*" } @@ -4791,8 +4791,7 @@ "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, "node_modules/locate-path": { "version": "6.0.0", @@ -4819,7 +4818,6 @@ "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", - "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -4852,8 +4850,7 @@ "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, "node_modules/merge2": { "version": "1.4.1", @@ -4931,6 +4928,14 @@ "node": "*" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5220,7 +5225,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dev": true, "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", @@ -5230,8 +5234,7 @@ "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -5728,6 +5731,18 @@ } ] }, + "node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -5826,6 +5841,17 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -5964,7 +5990,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6111,7 +6136,6 @@ "version": "4.2.19", "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.1", @@ -6133,6 +6157,54 @@ "node": ">=16" } }, + "node_modules/svelte-check": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.5.tgz", + "integrity": "sha512-icBTBZ3ibBaywbXUat3cK6hB5Du+Kq9Z8CRuyLmm64XIe2/r+lQcbuBx/IQgsbrC+kT2jQ0weVpZSSRIPwB6jQ==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-check/node_modules/fdir": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", + "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/svelte-check/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/svelte-preprocess": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.2.tgz", @@ -6389,7 +6461,6 @@ "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6591,7 +6662,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -7624,7 +7694,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, "requires": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -7634,14 +7703,12 @@ "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" }, "@jridgewell/set-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" }, "@jridgewell/source-map": { "version": "0.3.5", @@ -7656,14 +7723,12 @@ "@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -8267,8 +8332,7 @@ "@types/estree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", - "dev": true + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" }, "@types/json5": { "version": "0.0.29", @@ -8605,8 +8669,7 @@ "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" }, "acorn-jsx": { "version": "5.3.2", @@ -8652,7 +8715,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, "requires": { "dequal": "^2.0.3" } @@ -8748,7 +8810,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", - "dev": true, "requires": { "dequal": "^2.0.3" } @@ -8821,11 +8882,18 @@ "supports-color": "^7.1.0" } }, + "chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "requires": { + "readdirp": "^4.0.1" + } + }, "code-red": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.3.tgz", "integrity": "sha512-kVwJELqiILQyG5aeuyKFbdsI1fmQy1Cmf7dQ8eGmVuJoaRVdwey7WaMknr2ZFeVSYSKT0rExsa8EGw0aoI/1QQ==", - "dev": true, "requires": { "@jridgewell/sourcemap-codec": "^1.4.14", "@types/estree": "^1.0.0", @@ -8887,7 +8955,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, "requires": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" @@ -8966,8 +9033,7 @@ "dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" }, "detect-libc": { "version": "1.0.3", @@ -9408,7 +9474,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, "requires": { "@types/estree": "^1.0.0" } @@ -9906,7 +9971,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.1.tgz", "integrity": "sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==", - "dev": true, "requires": { "@types/estree": "*" } @@ -10027,8 +10091,7 @@ "locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, "locate-path": { "version": "6.0.0", @@ -10049,7 +10112,6 @@ "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", - "dev": true, "requires": { "@jridgewell/sourcemap-codec": "^1.4.15" } @@ -10072,8 +10134,7 @@ "mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, "merge2": { "version": "1.4.1", @@ -10126,6 +10187,11 @@ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", "dev": true }, + "mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -10324,7 +10390,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dev": true, "requires": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", @@ -10334,8 +10399,7 @@ "picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "picomatch": { "version": "2.3.1", @@ -10747,6 +10811,11 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==" + }, "regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -10806,6 +10875,14 @@ "queue-microtask": "^1.2.2" } }, + "sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "requires": { + "mri": "^1.1.0" + } + }, "safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -10903,8 +10980,7 @@ "source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" }, "source-map-support": { "version": "0.5.21", @@ -11008,7 +11084,6 @@ "version": "4.2.19", "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", - "dev": true, "requires": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -11026,6 +11101,33 @@ "periscopic": "^3.1.0" } }, + "svelte-check": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.5.tgz", + "integrity": "sha512-icBTBZ3ibBaywbXUat3cK6hB5Du+Kq9Z8CRuyLmm64XIe2/r+lQcbuBx/IQgsbrC+kT2jQ0weVpZSSRIPwB6jQ==", + "requires": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "dependencies": { + "fdir": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", + "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "requires": {} + }, + "picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "optional": true, + "peer": true + } + } + }, "svelte-preprocess": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.2.tgz", @@ -11181,8 +11283,7 @@ "typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==" }, "unbox-primitive": { "version": "1.0.2", diff --git a/package.json b/package.json index 257daad..2d66f3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.23.23", + "version": "0.24.0.dev-rc8", "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", @@ -8,7 +8,10 @@ "dev": "node esbuild.config.mjs", "build": "node esbuild.config.mjs production", "buildDev": "node esbuild.config.mjs dev", - "lint": "eslint src" + "lint": "eslint src", + "svelte-check": "svelte-check --tsconfig ./tsconfig.json", + "tsc-check": "tsc --noEmit", + "check": "npm run lint && npm run svelte-check && npm run tsc-check" }, "keywords": [], "author": "vorotamoroz", @@ -66,7 +69,8 @@ "idb": "^8.0.0", "minimatch": "^10.0.1", "octagonal-wheels": "^0.1.15", + "svelte-check": "^4.0.4", "xxhash-wasm": "0.4.2", "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" } -} \ No newline at end of file +} diff --git a/src/common/events.ts b/src/common/events.ts index 15baf5d..97a1d1a 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -3,9 +3,22 @@ export const EVENT_PLUGIN_LOADED = "plugin-loaded"; export const EVENT_PLUGIN_UNLOADED = "plugin-unloaded"; export const EVENT_SETTING_SAVED = "setting-saved"; export const EVENT_FILE_RENAMED = "file-renamed"; - +export const EVENT_FILE_SAVED = "file-saved"; export const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed"; +export const EVENT_LOG_ADDED = "log-added"; + +export const EVENT_REQUEST_OPEN_SETTINGS = "request-open-settings"; +export const EVENT_REQUEST_OPEN_SETTING_WIZARD = "request-open-setting-wizard"; +export const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri"; +export const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri"; + +export const EVENT_REQUEST_SHOW_HISTORY = "show-history"; + +export const EVENT_REQUEST_RELOAD_SETTING_TAB = "reload-setting-tab"; + +export const EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG = "request-open-plugin-sync-dialog"; + // export const EVENT_FILE_CHANGED = "file-changed"; diff --git a/src/common/types.ts b/src/common/types.ts index 2d0f118..c2aacf2 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,5 +1,5 @@ import { type PluginManifest, TFile } from "../deps.ts"; -import { type DatabaseEntry, type EntryBody, type FilePath } from "../lib/src/common/types.ts"; +import { type DatabaseEntry, type EntryBody, type FilePath, type UXFileInfoStub, type UXInternalFileInfoStub } from "../lib/src/common/types.ts"; export interface PluginDataEntry extends DatabaseEntry { deviceVaultName: string; @@ -49,9 +49,9 @@ export type queueItem = { }; export type CacheData = string | ArrayBuffer; -export type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL"; +export type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "INTERNAL"; export type FileEventArgs = { - file: FileInfo | InternalFileInfo; + file: UXFileInfoStub | UXInternalFileInfoStub; cache?: CacheData; oldPath?: string; ctx?: any; diff --git a/src/common/utils.ts b/src/common/utils.ts index 38cc920..7e25921 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,10 +1,9 @@ -import { normalizePath, Platform, TAbstractFile, App, type RequestUrlParam, requestUrl, TFile } from "../deps.ts"; +import { normalizePath, Platform, TAbstractFile, type RequestUrlParam, requestUrl } from "../deps.ts"; import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid, stripAllPrefixes } from "../lib/src/string_and_binary/path.ts"; import { Logger } from "../lib/src/common/logger.ts"; -import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "../lib/src/common/types.ts"; +import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix, type UXFileInfo, type UXFileInfoStub } from "../lib/src/common/types.ts"; import { CHeader, ICHeader, ICHeaderLength, ICXHeader, PSCHeader } from "./types.ts"; -import { InputStringDialog, PopoverSelectString } from "./dialogs.ts"; import type ObsidianLiveSyncPlugin from "../main.ts"; import { writeString } from "../lib/src/string_and_binary/convert.ts"; import { fireAndForget } from "../lib/src/common/utils.ts"; @@ -46,6 +45,10 @@ export function getPathWithoutPrefix(entry: AnyEntry) { export function getPathFromTFile(file: TAbstractFile) { return file.path as FilePath; } +export function getPathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWithPrefix) { + if (typeof file == "string") return file as FilePathWithPrefix; + return file.path; +} const memos: { [key: string]: any } = {}; @@ -72,206 +75,6 @@ export function disposeMemoObject(key: string) { delete memos[key]; } -export function isSensibleMargeApplicable(path: string) { - if (path.endsWith(".md")) return true; - return false; -} -export function isObjectMargeApplicable(path: string) { - if (path.endsWith(".canvas")) return true; - if (path.endsWith(".json")) return true; - return false; -} - -export function tryParseJSON(str: string, fallbackValue?: any) { - try { - return JSON.parse(str); - } catch (ex) { - return fallbackValue; - } -} - -const MARK_OPERATOR = `\u{0001}`; -const MARK_DELETED = `${MARK_OPERATOR}__DELETED`; -const MARK_ISARRAY = `${MARK_OPERATOR}__ARRAY`; -const MARK_SWAPPED = `${MARK_OPERATOR}__SWAP`; - -function unorderedArrayToObject(obj: Array) { - return obj.map(e => ({ [e.id as string]: e })).reduce((p, c) => ({ ...p, ...c }), {}) -} -function objectToUnorderedArray(obj: object) { - const entries = Object.entries(obj); - if (entries.some(e => e[0] != e[1]?.id)) throw new Error("Item looks like not unordered array") - return entries.map(e => e[1]); -} -function generatePatchUnorderedArray(from: Array, to: Array) { - if (from.every(e => typeof (e) == "object" && ("id" in e)) && to.every(e => typeof (e) == "object" && ("id" in e))) { - const fObj = unorderedArrayToObject(from); - const tObj = unorderedArrayToObject(to); - const diff = generatePatchObj(fObj, tObj); - if (Object.keys(diff).length > 0) { - return { [MARK_ISARRAY]: diff }; - } else { - return {}; - } - } - return { [MARK_SWAPPED]: to }; -} - -export function generatePatchObj(from: Record, to: Record) { - const entries = Object.entries(from); - const tempMap = new Map(entries); - const ret = {} as Record; - const newEntries = Object.entries(to); - for (const [key, value] of newEntries) { - if (!tempMap.has(key)) { - //New - ret[key] = value; - tempMap.delete(key); - } else { - //Exists - const v = tempMap.get(key); - if (typeof (v) !== typeof (value) || (Array.isArray(v) !== Array.isArray(value))) { - //if type is not match, replace completely. - ret[key] = { [MARK_SWAPPED]: value }; - } else { - if (v === null && value === null) { - // NO OP. - } else if (v === null && value !== null) { - ret[key] = { [MARK_SWAPPED]: value }; - } else if (v !== null && value === null) { - ret[key] = { [MARK_SWAPPED]: value }; - } else if (typeof (v) == "object" && typeof (value) == "object" && !Array.isArray(v) && !Array.isArray(value)) { - const wk = generatePatchObj(v, value); - if (Object.keys(wk).length > 0) ret[key] = wk; - } else if (typeof (v) == "object" && typeof (value) == "object" && Array.isArray(v) && Array.isArray(value)) { - const wk = generatePatchUnorderedArray(v, value); - if (Object.keys(wk).length > 0) ret[key] = wk; - } else if (typeof (v) != "object" && typeof (value) != "object") { - if (JSON.stringify(tempMap.get(key)) !== JSON.stringify(value)) { - ret[key] = value; - } - } else { - if (JSON.stringify(tempMap.get(key)) !== JSON.stringify(value)) { - ret[key] = { [MARK_SWAPPED]: value }; - } - } - } - tempMap.delete(key); - } - } - //Not used item, means deleted one - for (const [key,] of tempMap) { - ret[key] = MARK_DELETED - } - return ret; -} - - -export function applyPatch(from: Record, patch: Record) { - const ret = from; - const patches = Object.entries(patch); - for (const [key, value] of patches) { - if (value == MARK_DELETED) { - delete ret[key]; - continue; - } - if (value === null) { - ret[key] = null; - continue; - } - if (typeof (value) == "object") { - if (MARK_SWAPPED in value) { - ret[key] = value[MARK_SWAPPED]; - continue; - } - if (MARK_ISARRAY in value) { - if (!(key in ret)) ret[key] = []; - if (!Array.isArray(ret[key])) { - throw new Error("Patch target type is mismatched (array to something)"); - } - const orgArrayObject = unorderedArrayToObject(ret[key]); - const appliedObject = applyPatch(orgArrayObject, value[MARK_ISARRAY]); - const appliedArray = objectToUnorderedArray(appliedObject); - ret[key] = [...appliedArray]; - } else { - if (!(key in ret)) { - ret[key] = value; - continue; - } - ret[key] = applyPatch(ret[key], value); - } - } else { - ret[key] = value; - } - } - return ret; -} - -export function mergeObject( - objA: Record | [any], - objB: Record | [any] -) { - const newEntries = Object.entries(objB); - const ret: any = { ...objA }; - if ( - typeof objA !== typeof objB || - Array.isArray(objA) !== Array.isArray(objB) - ) { - return objB; - } - - for (const [key, v] of newEntries) { - if (key in ret) { - const value = ret[key]; - if ( - typeof v !== typeof value || - Array.isArray(v) !== Array.isArray(value) - ) { - //if type is not match, replace completely. - ret[key] = v; - } else { - if ( - typeof v == "object" && - typeof value == "object" && - !Array.isArray(v) && - !Array.isArray(value) - ) { - ret[key] = mergeObject(v, value); - } else if ( - typeof v == "object" && - typeof value == "object" && - Array.isArray(v) && - Array.isArray(value) - ) { - ret[key] = [...new Set([...v, ...value])]; - } else { - ret[key] = v; - } - } - } else { - ret[key] = v; - } - } - const retSorted = Object.fromEntries(Object.entries(ret).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)); - if (Array.isArray(objA) && Array.isArray(objB)) { - return Object.values(retSorted); - } - return retSorted; -} - -export function flattenObject(obj: Record, path: string[] = []): [string, any][] { - if (typeof (obj) != "object") return [[path.join("."), obj]]; - if (obj === null) return [[path.join("."), null]]; - if (Array.isArray(obj)) return [[path.join("."), JSON.stringify(obj)]]; - const e = Object.entries(obj); - const ret = [] - for (const [key, value] of e) { - const p = flattenObject(value, [...path, key]); - ret.push(...p); - } - return ret; -} - export function isValidPath(filename: string) { if (Platform.isDesktop) { @@ -319,29 +122,6 @@ export function isCustomisationSyncMetadata(str: string): boolean { return str.startsWith(ICXHeader); } -export const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => { - return new Promise((res) => { - const popover = new PopoverSelectString(app, message, undefined, undefined, (result) => res(result as "yes" | "no")); - popover.open(); - }); -}; - -export const askSelectString = (app: App, message: string, items: string[]): Promise => { - const getItemsFun = () => items; - return new Promise((res) => { - const popover = new PopoverSelectString(app, message, "", getItemsFun, (result) => res(result)); - popover.open(); - }); -}; - - -export const askString = (app: App, title: string, key: string, placeholder: string, isPassword: boolean = false): Promise => { - return new Promise((res) => { - const dialog = new InputStringDialog(app, title, key, placeholder, isPassword, (result) => res(result)); - dialog.open(); - }); -}; - export class PeriodicProcessor { _process: () => Promise; @@ -413,20 +193,6 @@ export const requestToCouchDB = async (baseUri: string, username: string, passwo return await _requestToCouchDB(baseUri, username, password, origin, uri, body, method); }; -export async function performRebuildDB(plugin: ObsidianLiveSyncPlugin, method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks") { - if (method == "localOnly") { - await plugin.addOnSetup.fetchLocal(); - } - if (method == "localOnlyWithChunks") { - await plugin.addOnSetup.fetchLocal(true); - } - if (method == "remoteOnly") { - await plugin.addOnSetup.rebuildRemote(); - } - if (method == "rebuildBothByThisDevice") { - await plugin.addOnSetup.rebuildEverything(); - } -} export const BASE_IS_NEW = Symbol("base"); export const TARGET_IS_NEW = Symbol("target"); @@ -445,9 +211,9 @@ export function compareMTime(baseMTime: number, targetMTime: number): typeof BAS throw new Error("Unexpected error"); } -export function markChangesAreSame(file: TFile | AnyEntry | string, mtime1: number, mtime2: number) { +export function markChangesAreSame(file: AnyEntry | string | UXFileInfoStub, mtime1: number, mtime2: number) { if (mtime1 === mtime2) return true; - const key = typeof file == "string" ? file : file instanceof TFile ? file.path : file.path ?? file._id; + const key = typeof file == "string" ? file : "_id" in file ? file._id : file.path; const pairs = sameChangePairs.get(key, []) || []; if (pairs.some(e => e == mtime1 || e == mtime2)) { sameChangePairs.set(key, [...new Set([...pairs, mtime1, mtime2])]); @@ -455,20 +221,20 @@ export function markChangesAreSame(file: TFile | AnyEntry | string, mtime1: numb sameChangePairs.set(key, [mtime1, mtime2]); } } -export function isMarkedAsSameChanges(file: TFile | AnyEntry | string, mtimes: number[]) { - const key = typeof file == "string" ? file : file instanceof TFile ? file.path : file.path ?? file._id; +export function isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | string, mtimes: number[]) { + const key = typeof file == "string" ? file : "_id" in file ? file._id : file.path; const pairs = sameChangePairs.get(key, []) || []; if (mtimes.every(e => pairs.indexOf(e) !== -1)) { return EVEN; } } -export function compareFileFreshness(baseFile: TFile | AnyEntry | undefined, checkTarget: TFile | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN { +export function compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN { if (baseFile === undefined && checkTarget == undefined) return EVEN; if (baseFile == undefined) return TARGET_IS_NEW; if (checkTarget == undefined) return BASE_IS_NEW; - const modifiedBase = baseFile instanceof TFile ? baseFile?.stat?.mtime ?? 0 : baseFile?.mtime ?? 0; - const modifiedTarget = checkTarget instanceof TFile ? checkTarget?.stat?.mtime ?? 0 : checkTarget?.mtime ?? 0; + const modifiedBase = "stat" in baseFile ? baseFile?.stat?.mtime ?? 0 : baseFile?.mtime ?? 0; + const modifiedTarget = "stat" in checkTarget ? checkTarget?.stat?.mtime ?? 0 : checkTarget?.mtime ?? 0; if (modifiedBase && modifiedTarget && isMarkedAsSameChanges(baseFile, [modifiedBase, modifiedTarget])) { return EVEN; @@ -484,15 +250,15 @@ const _cached = new Map boolean; + validator?: (context: Map) => boolean; } export function useMemo({ key, forceUpdate, validator }: MemoOption, updateFunc: (context: Map, prev: T) => T): T { const cached = _cached.get(key); - if (cached && !forceUpdate && (!validator || validator && !validator())) { + const context = cached?.context || new Map(); + if (cached && !forceUpdate && (!validator || validator && !validator(context))) { return cached.value; } - const context = cached?.context || new Map(); const value = updateFunc(context, cached?.value); if (value !== cached?.value) { _cached.set(key, { value, context }); @@ -535,4 +301,13 @@ export function disposeMemo(key: string) { export function disposeAllMemo() { _cached.clear(); -} \ No newline at end of file +} + +export function displayRev(rev: string) { + const [number, hash] = rev.split("-"); + return `${number}-${hash.substring(0, 6)}`; +} + +// export function getPathFromUXFileInfo(file: UXFileInfoStub | UXFileInfo | string) { +// return (typeof file == "string" ? file : file.path) as FilePathWithPrefix; +// } \ No newline at end of file diff --git a/src/features/CmdHiddenFileSync.ts b/src/features/CmdHiddenFileSync.ts deleted file mode 100644 index fe183ed..0000000 --- a/src/features/CmdHiddenFileSync.ts +++ /dev/null @@ -1,776 +0,0 @@ -import { normalizePath, type PluginManifest, type ListedFiles } from "../deps.ts"; -import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED, type SavingEntry, type DocumentID } from "../lib/src/common/types.ts"; -import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "../common/types.ts"; -import { readAsBlob, isDocContentSame, sendSignal, readContent, createBlob } from "../lib/src/common/utils.ts"; -import { Logger } from "../lib/src/common/logger.ts"; -import { PouchDB } from "../lib/src/pouchdb/pouchdb-browser.js"; -import { isInternalMetadata, PeriodicProcessor } from "../common/utils.ts"; -import { serialized } from "../lib/src/concurrency/lock.ts"; -import { JsonResolveModal } from "../ui/JsonResolveModal.ts"; -import { LiveSyncCommands } from "./LiveSyncCommands.ts"; -import { addPrefix, stripAllPrefixes } from "../lib/src/string_and_binary/path.ts"; -import { QueueProcessor } from "../lib/src/concurrency/processor.ts"; -import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "../lib/src/mock_and_interop/stores.ts"; - -export class HiddenFileSync extends LiveSyncCommands { - periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(this.plugin, async () => this.settings.syncInternalFiles && this.localDatabase.isReady && await this.syncInternalFilesAndDatabase("push", false)); - - get kvDB() { - return this.plugin.kvDB; - } - getConflictedDoc(path: FilePathWithPrefix, rev: string) { - return this.plugin.getConflictedDoc(path, rev); - } - onunload() { - this.periodicInternalFileScanProcessor?.disable(); - } - onload() { - this.plugin.addCommand({ - id: "livesync-scaninternal", - name: "Sync hidden files", - callback: () => { - this.syncInternalFilesAndDatabase("safe", true); - }, - }); - } - async onInitializeDatabase(showNotice: boolean) { - if (this.settings.syncInternalFiles) { - try { - Logger("Synchronizing hidden files..."); - await this.syncInternalFilesAndDatabase("push", showNotice); - Logger("Synchronizing hidden files done"); - } catch (ex) { - Logger("Synchronizing hidden files failed"); - Logger(ex, LOG_LEVEL_VERBOSE); - } - } - } - async beforeReplicate(showNotice: boolean) { - if (this.localDatabase.isReady && this.settings.syncInternalFiles && this.settings.syncInternalFilesBeforeReplication && !this.settings.watchInternalFileChanges) { - await this.syncInternalFilesAndDatabase("push", showNotice); - } - } - async onResume() { - this.periodicInternalFileScanProcessor?.disable(); - if (this.plugin.suspended) - return; - if (this.settings.syncInternalFiles) { - await this.syncInternalFilesAndDatabase("safe", false); - } - this.periodicInternalFileScanProcessor.enable(this.settings.syncInternalFiles && this.settings.syncInternalFilesInterval ? (this.settings.syncInternalFilesInterval * 1000) : 0); - } - parseReplicationResultItem(docs: PouchDB.Core.ExistingDocument) { - return false; - } - realizeSettingSyncMode(): Promise { - this.periodicInternalFileScanProcessor?.disable(); - if (this.plugin.suspended) - return Promise.resolve(); - if (!this.plugin.isReady) - return Promise.resolve(); - this.periodicInternalFileScanProcessor.enable(this.settings.syncInternalFiles && this.settings.syncInternalFilesInterval ? (this.settings.syncInternalFilesInterval * 1000) : 0); - return Promise.resolve(); - } - - procInternalFile(filename: string) { - this.internalFileProcessor.enqueue(filename); - } - internalFileProcessor = new QueueProcessor( - async (filenames) => { - Logger(`START :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE); - await this.syncInternalFilesAndDatabase("pull", false, false, filenames); - Logger(`DONE :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE); - return; - }, { batchSize: 100, concurrentLimit: 1, delay: 10, yieldThreshold: 100, suspended: false, totalRemainingReactiveSource: hiddenFilesEventCount } - ); - - recentProcessedInternalFiles = [] as string[]; - async watchVaultRawEventsAsync(path: FilePath) { - if (!this.settings.syncInternalFiles) return; - - // Exclude files handled by customization sync - const configDir = normalizePath(this.app.vault.configDir); - const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); - if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) { - Logger(`Hidden file skipped: ${path} is synchronized in customization sync.`, LOG_LEVEL_VERBOSE); - return; - } - const stat = await this.vaultAccess.adapterStat(path); - // sometimes folder is coming. - if (stat != null && stat.type != "file") { - return; - } - const mtime = stat == null ? 0 : stat?.mtime ?? 0; - const storageMTime = ~~((mtime) / 1000); - const key = `${path}-${storageMTime}`; - if (mtime != 0 && this.recentProcessedInternalFiles.contains(key)) { - //If recently processed, it may caused by self. - return; - } - this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100); - // const id = await this.path2id(path, ICHeader); - const prefixedFileName = addPrefix(path, ICHeader); - const filesOnDB = await this.localDatabase.getDBEntryMeta(prefixedFileName); - const dbMTime = ~~((filesOnDB && filesOnDB.mtime || 0) / 1000); - - // Skip unchanged file. - if (dbMTime == storageMTime) { - // Logger(`STORAGE --> DB:${path}: (hidden) Nothing changed`); - return; - } - - // Do not compare timestamp. Always local data should be preferred except this plugin wrote one. - if (storageMTime == 0) { - await this.deleteInternalFileOnDatabase(path); - } else { - await this.storeInternalFileToDatabase({ path: path, mtime, ctime: stat?.ctime ?? mtime, size: stat?.size ?? 0 }); - } - - } - - async resolveConflictOnInternalFiles() { - // Scan all conflicted internal files - const conflicted = this.localDatabase.findEntries(ICHeader, ICHeaderEnd, { conflicts: true }); - this.conflictResolutionProcessor.suspend(); - try { - for await (const doc of conflicted) { - if (!("_conflicts" in doc)) - continue; - if (isInternalMetadata(doc._id)) { - this.conflictResolutionProcessor.enqueue(doc.path); - } - } - } catch (ex) { - Logger("something went wrong on resolving all conflicted internal files"); - Logger(ex, LOG_LEVEL_VERBOSE); - } - await this.conflictResolutionProcessor.startPipeline().waitForAllProcessed(); - } - - async resolveByNewerEntry(id: DocumentID, path: FilePathWithPrefix, currentDoc: EntryDoc, currentRev: string, conflictedRev: string) { - const conflictedDoc = await this.localDatabase.getRaw(id, { rev: conflictedRev }); - // determine which revision should been deleted. - // simply check modified time - const mtimeCurrent = ("mtime" in currentDoc && currentDoc.mtime) || 0; - const mtimeConflicted = ("mtime" in conflictedDoc && conflictedDoc.mtime) || 0; - // Logger(`Revisions:${new Date(mtimeA).toLocaleString} and ${new Date(mtimeB).toLocaleString}`); - // console.log(`mtime:${mtimeA} - ${mtimeB}`); - const delRev = mtimeCurrent < mtimeConflicted ? currentRev : conflictedRev; - // delete older one. - await this.localDatabase.removeRevision(id, delRev); - Logger(`Older one has been deleted:${path}`); - const cc = await this.localDatabase.getRaw(id, { conflicts: true }); - if (cc._conflicts?.length === 0) { - await this.extractInternalFileFromDatabase(stripAllPrefixes(path)) - } else { - this.conflictResolutionProcessor.enqueue(path); - } - // check the file again - - } - conflictResolutionProcessor = new QueueProcessor(async (paths: FilePathWithPrefix[]) => { - const path = paths[0]; - sendSignal(`cancel-internal-conflict:${path}`); - try { - // Retrieve data - const id = await this.path2id(path, ICHeader); - const doc = await this.localDatabase.getRaw(id, { conflicts: true }); - // if (!("_conflicts" in doc)){ - // return []; - // } - if (doc._conflicts === undefined) return []; - if (doc._conflicts.length == 0) - return []; - Logger(`Hidden file conflicted:${path}`); - const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0])); - const revA = doc._rev; - const revB = conflicts[0]; - - if (path.endsWith(".json")) { - const conflictedRev = conflicts[0]; - const conflictedRevNo = Number(conflictedRev.split("-")[0]); - //Search - const revFrom = (await this.localDatabase.getRaw(id, { revs_info: true })); - const commonBase = revFrom._revs_info?.filter(e => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo).first()?.rev ?? ""; - const result = await this.plugin.mergeObject(path, commonBase, doc._rev, conflictedRev); - if (result) { - Logger(`Object merge:${path}`, LOG_LEVEL_INFO); - const filename = stripAllPrefixes(path); - const isExists = await this.plugin.vaultAccess.adapterExists(filename); - if (!isExists) { - await this.vaultAccess.ensureDirectory(filename); - } - await this.plugin.vaultAccess.adapterWrite(filename, result); - const stat = await this.vaultAccess.adapterStat(filename); - if (!stat) { - throw new Error(`conflictResolutionProcessor: Failed to stat file ${filename}`); - } - await this.storeInternalFileToDatabase({ path: filename, ...stat }); - await this.extractInternalFileFromDatabase(filename); - await this.localDatabase.removeRevision(id, revB); - this.conflictResolutionProcessor.enqueue(path); - return []; - } else { - Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE); - } - return [{ path, revA, revB, id, doc }]; - } - // When not JSON file, resolve conflicts by choosing a newer one. - await this.resolveByNewerEntry(id, path, doc, revA, revB); - return []; - } catch (ex) { - Logger(`Failed to resolve conflict (Hidden): ${path}`); - Logger(ex, LOG_LEVEL_VERBOSE); - return []; - } - }, { - suspended: false, batchSize: 1, concurrentLimit: 5, delay: 10, keepResultUntilDownstreamConnected: true, yieldThreshold: 10, - pipeTo: new QueueProcessor(async (results) => { - const { id, doc, path, revA, revB } = results[0]; - const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA }); - const docBMerge = await this.localDatabase.getDBEntry(path, { rev: revB }); - if (docAMerge != false && docBMerge != false) { - if (await this.showJSONMergeDialogAndMerge(docAMerge, docBMerge)) { - // Again for other conflicted revisions. - this.conflictResolutionProcessor.enqueue(path); - } - return; - } else { - // If either revision could not read, force resolving by the newer one. - await this.resolveByNewerEntry(id, path, doc, revA, revB); - } - }, { suspended: false, batchSize: 1, concurrentLimit: 1, delay: 10, keepResultUntilDownstreamConnected: false, yieldThreshold: 10 }) - }) - - queueConflictCheck(path: FilePathWithPrefix) { - this.conflictResolutionProcessor.enqueue(path); - } - - //TODO: Tidy up. Even though it is experimental feature, So dirty... - async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", showMessage: boolean, filesAll: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) { - await this.resolveConflictOnInternalFiles(); - const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; - Logger("Scanning hidden files.", logLevel, "sync_internal"); - const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns - .replace(/\n| /g, "") - .split(",").filter(e => e).map(e => new RegExp(e, "i")); - - const configDir = normalizePath(this.app.vault.configDir); - let files: InternalFileInfo[] = - filesAll ? filesAll : (await this.scanInternalFiles()) - - const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); - files = files.filter(file => synchronisedInConfigSync.every(filterFile => !file.path.toLowerCase().startsWith(filterFile))) - - const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted); - const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => stripAllPrefixes(this.getPath(e)))])]; - const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1)).filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile))) - function compareMTime(a: number, b: number) { - const wa = ~~(a / 1000); - const wb = ~~(b / 1000); - const diff = wa - wb; - return diff; - } - - const fileCount = allFileNames.length; - let processed = 0; - let filesChanged = 0; - // count updated files up as like this below: - // .obsidian: 2 - // .obsidian/workspace: 1 - // .obsidian/plugins: 1 - // .obsidian/plugins/recent-files-obsidian: 1 - // .obsidian/plugins/recent-files-obsidian/data.json: 1 - const updatedFolders: { [key: string]: number; } = {}; - const countUpdatedFolder = (path: string) => { - const pieces = path.split("/"); - let c = pieces.shift(); - let pathPieces = ""; - filesChanged++; - while (c) { - pathPieces += (pathPieces != "" ? "/" : "") + c; - pathPieces = normalizePath(pathPieces); - if (!(pathPieces in updatedFolders)) { - updatedFolders[pathPieces] = 0; - } - updatedFolders[pathPieces]++; - c = pieces.shift(); - } - }; - // Cache update time information for files which have already been processed (mainly for files that were skipped due to the same content) - let caches: { [key: string]: { storageMtime: number; docMtime: number; }; } = {}; - caches = await this.kvDB.get<{ [key: string]: { storageMtime: number; docMtime: number; }; }>("diff-caches-internal") || {}; - const filesMap = files.reduce((acc, cur) => { - acc[cur.path] = cur; - return acc; - }, {} as { [key: string]: InternalFileInfo; }); - const filesOnDBMap = filesOnDB.reduce((acc, cur) => { - acc[stripAllPrefixes(this.getPath(cur))] = cur; - return acc; - }, {} as { [key: string]: InternalFileEntry; }); - await new QueueProcessor(async (filenames: FilePath[]) => { - const filename = filenames[0]; - processed++; - if (processed % 100 == 0) { - Logger(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal"); - } - if (!filename) return []; - if (ignorePatterns.some(e => filename.match(e))) - return []; - if (await this.plugin.isIgnoredByIgnoreFiles(filename)) { - return []; - } - - const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined; - const fileOnDatabase = filename in filesOnDBMap ? filesOnDBMap[filename] : undefined; - - return [{ - filename, - fileOnStorage, - fileOnDatabase, - }] - - }, { suspended: true, batchSize: 1, concurrentLimit: 10, delay: 0, totalRemainingReactiveSource: hiddenFilesProcessingCount }) - .pipeTo(new QueueProcessor(async (params) => { - const - { - filename, - fileOnStorage: xFileOnStorage, - fileOnDatabase: xFileOnDatabase - } = params[0]; - if (xFileOnStorage && xFileOnDatabase) { - const cache = filename in caches ? caches[filename] : { storageMtime: 0, docMtime: 0 }; - // Both => Synchronize - if ((direction != "pullForce" && direction != "pushForce") && xFileOnDatabase.mtime == cache.docMtime && xFileOnStorage.mtime == cache.storageMtime) { - return; - } - const nw = compareMTime(xFileOnStorage.mtime, xFileOnDatabase.mtime); - if (nw > 0 || direction == "pushForce") { - await this.storeInternalFileToDatabase(xFileOnStorage); - } - if (nw < 0 || direction == "pullForce") { - // skip if not extraction performed. - if (!await this.extractInternalFileFromDatabase(filename)) - return; - } - // If process successfully updated or file contents are same, update cache. - cache.docMtime = xFileOnDatabase.mtime; - cache.storageMtime = xFileOnStorage.mtime; - caches[filename] = cache; - countUpdatedFolder(filename); - } else if (!xFileOnStorage && xFileOnDatabase) { - if (direction == "push" || direction == "pushForce") { - if (xFileOnDatabase.deleted) - return; - await this.deleteInternalFileOnDatabase(filename, false); - } else if (direction == "pull" || direction == "pullForce") { - if (await this.extractInternalFileFromDatabase(filename)) { - countUpdatedFolder(filename); - } - } else if (direction == "safe") { - if (xFileOnDatabase.deleted) - return; - if (await this.extractInternalFileFromDatabase(filename)) { - countUpdatedFolder(filename); - } - } - } else if (xFileOnStorage && !xFileOnDatabase) { - if (direction == "push" || direction == "pushForce" || direction == "safe") { - await this.storeInternalFileToDatabase(xFileOnStorage); - } else { - await this.extractInternalFileFromDatabase(xFileOnStorage.path); - } - } else { - throw new Error("Invalid state on hidden file sync"); - // Something corrupted? - } - return; - }, { suspended: true, batchSize: 1, concurrentLimit: 5, delay: 0 })) - .root - .enqueueAll(allFileNames) - .startPipeline().waitForAllDoneAndTerminate(); - - await this.kvDB.set("diff-caches-internal", caches); - - // When files has been retrieved from the database. they must be reloaded. - if ((direction == "pull" || direction == "pullForce") && filesChanged != 0) { - // Show notification to restart obsidian when something has been changed in configDir. - if (configDir in updatedFolders) { - // Numbers of updated files that is below of configDir. - let updatedCount = updatedFolders[configDir]; - try { - //@ts-ignore - const manifests = Object.values(this.app.plugins.manifests) as any as PluginManifest[]; - //@ts-ignore - const enabledPlugins = this.app.plugins.enabledPlugins as Set; - const enabledPluginManifests = manifests.filter(e => enabledPlugins.has(e.id)); - for (const manifest of enabledPluginManifests) { - if (manifest.dir && manifest.dir in updatedFolders) { - // If notified about plug-ins, reloading Obsidian may not be necessary. - updatedCount -= updatedFolders[manifest.dir]; - const updatePluginId = manifest.id; - const updatePluginName = manifest.name; - this.plugin.askInPopup(`updated-${updatePluginId}`, `Files in ${updatePluginName} has been updated, Press {HERE} to reload ${updatePluginName}, or press elsewhere to dismiss this message.`, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", async () => { - Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); - // @ts-ignore - await this.app.plugins.unloadPlugin(updatePluginId); - // @ts-ignore - await this.app.plugins.loadPlugin(updatePluginId); - Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); - }); - } - ); - } - } - } catch (ex) { - Logger("Error on checking plugin status."); - Logger(ex, LOG_LEVEL_VERBOSE); - - } - - // If something changes left, notify for reloading Obsidian. - if (updatedCount != 0) { - if (!this.plugin.isReloadingScheduled) { - this.plugin.askInPopup(`updated-any-hidden`, `Hidden files have been synchronised, Press {HERE} to schedule a reload of Obsidian, or press elsewhere to dismiss this message.`, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", () => { - this.plugin.scheduleAppReload(); - }); - }); - } - } - } - } - - Logger(`Hidden files scanned: ${filesChanged} files had been modified`, logLevel, "sync_internal"); - } - - async storeInternalFileToDatabase(file: InternalFileInfo, forceWrite = false) { - if (await this.plugin.isIgnoredByIgnoreFiles(file.path)) { - return - } - - const id = await this.path2id(file.path, ICHeader); - const prefixedFileName = addPrefix(file.path, ICHeader); - const content = createBlob(await this.plugin.vaultAccess.adapterReadAuto(file.path)); - const mtime = file.mtime; - return await serialized("file-" + prefixedFileName, async () => { - try { - const old = await this.localDatabase.getDBEntry(prefixedFileName, undefined, false, false); - let saveData: SavingEntry; - if (old === false) { - saveData = { - _id: id, - path: prefixedFileName, - data: content, - mtime, - ctime: mtime, - datatype: "newnote", - size: file.size, - children: [], - deleted: false, - type: "newnote", - eden: {}, - }; - } else { - if (await isDocContentSame(readAsBlob(old), content) && !forceWrite) { - // Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL_VERBOSE); - return; - } - saveData = - { - ...old, - data: content, - mtime, - size: file.size, - datatype: old.datatype, - children: [], - deleted: false, - type: old.datatype, - }; - } - const ret = await this.localDatabase.putDBEntry(saveData); - Logger(`STORAGE --> DB:${file.path}: (hidden) Done`); - return ret; - } catch (ex) { - Logger(`STORAGE --> DB:${file.path}: (hidden) Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); - return false; - } - }); - } - - async deleteInternalFileOnDatabase(filename: FilePath, forceWrite = false) { - const id = await this.path2id(filename, ICHeader); - const prefixedFileName = addPrefix(filename, ICHeader); - const mtime = new Date().getTime(); - if (await this.plugin.isIgnoredByIgnoreFiles(filename)) { - return - } - await serialized("file-" + prefixedFileName, async () => { - try { - const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, undefined, true) as InternalFileEntry | false; - let saveData: InternalFileEntry; - if (old === false) { - saveData = { - _id: id, - path: prefixedFileName, - mtime, - ctime: mtime, - size: 0, - children: [], - deleted: true, - type: "newnote", - eden: {} - }; - } else { - // Remove all conflicted before deleting. - const conflicts = await this.localDatabase.getRaw(old._id, { conflicts: true }); - if (conflicts._conflicts !== undefined) { - for (const conflictRev of conflicts._conflicts) { - await this.localDatabase.removeRevision(old._id, conflictRev); - Logger(`STORAGE -x> DB:${filename}: (hidden) conflict removed ${old._rev} => ${conflictRev}`, LOG_LEVEL_VERBOSE); - } - } - if (old.deleted) { - Logger(`STORAGE -x> DB:${filename}: (hidden) already deleted`); - return; - } - saveData = - { - ...old, - mtime, - size: 0, - children: [], - deleted: true, - type: "newnote", - }; - } - await this.localDatabase.putRaw(saveData); - Logger(`STORAGE -x> DB:${filename}: (hidden) Done`); - } catch (ex) { - Logger(`STORAGE -x> DB:${filename}: (hidden) Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); - return false; - } - }); - } - - async extractInternalFileFromDatabase(filename: FilePath, force = false) { - const isExists = await this.plugin.vaultAccess.adapterExists(filename); - const prefixedFileName = addPrefix(filename, ICHeader); - if (await this.plugin.isIgnoredByIgnoreFiles(filename)) { - return; - } - return await serialized("file-" + prefixedFileName, async () => { - try { - // Check conflicted status - const fileOnDB = await this.localDatabase.getDBEntry(prefixedFileName, { conflicts: true }, false, true, true); - if (fileOnDB === false) - throw new Error(`File not found on database.:${filename}`); - // Prevent overwrite for Prevent overwriting while some conflicted revision exists. - if (fileOnDB?._conflicts?.length) { - Logger(`Hidden file ${filename} has conflicted revisions, to keep in safe, writing to storage has been prevented`, LOG_LEVEL_INFO); - return; - } - const deleted = fileOnDB.deleted || fileOnDB._deleted || false; - if (deleted) { - if (!isExists) { - Logger(`STORAGE { - return new Promise((res) => { - Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE); - const docs = [docA, docB]; - const path = stripAllPrefixes(docA.path); - const modal = new JsonResolveModal(this.app, path, [docA, docB], async (keep, result) => { - // modal.close(); - try { - const filename = path; - let needFlush = false; - if (!result && !keep) { - Logger(`Skipped merging: ${filename}`); - res(false); - return; - } - //Delete old revisions - if (result || keep) { - for (const doc of docs) { - if (doc._rev != keep) { - if (await this.localDatabase.deleteDBEntry(this.getPath(doc), { rev: doc._rev })) { - Logger(`Conflicted revision has been deleted: ${filename}`); - needFlush = true; - } - } - } - } - if (!keep && result) { - const isExists = await this.plugin.vaultAccess.adapterExists(filename); - if (!isExists) { - await this.vaultAccess.ensureDirectory(filename); - } - await this.plugin.vaultAccess.adapterWrite(filename, result); - const stat = await this.plugin.vaultAccess.adapterStat(filename); - if (!stat) { - throw new Error("Stat failed"); - } - const mtime = stat?.mtime ?? 0; - await this.storeInternalFileToDatabase({ path: filename, mtime, ctime: stat?.ctime ?? mtime, size: stat?.size ?? 0 }, true); - try { - //@ts-ignore internalAPI - await this.app.vault.adapter.reconcileInternalFile(filename); - } catch (ex) { - Logger("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL_VERBOSE); - Logger(ex, LOG_LEVEL_VERBOSE); - } - Logger(`STORAGE <-- DB:${filename}: written (hidden,merged)`); - } - if (needFlush) { - await this.extractInternalFileFromDatabase(filename, false); - Logger(`STORAGE --> DB:${filename}: extracted (hidden,merged)`); - } - res(true); - } catch (ex) { - Logger("Could not merge conflicted json"); - Logger(ex, LOG_LEVEL_VERBOSE); - res(false); - } - }); - modal.open(); - }); - } - - async scanInternalFiles(): Promise { - const configDir = normalizePath(this.app.vault.configDir); - const ignoreFilter = this.settings.syncInternalFilesIgnorePatterns - .replace(/\n| /g, "") - .split(",").filter(e => e).map(e => new RegExp(e, "i")); - const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); - const root = this.app.vault.getRoot(); - const findRoot = root.path; - - const filenames = (await this.getFiles(findRoot, [], undefined, ignoreFilter)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash")); - const files = filenames.filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile))).map(async (e) => { - return { - path: e as FilePath, - stat: await this.plugin.vaultAccess.adapterStat(e) - }; - }); - const result: InternalFileInfo[] = []; - for (const f of files) { - const w = await f; - if (await this.plugin.isIgnoredByIgnoreFiles(w.path)) { - continue - } - const mtime = w.stat?.mtime ?? 0 - const ctime = w.stat?.ctime ?? mtime; - const size = w.stat?.size ?? 0; - result.push({ - ...w, - mtime, ctime, size - }); - } - return result; - } - - - - async getFiles( - path: string, - ignoreList: string[], - filter?: RegExp[], - ignoreFilter?: RegExp[] - ) { - let w: ListedFiles; - try { - w = await this.app.vault.adapter.list(path); - } catch (ex) { - Logger(`Could not traverse(HiddenSync):${path}`, LOG_LEVEL_INFO); - Logger(ex, LOG_LEVEL_VERBOSE); - return []; - } - const filesSrc = [ - ...w.files - .filter((e) => !ignoreList.some((ee) => e.endsWith(ee))) - .filter((e) => !filter || filter.some((ee) => e.match(ee))) - .filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee))) - ]; - let files = [] as string[]; - for (const file of filesSrc) { - if (!await this.plugin.isIgnoredByIgnoreFiles(file)) { - files.push(file); - } - } - - L1: for (const v of w.folders) { - for (const ignore of ignoreList) { - if (v.endsWith(ignore)) { - continue L1; - } - } - if (ignoreFilter && ignoreFilter.some(e => v.match(e))) { - continue L1; - } - if (await this.plugin.isIgnoredByIgnoreFiles(v)) { - continue L1; - } - files = files.concat(await this.getFiles(v, ignoreList, filter, ignoreFilter)); - } - return files; - } -} diff --git a/src/features/CmdSetupLiveSync.ts b/src/features/CmdSetupLiveSync.ts deleted file mode 100644 index 268a702..0000000 --- a/src/features/CmdSetupLiveSync.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { type EntryDoc, type ObsidianLiveSyncSettings, DEFAULT_SETTINGS, LOG_LEVEL_NOTICE, REMOTE_COUCHDB, REMOTE_MINIO } from "../lib/src/common/types.ts"; -import { configURIBase } from "../common/types.ts"; -import { Logger } from "../lib/src/common/logger.ts"; -import { PouchDB } from "../lib/src/pouchdb/pouchdb-browser.js"; -import { askSelectString, askYesNo, askString } from "../common/utils.ts"; -import { decrypt, encrypt } from "../lib/src/encryption/e2ee_v2.ts"; -import { LiveSyncCommands } from "./LiveSyncCommands.ts"; -import { delay, fireAndForget } from "../lib/src/common/utils.ts"; -import { confirmWithMessage } from "../common/dialogs.ts"; -import { Platform } from "../deps.ts"; -import { fetchAllUsedChunks } from "../lib/src/pouchdb/utils_couchdb.ts"; -import type { LiveSyncCouchDBReplicator } from "../lib/src/replication/couchdb/LiveSyncReplicator.js"; - -export class SetupLiveSync extends LiveSyncCommands { - onunload() { } - onload(): void | Promise { - this.plugin.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => await this.setupWizard(conf.settings)); - - this.plugin.addCommand({ - id: "livesync-copysetupuri", - name: "Copy settings as a new setup URI", - callback: () => fireAndForget(this.command_copySetupURI()), - }); - this.plugin.addCommand({ - id: "livesync-copysetupuri-short", - name: "Copy settings as a new setup URI (With customization sync)", - callback: () => fireAndForget(this.command_copySetupURIWithSync()), - }); - - this.plugin.addCommand({ - id: "livesync-copysetupurifull", - name: "Copy settings as a new setup URI (Full)", - callback: () => fireAndForget(this.command_copySetupURIFull()), - }); - - this.plugin.addCommand({ - id: "livesync-opensetupuri", - name: "Use the copied setup URI (Formerly Open setup URI)", - callback: () => fireAndForget(this.command_openSetupURI()), - }); - } - onInitializeDatabase(showNotice: boolean) { } - beforeReplicate(showNotice: boolean) { } - onResume() { } - parseReplicationResultItem(docs: PouchDB.Core.ExistingDocument): boolean | Promise { - return false; - } - async realizeSettingSyncMode() { } - - async command_copySetupURI(stripExtra = true) { - const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true); - if (encryptingPassphrase === false) - return; - const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" } as Partial; - if (stripExtra) { - delete setting.pluginSyncExtendedSetting; - } - const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[]; - for (const k of keys) { - if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) { - delete setting[k]; - } - } - const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false)); - const uri = `${configURIBase}${encryptedSetting}`; - await navigator.clipboard.writeText(uri); - Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); - } - async command_copySetupURIFull() { - const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true); - if (encryptingPassphrase === false) - return; - const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" }; - const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false)); - const uri = `${configURIBase}${encryptedSetting}`; - await navigator.clipboard.writeText(uri); - Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); - } - async command_copySetupURIWithSync() { - await this.command_copySetupURI(false); - } - async command_openSetupURI() { - const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`); - if (setupURI === false) - return; - if (!setupURI.startsWith(`${configURIBase}`)) { - Logger("Set up URI looks wrong.", LOG_LEVEL_NOTICE); - return; - } - const config = decodeURIComponent(setupURI.substring(configURIBase.length)); - console.dir(config); - await this.setupWizard(config); - } - async setupWizard(confString: string) { - try { - const oldConf = JSON.parse(JSON.stringify(this.settings)); - const encryptingPassphrase = await askString(this.app, "Passphrase", "The passphrase to decrypt your setup URI", "", true); - if (encryptingPassphrase === false) - return; - const newConf = await JSON.parse(await decrypt(confString, encryptingPassphrase, false)); - if (newConf) { - const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?"); - if (result == "yes") { - const newSettingW = Object.assign({}, DEFAULT_SETTINGS, newConf) as ObsidianLiveSyncSettings; - this.plugin.replicator.closeReplication(); - this.settings.suspendFileWatching = true; - console.dir(newSettingW); - // Back into the default method once. - newSettingW.configPassphraseStore = ""; - newSettingW.encryptedPassphrase = ""; - newSettingW.encryptedCouchDBConnection = ""; - newSettingW.additionalSuffixOfDatabaseName = `${("appId" in this.app ? this.app.appId : "")}` - const setupJustImport = "Just import setting"; - const setupAsNew = "Set it up as secondary or subsequent device"; - const setupAsMerge = "Secondary device but try keeping local changes"; - const setupAgain = "Reconfigure and reconstitute the data"; - const setupManually = "Leave everything to me"; - newSettingW.syncInternalFiles = false; - newSettingW.usePluginSync = false; - newSettingW.isConfigured = true; - // Migrate completely obsoleted configuration. - if (!newSettingW.useIndexedDBAdapter) { - newSettingW.useIndexedDBAdapter = true; - } - - const setupType = await askSelectString(this.app, "How would you like to set it up?", [setupAsNew, setupAgain, setupAsMerge, setupJustImport, setupManually]); - if (setupType == setupJustImport) { - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - await this.plugin.saveSettings(); - } else if (setupType == setupAsNew) { - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - await this.fetchLocal(); - } else if (setupType == setupAsMerge) { - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - await this.fetchLocalWithRebuild(); - } else if (setupType == setupAgain) { - const confirm = "I know this operation will rebuild all my databases with files on this device, and files that are on the remote database and I didn't synchronize to any other devices will be lost and want to proceed indeed."; - if (await askSelectString(this.app, "Do you really want to do this?", ["Cancel", confirm]) != confirm) { - return; - } - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - await this.rebuildEverything(); - } else if (setupType == setupManually) { - const keepLocalDB = await askYesNo(this.app, "Keep local DB?"); - const keepRemoteDB = await askYesNo(this.app, "Keep remote DB?"); - if (keepLocalDB == "yes" && keepRemoteDB == "yes") { - // nothing to do. so peaceful. - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - this.suspendAllSync(); - this.suspendExtraSync(); - await this.plugin.saveSettings(); - const replicate = await askYesNo(this.app, "Unlock and replicate?"); - if (replicate == "yes") { - await this.plugin.replicate(true); - await this.plugin.markRemoteUnlocked(); - } - Logger("Configuration loaded.", LOG_LEVEL_NOTICE); - return; - } - if (keepLocalDB == "no" && keepRemoteDB == "no") { - const reset = await askYesNo(this.app, "Drop everything?"); - if (reset != "yes") { - Logger("Cancelled", LOG_LEVEL_NOTICE); - this.plugin.settings = oldConf; - return; - } - } - let initDB; - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - await this.plugin.saveSettings(); - if (keepLocalDB == "no") { - await this.plugin.resetLocalDatabase(); - await this.plugin.localDatabase.initializeDatabase(); - const rebuild = await askYesNo(this.app, "Rebuild the database?"); - if (rebuild == "yes") { - initDB = this.plugin.initializeDatabase(true); - } else { - await this.plugin.markRemoteResolved(); - } - } - if (keepRemoteDB == "no") { - await this.plugin.tryResetRemoteDatabase(); - await this.plugin.markRemoteLocked(); - } - if (keepLocalDB == "no" || keepRemoteDB == "no") { - const replicate = await askYesNo(this.app, "Replicate once?"); - if (replicate == "yes") { - if (initDB != null) { - await initDB; - } - await this.plugin.replicate(true); - } - } - } - } - - Logger("Configuration loaded.", LOG_LEVEL_NOTICE); - } else { - Logger("Cancelled.", LOG_LEVEL_NOTICE); - } - } catch (ex) { - Logger("Couldn't parse or decrypt configuration uri.", LOG_LEVEL_NOTICE); - } - } - - suspendExtraSync() { - Logger("Hidden files and plugin synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) - this.plugin.settings.syncInternalFiles = false; - this.plugin.settings.usePluginSync = false; - this.plugin.settings.autoSweepPlugins = false; - } - async askHiddenFileConfiguration(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) { - this.plugin.addOnSetup.suspendExtraSync(); - const message = `Would you like to enable \`Hidden File Synchronization\` or \`Customization sync\`? -${opt.enableFetch ? " - Fetch: Use files stored from other devices. \n" : ""}${opt.enableOverwrite ? "- Overwrite: Use files from this device. \n" : ""}- Custom: Synchronize only customization files with a dedicated interface. -- Keep them disabled: Do not use hidden file synchronization. -Of course, we are able to disable these features.` - const CHOICE_FETCH = "Fetch"; - const CHOICE_OVERWRITE = "Overwrite"; - const CHOICE_CUSTOMIZE = "Custom"; - const CHOICE_DISMISS = "keep them disabled"; - const choices = []; - if (opt?.enableFetch) { - choices.push(CHOICE_FETCH); - } - if (opt?.enableOverwrite) { - choices.push(CHOICE_OVERWRITE); - } - choices.push(CHOICE_CUSTOMIZE); - choices.push(CHOICE_DISMISS); - - const ret = await confirmWithMessage(this.plugin, "Hidden file sync", message, choices, CHOICE_DISMISS, 40); - if (ret == CHOICE_FETCH) { - await this.configureHiddenFileSync("FETCH"); - } else if (ret == CHOICE_OVERWRITE) { - await this.configureHiddenFileSync("OVERWRITE"); - } else if (ret == CHOICE_DISMISS) { - await this.configureHiddenFileSync("DISABLE"); - } else if (ret == CHOICE_CUSTOMIZE) { - await this.configureHiddenFileSync("CUSTOMIZE"); - } - } - async configureHiddenFileSync(mode: "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "CUSTOMIZE") { - this.plugin.addOnSetup.suspendExtraSync(); - if (mode == "DISABLE") { - this.plugin.settings.syncInternalFiles = false; - this.plugin.settings.usePluginSync = false; - await this.plugin.saveSettings(); - return; - } - if (mode != "CUSTOMIZE") { - Logger("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE); - if (mode == "FETCH") { - await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("pullForce", true); - } else if (mode == "OVERWRITE") { - await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("pushForce", true); - } else if (mode == "MERGE") { - await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("safe", true); - } - this.plugin.settings.syncInternalFiles = true; - await this.plugin.saveSettings(); - Logger(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL_NOTICE); - } else if (mode == "CUSTOMIZE") { - if (!this.plugin.deviceAndVaultName) { - let name = await askString(this.app, "Device name", "Please set this device name", `desktop`); - if (!name) { - if (Platform.isAndroidApp) { - name = "android-app" - } else if (Platform.isIosApp) { - name = "ios" - } else if (Platform.isMacOS) { - name = "macos" - } else if (Platform.isMobileApp) { - name = "mobile-app" - } else if (Platform.isMobile) { - name = "mobile" - } else if (Platform.isSafari) { - name = "safari" - } else if (Platform.isDesktop) { - name = "desktop" - } else if (Platform.isDesktopApp) { - name = "desktop-app" - } else { - name = "unknown" - } - name = name + Math.random().toString(36).slice(-4); - } - this.plugin.deviceAndVaultName = name; - } - this.plugin.settings.usePluginSync = true; - await this.plugin.saveSettings(); - await this.plugin.addOnConfigSync.scanAllConfigFiles(true); - } - - } - - suspendAllSync() { - this.plugin.settings.liveSync = false; - this.plugin.settings.periodicReplication = false; - this.plugin.settings.syncOnSave = false; - this.plugin.settings.syncOnEditorSave = false; - this.plugin.settings.syncOnStart = false; - this.plugin.settings.syncOnFileOpen = false; - this.plugin.settings.syncAfterMerge = false; - //this.suspendExtraSync(); - } - async suspendReflectingDatabase() { - if (this.plugin.settings.doNotSuspendOnFetching) return; - if (this.plugin.settings.remoteType == REMOTE_MINIO) return; - Logger(`Suspending reflection: Database and storage changes will not be reflected in each other until completely finished the fetching.`, LOG_LEVEL_NOTICE); - this.plugin.settings.suspendParseReplicationResult = true; - this.plugin.settings.suspendFileWatching = true; - await this.plugin.saveSettings(); - } - async resumeReflectingDatabase() { - if (this.plugin.settings.doNotSuspendOnFetching) return; - if (this.plugin.settings.remoteType == REMOTE_MINIO) return; - Logger(`Database and storage reflection has been resumed!`, LOG_LEVEL_NOTICE); - this.plugin.settings.suspendParseReplicationResult = false; - this.plugin.settings.suspendFileWatching = false; - await this.plugin.syncAllFiles(true); - await this.plugin.loadQueuedFiles(); - await this.plugin.saveSettings(); - - } - async askUseNewAdapter() { - if (!this.plugin.settings.useIndexedDBAdapter) { - const message = `Now this plugin 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 confirmWithMessage(this.plugin, "Database adapter", message, choices, CHOICE_YES, 10); - if (ret == CHOICE_YES) { - this.plugin.settings.useIndexedDBAdapter = true; - } - } - } - async resetLocalDatabase() { - if (this.plugin.settings.isConfigured && this.plugin.settings.additionalSuffixOfDatabaseName == "") { - // Discard the non-suffixed database - await this.plugin.resetLocalDatabase(); - } - this.plugin.settings.additionalSuffixOfDatabaseName = `${("appId" in this.app ? this.app.appId : "")}` - await this.plugin.resetLocalDatabase(); - } - async fetchRemoteChunks() { - if (!this.plugin.settings.doNotSuspendOnFetching && this.plugin.settings.readChunksOnline && this.plugin.settings.remoteType == REMOTE_COUCHDB) { - Logger(`Fetching chunks`, LOG_LEVEL_NOTICE); - const replicator = this.plugin.getReplicator() as LiveSyncCouchDBReplicator; - const remoteDB = await replicator.connectRemoteCouchDBWithSetting(this.settings, this.plugin.getIsMobile(), true); - if (typeof remoteDB == "string") { - Logger(remoteDB, LOG_LEVEL_NOTICE); - } else { - await fetchAllUsedChunks(this.localDatabase.localDatabase, remoteDB.db); - } - Logger(`Fetching chunks done`, LOG_LEVEL_NOTICE); - } - } - async fetchLocal(makeLocalChunkBeforeSync?: boolean) { - this.suspendExtraSync(); - await this.askUseNewAdapter(); - this.plugin.settings.isConfigured = true; - await this.suspendReflectingDatabase(); - await this.plugin.realizeSettingSyncMode(); - await this.resetLocalDatabase(); - await delay(1000); - await this.plugin.openDatabase(); - this.plugin.isReady = true; - if (makeLocalChunkBeforeSync) { - await this.plugin.createAllChunks(true); - } - await this.plugin.markRemoteResolved(); - await delay(500); - await this.plugin.replicateAllFromServer(true); - await delay(1000); - await this.plugin.replicateAllFromServer(true); - await this.resumeReflectingDatabase(); - await this.askHiddenFileConfiguration({ enableFetch: true }); - } - async fetchLocalWithRebuild() { - return await this.fetchLocal(true); - } - async rebuildRemote() { - this.suspendExtraSync(); - this.plugin.settings.isConfigured = true; - - await this.plugin.realizeSettingSyncMode(); - await this.plugin.markRemoteLocked(); - await this.plugin.tryResetRemoteDatabase(); - await this.plugin.markRemoteLocked(); - await delay(500); - await this.askHiddenFileConfiguration({ enableOverwrite: true }); - await delay(1000); - await this.plugin.replicateAllToServer(true); - await delay(1000); - await this.plugin.replicateAllToServer(true); - } - async rebuildEverything() { - this.suspendExtraSync(); - await this.askUseNewAdapter(); - this.plugin.settings.isConfigured = true; - await this.plugin.realizeSettingSyncMode(); - await this.resetLocalDatabase(); - await delay(1000); - await this.plugin.initializeDatabase(true); - await this.plugin.markRemoteLocked(); - await this.plugin.tryResetRemoteDatabase(); - await this.plugin.markRemoteLocked(); - await delay(500); - await this.askHiddenFileConfiguration({ enableOverwrite: true }); - await delay(1000); - await this.plugin.replicateAllToServer(true); - await delay(1000); - await this.plugin.replicateAllToServer(true); - - } -} diff --git a/src/features/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts similarity index 72% rename from src/features/CmdConfigSync.ts rename to src/features/ConfigSync/CmdConfigSync.ts index 13ae532..62acee4 100644 --- a/src/features/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -1,25 +1,26 @@ import { writable } from 'svelte/store'; -import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles, diff_match_patch } from "../deps.ts"; +import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles, diff_match_patch, Platform, addIcon } from "../../deps.ts"; -import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, AnyEntry, SavingEntry, diff_result } from "../lib/src/common/types.ts"; -import { CANCELLED, LEAVE_TO_SUBSEQUENT, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_SHINY } from "../lib/src/common/types.ts"; -import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "../common/types.ts"; -import { createBlob, createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocData, getDocDataAsArray, isDocContentSame, isLoadedEntry, isObjectDifferent } from "../lib/src/common/utils.ts"; -import { Logger } from "../lib/src/common/logger.ts"; -import { digestHash } from "../lib/src/string_and_binary/hash.ts"; -import { arrayBufferToBase64, decodeBinary, readString } from 'src/lib/src/string_and_binary/convert.ts'; -import { serialized, shareRunningResult } from "../lib/src/concurrency/lock.ts"; -import { LiveSyncCommands } from "./LiveSyncCommands.ts"; -import { stripAllPrefixes } from "../lib/src/string_and_binary/path.ts"; -import { EVEN, PeriodicProcessor, disposeMemoObject, isMarkedAsSameChanges, markChangesAreSame, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../common/utils.ts"; -import { PluginDialogModal } from "../common/dialogs.ts"; -import { JsonResolveModal } from "../ui/JsonResolveModal.ts"; -import { QueueProcessor } from '../lib/src/concurrency/processor.ts'; -import { pluginScanningCount } from '../lib/src/mock_and_interop/stores.ts'; -import type ObsidianLiveSyncPlugin from '../main.ts'; +import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, AnyEntry, SavingEntry, diff_result } from "../../lib/src/common/types.ts"; +import { CANCELLED, LEAVE_TO_SUBSEQUENT, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_SHINY } from "../../lib/src/common/types.ts"; +import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "../../common/types.ts"; +import { createBlob, createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocData, getDocDataAsArray, isDocContentSame, isLoadedEntry, isObjectDifferent } from "../../lib/src/common/utils.ts"; +import { digestHash } from "../../lib/src/string_and_binary/hash.ts"; +import { arrayBufferToBase64, decodeBinary, readString } from '../../lib/src/string_and_binary/convert.ts'; +import { serialized, shareRunningResult } from "../../lib/src/concurrency/lock.ts"; +import { LiveSyncCommands } from "../LiveSyncCommands.ts"; +import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts"; +import { EVEN, PeriodicProcessor, disposeMemoObject, isCustomisationSyncMetadata, isMarkedAsSameChanges, isPluginMetadata, markChangesAreSame, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../../common/utils.ts"; +import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts"; +import { QueueProcessor } from '../../lib/src/concurrency/processor.ts'; +import { pluginScanningCount } from '../../lib/src/mock_and_interop/stores.ts'; +import type ObsidianLiveSyncPlugin from '../../main.ts'; import { base64ToArrayBuffer, base64ToString } from 'octagonal-wheels/binary/base64'; -import { ConflictResolveModal } from '../ui/ConflictResolveModal.ts'; +import { ConflictResolveModal } from '../../modules/features/InteractiveConflictResolving/ConflictResolveModal.ts'; import { Semaphore } from 'octagonal-wheels/concurrency/semaphore'; +import type { IObsidianModule } from '../../modules/AbstractObsidianModule.ts'; +import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from '../../common/events.ts'; +import { PluginDialogModal } from "./PluginDialogModal.ts"; const d = "\u200b"; const d2 = "\n"; @@ -184,10 +185,10 @@ function deserialize(str: string[], def: T) { return o; } return JSON.parse(str.join("")) as T; - } catch (ex) { + } catch { try { return parseYaml(str.join("")); - } catch (ex) { + } catch { return def; } } @@ -322,13 +323,14 @@ export type PluginDataEx = { mtime: number, }; -export class ConfigSync extends LiveSyncCommands { +export class ConfigSync extends LiveSyncCommands implements IObsidianModule { constructor(plugin: ObsidianLiveSyncPlugin) { super(plugin); pluginScanningCount.onChanged((e) => { const total = e.value; pluginIsEnumerating.set(total != 0); }) + } get kvDB() { return this.plugin.kvDB; @@ -340,13 +342,16 @@ export class ConfigSync extends LiveSyncCommands { get useSyncPluginEtc() { return this.plugin.settings.usePluginEtc; } + _isThisModuleEnabled() { + return this.plugin.settings.usePluginSync; + } pluginDialog?: PluginDialogModal = undefined; periodicPluginSweepProcessor = new PeriodicProcessor(this.plugin, async () => await this.scanAllConfigFiles(false)); pluginList: IPluginDataExDisplay[] = []; showPluginSyncModal() { - if (!this.settings.usePluginSync) { + if (!this._isThisModuleEnabled()) { return; } if (this.pluginDialog) { @@ -367,7 +372,14 @@ export class ConfigSync extends LiveSyncCommands { this.hidePluginSyncModal(); this.periodicPluginSweepProcessor?.disable(); } + addRibbonIcon = this.plugin.addRibbonIcon.bind(this.plugin); onload() { + addIcon( + "custom-sync", + ` + + ` + ); this.plugin.addCommand({ id: "livesync-plugin-dialog-ex", name: "Show customization sync dialog", @@ -375,6 +387,10 @@ export class ConfigSync extends LiveSyncCommands { this.showPluginSyncModal(); }, }); + this.addRibbonIcon("custom-sync", "Show Customization sync", () => { + this.showPluginSyncModal(); + }).addClass("livesync-ribbon-showcustom"); + eventHub.onEvent(EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, () => this.showPluginSyncModal()); } getFileCategory(filePath: string): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "" { @@ -399,33 +415,41 @@ export class ConfigSync extends LiveSyncCommands { // Idea non-filter option? return this.getFileCategory(filePath) != ""; } - async onInitializeDatabase(showNotice: boolean) { - if (this.settings.usePluginSync) { - try { - Logger("Scanning customizations..."); - await this.scanAllConfigFiles(showNotice); - Logger("Scanning customizations : done"); - } catch (ex) { - Logger("Scanning customizations : failed"); - Logger(ex, LOG_LEVEL_VERBOSE); - } - - } - } - async beforeReplicate(showNotice: boolean) { - if (this.settings.autoSweepPlugins && this.settings.usePluginSync) { + async $everyOnDatabaseInitialized(showNotice: boolean) { + if (!this._isThisModuleEnabled()) return true; + try { + this._log("Scanning customizations..."); await this.scanAllConfigFiles(showNotice); + this._log("Scanning customizations : done"); + } catch (ex) { + this._log("Scanning customizations : failed"); + this._log(ex, LOG_LEVEL_VERBOSE); } + return true; } - async onResume() { - if (this.plugin.suspended) { - return; + async $everyBeforeReplicate(showNotice: boolean) { + if (!this._isThisModuleEnabled()) return true; + if (this.settings.autoSweepPlugins) { + await this.scanAllConfigFiles(showNotice); + return true; } - if (this.settings.autoSweepPlugins && this.settings.usePluginSync) { + return true; + } + async $everyOnResumeProcess(): Promise { + if (!this._isThisModuleEnabled()) return true; + if (this._isMainSuspended()) { + return true; + } + if (this.settings.autoSweepPlugins) { await this.scanAllConfigFiles(false); } this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0); - + return true; + } + $everyAfterResumeProcess(): Promise { + const q = activeDocument.querySelector(`.livesync-ribbon-showcustom`); + q?.toggleClass("sls-hidden", !this._isThisModuleEnabled()); + return Promise.resolve(true); } async reloadPluginList(showMessage: boolean) { this.pluginList = []; @@ -452,7 +476,7 @@ export class ConfigSync extends LiveSyncCommands { xFiles.push(work); } if (missingHash) { - Logger(`Digest created for ${path} to improve checking`, LOG_LEVEL_VERBOSE); + this._log(`Digest created for ${path} to improve checking`, LOG_LEVEL_VERBOSE); wx.data = serialize(data); fireAndForget(() => this.localDatabase.putDBEntry(createSavingEntryFromLoadedEntry(wx))); } @@ -487,8 +511,8 @@ export class ConfigSync extends LiveSyncCommands { return []; } catch (ex) { - Logger(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); + this._log(ex, LOG_LEVEL_VERBOSE); } return []; }, { suspended: false, batchSize: 1, concurrentLimit: 10, delay: 100, yieldThreshold: 10, maintainDelay: false, totalRemainingReactiveSource: pluginScanningCount }).startPipeline(); @@ -511,8 +535,8 @@ export class ConfigSync extends LiveSyncCommands { return []; } catch (ex) { - Logger(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); + this._log(ex, LOG_LEVEL_VERBOSE); } return []; }, { suspended: false, batchSize: 1, concurrentLimit: 10, delay: 100, yieldThreshold: 10, maintainDelay: false, totalRemainingReactiveSource: pluginScanningCount }).startPipeline(); @@ -558,11 +582,11 @@ export class ConfigSync extends LiveSyncCommands { if (!loaded) { const d = await this.localDatabase.getDBEntry(unifiedPathV2); if (!d) { - Logger(`The file ${unifiedPathV2} is not found`, LOG_LEVEL_VERBOSE); + this._log(`The file ${unifiedPathV2} is not found`, LOG_LEVEL_VERBOSE); return false; } if (!isLoadedEntry(d)) { - Logger(`The file ${unifiedPathV2} is not a note`, LOG_LEVEL_VERBOSE); + this._log(`The file ${unifiedPathV2} is not a note`, LOG_LEVEL_VERBOSE); return false; } loaded = d; @@ -588,8 +612,8 @@ export class ConfigSync extends LiveSyncCommands { this.pluginList.filter(e => e instanceof PluginDataExDisplayV2 && e.confKey == confKey).forEach(e => (e as PluginDataExDisplayV2).applyLoadedManifest()); pluginList.set(this.pluginList); } catch (ex) { - Logger(`The file ${loaded.path} seems to manifest, but could not be decoded as JSON`, LOG_LEVEL_VERBOSE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`The file ${loaded.path} seems to manifest, but could not be decoded as JSON`, LOG_LEVEL_VERBOSE); + this._log(ex, LOG_LEVEL_VERBOSE); } this.loadedManifest_mTime.set(confKey, file.mtime); } else { @@ -662,22 +686,22 @@ export class ConfigSync extends LiveSyncCommands { async migrateV1ToV2(showMessage: boolean, entry: AnyEntry): Promise { const v1Path = entry.path; - Logger(`Migrating ${entry.path} to V2`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO); + this._log(`Migrating ${entry.path} to V2`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO); if (entry.deleted) { - Logger(`The entry ${v1Path} is already deleted`, LOG_LEVEL_VERBOSE); + this._log(`The entry ${v1Path} is already deleted`, LOG_LEVEL_VERBOSE); return; } if (!v1Path.endsWith(".md") && !v1Path.startsWith(ICXHeader)) { - Logger(`The entry ${v1Path} is not a customisation sync binder`, LOG_LEVEL_VERBOSE); + this._log(`The entry ${v1Path} is not a customisation sync binder`, LOG_LEVEL_VERBOSE); return } if (v1Path.indexOf("%") !== -1) { - Logger(`The entry ${v1Path} is already migrated`, LOG_LEVEL_VERBOSE); + this._log(`The entry ${v1Path} is already migrated`, LOG_LEVEL_VERBOSE); return; } const loadedEntry = await this.localDatabase.getDBEntry(v1Path); if (!loadedEntry) { - Logger(`The entry ${v1Path} is not found`, LOG_LEVEL_VERBOSE); + this._log(`The entry ${v1Path} is not found`, LOG_LEVEL_VERBOSE); return; } @@ -698,8 +722,8 @@ export class ConfigSync extends LiveSyncCommands { const relativeFilename = f.filename.split("/").slice(deletePrefixCount).join("/"); const v2Path = (prefixPath + relativeFilename) as FilePathWithPrefix; // console.warn(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`); - Logger(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`, LOG_LEVEL_VERBOSE); - const newId = await this.plugin.path2id(v2Path); + this._log(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`, LOG_LEVEL_VERBOSE); + const newId = await this.plugin.$$path2id(v2Path); // const buf = const data = createBlob([DUMMY_HEAD, DUMMY_END, ...getDocDataAsArray(f.data)]); @@ -717,19 +741,19 @@ export class ConfigSync extends LiveSyncCommands { } const r = await this.plugin.localDatabase.putDBEntry(saving); if (r && r.ok) { - Logger(`Migrated ${v1Path} / ${f.filename} to ${v2Path}`, LOG_LEVEL_INFO); + this._log(`Migrated ${v1Path} / ${f.filename} to ${v2Path}`, LOG_LEVEL_INFO); const delR = await this.deleteConfigOnDatabase(v1Path); if (delR) { - Logger(`Deleted ${v1Path} successfully`, LOG_LEVEL_INFO); + this._log(`Deleted ${v1Path} successfully`, LOG_LEVEL_INFO); } else { - Logger(`Failed to delete ${v1Path}`, LOG_LEVEL_NOTICE); + this._log(`Failed to delete ${v1Path}`, LOG_LEVEL_NOTICE); } } } } async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise { - if (!this.settings.usePluginSync) { + if (!this._isThisModuleEnabled()) { this.pluginScanProcessor.clearQueue(); this.pluginList = []; pluginList.set(this.pluginList) @@ -777,9 +801,9 @@ export class ConfigSync extends LiveSyncCommands { } const fileA = await loadFile(dataA); const fileB = await loadFile(dataB); - Logger(`Comparing: ${dataA.documentPath} <-> ${dataB.documentPath}`, LOG_LEVEL_VERBOSE); + this._log(`Comparing: ${dataA.documentPath} <-> ${dataB.documentPath}`, LOG_LEVEL_VERBOSE); if (!fileA || !fileB) { - Logger(`Could not load ${dataA.name} for comparison: ${!fileA ? dataA.term : ""}${!fileB ? dataB.term : ""}`, LOG_LEVEL_NOTICE); + this._log(`Could not load ${dataA.name} for comparison: ${!fileA ? dataA.term : ""}${!fileB ? dataB.term : ""}`, LOG_LEVEL_NOTICE); return false; } let path = stripAllPrefixes(fileA.path.split("/").slice(-1).join("/") as FilePath); // TODO:adjust @@ -788,15 +812,15 @@ export class ConfigSync extends LiveSyncCommands { } if (fileA.path.endsWith(".json")) { return serialized("config:merge-data", () => new Promise((res) => { - Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE); + this._log("Opening data-merging dialog", LOG_LEVEL_VERBOSE); // const docs = [docA, docB]; const modal = new JsonResolveModal(this.app, path, [fileA, fileB], async (keep, result) => { if (result == null) return res(false); try { res(await this.applyData(dataA, result)); } catch (ex) { - Logger("Could not apply merged file"); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log("Could not apply merged file"); + this._log(ex, LOG_LEVEL_VERBOSE); res(false); } }, "Local", `${dataB.term}`, "B", true, true, "Difference between local and remote"); @@ -822,7 +846,7 @@ export class ConfigSync extends LiveSyncCommands { right: { rev: "B", ...fileB, data: docBData }, diff: diff } - console.dir(diffResult); + // console.dir(diffResult); const d = new ConflictResolveModal(this.app, path, diffResult, true, dataB.term); d.open(); const ret = await d.waitForResult(); @@ -841,11 +865,11 @@ export class ConfigSync extends LiveSyncCommands { if (content) { // const dt = createBlob(content); const filename = data.files[0].filename; - Logger(`Applying ${filename} of ${data.displayName || data.name}..`); + this._log(`Applying ${filename} of ${data.displayName || data.name}..`); const path = `${baseDir}/${filename}` as FilePath; - await this.vaultAccess.ensureDirectory(path); + await this.plugin.storageAccess.ensureDir(path); // If the content has applied, modified time will be updated to the current time. - await this.vaultAccess.adapterWrite(path, content); + await this.plugin.storageAccess.writeHiddenFileAuto(path, content); await this.storeCustomisationFileV2(path, this.plugin.deviceAndVaultName); } else { @@ -854,50 +878,54 @@ export class ConfigSync extends LiveSyncCommands { // If files have applied, modified time will be updated to the current time. const stat = { mtime: f.mtime, ctime: f.ctime }; const path = `${baseDir}/${f.filename}` as FilePath; - Logger(`Applying ${f.filename} of ${data.displayName || data.name}..`); + this._log(`Applying ${f.filename} of ${data.displayName || data.name}..`); // const contentEach = createBlob(f.data); - this.vaultAccess.ensureDirectory(path); + await this.plugin.storageAccess.ensureDir(path); if (f.datatype == "newnote") { let oldData; try { - oldData = await this.vaultAccess.adapterReadBinary(path); + oldData = await this.plugin.storageAccess.readHiddenFileBinary(path); } catch (ex) { + this._log(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE); + this._log(ex, LOG_LEVEL_VERBOSE); oldData = new ArrayBuffer(0); } const content = base64ToArrayBuffer(f.data); if (await isDocContentSame(oldData, content)) { - Logger(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE); + this._log(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE); continue; } - await this.vaultAccess.adapterWrite(path, content, stat); + await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat); } else { let oldData; try { - oldData = await this.vaultAccess.adapterRead(path); + oldData = await this.plugin.storageAccess.readHiddenFileText(path); } catch (ex) { + this._log(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE); + this._log(ex, LOG_LEVEL_VERBOSE); oldData = ""; } const content = getDocData(f.data); if (await isDocContentSame(oldData, content)) { - Logger(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE); + this._log(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE); continue; } - await this.vaultAccess.adapterWrite(path, content, stat); + await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat); } - Logger(`Applied ${f.filename} of ${data.displayName || data.name}..`); + this._log(`Applied ${f.filename} of ${data.displayName || data.name}..`); await this.storeCustomisationFileV2(path, this.plugin.deviceAndVaultName); } } } catch (ex) { - Logger(`Applying ${data.displayName || data.name}.. Failed`, LOG_LEVEL_NOTICE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Applying ${data.displayName || data.name}.. Failed`, LOG_LEVEL_NOTICE); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } return true; } async applyData(data: IPluginDataExDisplay, content?: string): Promise { - Logger(`Applying ${data.displayName || data.name + this._log(`Applying ${data.displayName || data.name }..`); if (data instanceof PluginDataExDisplayV2) { @@ -912,22 +940,22 @@ export class ConfigSync extends LiveSyncCommands { } const loadedData = deserialize(getDocDataAsArray(dx.data), {}) as PluginDataEx; for (const f of loadedData.files) { - Logger(`Applying ${f.filename} of ${data.displayName || data.name}..`); + this._log(`Applying ${f.filename} of ${data.displayName || data.name}..`); try { // console.dir(f); const path = `${baseDir}/${f.filename}`; - await this.vaultAccess.ensureDirectory(path); + await this.plugin.storageAccess.ensureDir(path); if (!content) { const dt = decodeBinary(f.data); - await this.vaultAccess.adapterWrite(path, dt); + await this.plugin.storageAccess.writeHiddenFileAuto(path, dt); } else { - await this.vaultAccess.adapterWrite(path, content); + await this.plugin.storageAccess.writeHiddenFileAuto(path, content); } - Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`); + this._log(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`); } catch (ex) { - Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Applying ${f.filename} of ${data.displayName || data.name}.. Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); } } @@ -935,7 +963,7 @@ export class ConfigSync extends LiveSyncCommands { await this.storeCustomizationFiles(uPath); await this.updatePluginList(true, uPath); await delay(100); - Logger(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL_NOTICE); + this._log(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL_NOTICE); if (data.category == "PLUGIN_DATA" || data.category == "PLUGIN_MAIN") { //@ts-ignore const manifests = Object.values(this.app.plugins.manifests) as any as PluginManifest[]; @@ -943,20 +971,20 @@ export class ConfigSync extends LiveSyncCommands { const enabledPlugins = this.app.plugins.enabledPlugins as Set; const pluginManifest = manifests.find((manifest) => enabledPlugins.has(manifest.id) && manifest.dir == `${baseDir}/plugins/${data.name}`); if (pluginManifest) { - Logger(`Unloading plugin: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); + this._log(`Unloading plugin: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); // @ts-ignore await this.app.plugins.unloadPlugin(pluginManifest.id); // @ts-ignore await this.app.plugins.loadPlugin(pluginManifest.id); - Logger(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); + this._log(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); } } else if (data.category == "CONFIG") { - this.plugin.askReload(); + this.plugin.$$askReload(); } return true; } catch (ex) { - Logger(`Applying ${data.displayName || data.name}.. Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Applying ${data.displayName || data.name}.. Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } } @@ -978,84 +1006,81 @@ export class ConfigSync extends LiveSyncCommands { await Promise.allSettled(p); // await this.deleteConfigOnDatabase(data.documentPath); // await this.updatePluginList(false, data.documentPath); - Logger(`Deleted: ${data.category}/${data.name} of ${data.category} (${delList.length} items)`, LOG_LEVEL_NOTICE); + this._log(`Deleted: ${data.category}/${data.name} of ${data.category} (${delList.length} items)`, LOG_LEVEL_NOTICE); } return true; } catch (ex) { - Logger(`Failed to delete: ${data.documentPath}`, LOG_LEVEL_NOTICE); + this._log(`Failed to delete: ${data.documentPath}`, LOG_LEVEL_NOTICE); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } } - async parseReplicationResultItem(docs: PouchDB.Core.ExistingDocument) { - if (docs._id.startsWith(ICXHeader)) { - if (this.plugin.settings.usePluginSync) { - await this.updatePluginList(false, (docs as AnyEntry).path ? (docs as AnyEntry).path : this.getPath((docs as AnyEntry))); - } - if (this.plugin.settings.usePluginSync && this.plugin.settings.notifyPluginOrSettingUpdated) { - if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) { - const fragment = createFragment((doc) => { - doc.createEl("span", undefined, (a) => { - a.appendText(`Some configuration has been arrived, Press `); - a.appendChild(a.createEl("a", undefined, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", () => { - this.showPluginSyncModal(); - }); - })); + async $anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument) { + if (!docs._id.startsWith(ICXHeader)) return undefined; + if (this._isThisModuleEnabled()) { + await this.updatePluginList(false, (docs as AnyEntry).path ? (docs as AnyEntry).path : this.getPath((docs as AnyEntry))); + } + if (this._isThisModuleEnabled() && this.plugin.settings.notifyPluginOrSettingUpdated) { + if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) { + const fragment = createFragment((doc) => { + doc.createEl("span", undefined, (a) => { + a.appendText(`Some configuration has been arrived, Press `); + a.appendChild(a.createEl("a", undefined, (anchor) => { + anchor.text = "HERE"; + anchor.addEventListener("click", () => { + this.showPluginSyncModal(); + }); + })); - a.appendText(` to open the config sync dialog , or press elsewhere to dismiss this message.`); - }); + a.appendText(` to open the config sync dialog , or press elsewhere to dismiss this message.`); }); + }); - const updatedPluginKey = "popupUpdated-plugins"; - scheduleTask(updatedPluginKey, 1000, async () => { - const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0)); + const updatedPluginKey = "popupUpdated-plugins"; + scheduleTask(updatedPluginKey, 1000, async () => { + const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0)); + //@ts-ignore + const isShown = popup?.noticeEl?.isShown(); + if (!isShown) { + memoObject(updatedPluginKey, new Notice(fragment, 0)); + } + scheduleTask(updatedPluginKey + "-close", 20000, () => { + const popup = retrieveMemoObject(updatedPluginKey); + if (!popup) + return; //@ts-ignore - const isShown = popup?.noticeEl?.isShown(); - if (!isShown) { - memoObject(updatedPluginKey, new Notice(fragment, 0)); + if (popup?.noticeEl?.isShown()) { + popup.hide(); } - scheduleTask(updatedPluginKey + "-close", 20000, () => { - const popup = retrieveMemoObject(updatedPluginKey); - if (!popup) - return; - //@ts-ignore - if (popup?.noticeEl?.isShown()) { - popup.hide(); - } - disposeMemoObject(updatedPluginKey); - }); + disposeMemoObject(updatedPluginKey); }); - } + }); } - return true; } - return false; + return true; } - async realizeSettingSyncMode(): Promise { + async $everyRealizeSettingSyncMode(): Promise { this.periodicPluginSweepProcessor?.disable(); - if (this.plugin.suspended) - return; - if (!this.settings.usePluginSync) { - return; - } + if (!this._isMainReady) return true; + if (!this._isMainSuspended()) return true; + if (!this._isThisModuleEnabled()) return true; if (this.settings.autoSweepPlugins) { await this.scanAllConfigFiles(false); } this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0); - return; + return true; } recentProcessedInternalFiles = [] as string[]; async makeEntryFromFile(path: FilePath): Promise { - const stat = await this.vaultAccess.adapterStat(path); + const stat = await this.plugin.storageAccess.statHidden(path); let version: string | undefined; let displayName: string | undefined; if (!stat) { return false; } - const contentBin = await this.vaultAccess.adapterReadBinary(path); + const contentBin = await this.plugin.storageAccess.readHiddenFileBinary(path); let content: string[]; try { content = await arrayBufferToBase64(contentBin); @@ -1070,12 +1095,13 @@ export class ConfigSync extends LiveSyncCommands { displayName = `${json.name}`; } } catch (ex) { - Logger(`Configuration sync data: ${path} looks like manifest, but could not read the version`, LOG_LEVEL_INFO); + this._log(`Configuration sync data: ${path} looks like manifest, but could not read the version`, LOG_LEVEL_INFO); + this._log(ex, LOG_LEVEL_VERBOSE); } } } catch (ex) { - Logger(`The file ${path} could not be encoded`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`The file ${path} could not be encoded`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } const mtime = stat.mtime; @@ -1096,12 +1122,12 @@ export class ConfigSync extends LiveSyncCommands { const prefixedFileName = vf; const id = await this.path2id(prefixedFileName); - const stat = await this.vaultAccess.adapterStat(path); + const stat = await this.plugin.storageAccess.statHidden(path); if (!stat) { return false; } const mtime = stat.mtime; - const content = await this.vaultAccess.adapterReadBinary(path); + const content = await this.plugin.storageAccess.readHiddenFileBinary(path); const contentBlob = createBlob([DUMMY_HEAD, DUMMY_END, ...await arrayBufferToBase64(content)]); // const contentBlob = createBlob(content); try { @@ -1123,7 +1149,7 @@ export class ConfigSync extends LiveSyncCommands { }; } else { if (isMarkedAsSameChanges(prefixedFileName, [old.mtime, mtime + 1]) == EVEN) { - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Already checked the same)`, LOG_LEVEL_DEBUG); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Already checked the same)`, LOG_LEVEL_DEBUG); return; } const docXDoc = await this.localDatabase.getDBEntryFromMeta(old, {}, false, false); @@ -1135,7 +1161,7 @@ export class ConfigSync extends LiveSyncCommands { const oldContent = dataSrc.substring(dataStart + DUMMY_END.length); const oldContentArray = base64ToArrayBuffer(oldContent); if (await isDocContentSame(oldContentArray, content)) { - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (the same content)`, LOG_LEVEL_VERBOSE); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (the same content)`, LOG_LEVEL_VERBOSE); markChangesAreSame(prefixedFileName, old.mtime, mtime + 1); return true; } @@ -1152,12 +1178,12 @@ export class ConfigSync extends LiveSyncCommands { }; } const ret = await this.localDatabase.putDBEntry(saveData); - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Done`); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Done`); fireAndForget(() => this.updatePluginListV2(false, this.filenameWithUnifiedKey(path))); return ret; } catch (ex) { - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } }) @@ -1165,7 +1191,7 @@ export class ConfigSync extends LiveSyncCommands { async storeCustomizationFiles(path: FilePath, termOverRide?: string) { const term = termOverRide || this.plugin.deviceAndVaultName; if (term == "") { - Logger("We have to configure the device name", LOG_LEVEL_NOTICE); + this._log("We have to configure the device name", LOG_LEVEL_NOTICE); return; } if (this.useV2) { @@ -1207,7 +1233,7 @@ export class ConfigSync extends LiveSyncCommands { for (const target of fileTargets) { const data = await this.makeEntryFromFile(target); if (data == false) { - Logger(`Config: skipped (Possibly is not exist): ${target} `, LOG_LEVEL_VERBOSE); + this._log(`Config: skipped (Possibly is not exist): ${target} `, LOG_LEVEL_VERBOSE); continue; } if (data.version) { @@ -1222,9 +1248,9 @@ export class ConfigSync extends LiveSyncCommands { } dt.mtime = mtime; - // Logger(`Configuration saving: ${prefixedFileName}`); + // this._log(`Configuration saving: ${prefixedFileName}`); if (dt.files.length == 0) { - Logger(`Nothing left: deleting.. ${path}`); + this._log(`Nothing left: deleting.. ${path}`); await this.deleteConfigOnDatabase(prefixedFileName); await this.updatePluginList(false, prefixedFileName); return @@ -1250,7 +1276,7 @@ export class ConfigSync extends LiveSyncCommands { }; } else { if (old.mtime == mtime) { - // Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same time)`, LOG_LEVEL_VERBOSE); + // this._log(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same time)`, LOG_LEVEL_VERBOSE); return true; } const oldC = await this.localDatabase.getDBEntryFromMeta(old, {}, false, false); @@ -1258,11 +1284,11 @@ export class ConfigSync extends LiveSyncCommands { const d = await deserialize(getDocDataAsArray(oldC.data), {}) as PluginDataEx; if (d.files.length == dt.files.length) { const diffs = (d.files.map(previous => ({ prev: previous, curr: dt.files.find(e => e.filename == previous.filename) })).map(async e => { - try { return await isDocContentSame(e.curr?.data ?? [], e.prev.data) } catch (_) { return false } + try { return await isDocContentSame(e.curr?.data ?? [], e.prev.data) } catch { return false } })) const isSame = (await Promise.all(diffs)).every(e => e == true); if (isSame) { - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same content)`, LOG_LEVEL_VERBOSE); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same content)`, LOG_LEVEL_VERBOSE); return true; } } @@ -1281,20 +1307,25 @@ export class ConfigSync extends LiveSyncCommands { } const ret = await this.localDatabase.putDBEntry(saveData); await this.updatePluginList(false, saveData.path); - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Done`); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Done`); return ret; } catch (ex) { - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } }) - } + async $anyProcessOptionalFileEvent(path: FilePath): Promise { + return await this.watchVaultRawEventsAsync(path); + } + async watchVaultRawEventsAsync(path: FilePath) { - if (!this.settings.usePluginSync) return false; - if (!this.isTargetPath(path)) return false; - const stat = await this.vaultAccess.adapterStat(path); + if (!this._isMainReady) return false; + if (this._isMainSuspended()) return false; + if (!this._isThisModuleEnabled()) return false; + // if (!this.isTargetPath(path)) return false; + const stat = await this.plugin.storageAccess.statHidden(path); // Make sure that target is a file. if (stat && stat.type != "file") return false; @@ -1304,13 +1335,16 @@ export class ConfigSync extends LiveSyncCommands { e.mode != MODE_SELECTIVE && e.mode != MODE_SHINY ).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) { - Logger(`Customization file skipped: ${path}`, LOG_LEVEL_VERBOSE); - return; + this._log(`Customization file skipped: ${path}`, LOG_LEVEL_VERBOSE); + // This file could be handled by the other module. + return false; } + // this._log(`Customization file detected: ${path}`, LOG_LEVEL_VERBOSE); const storageMTime = ~~((stat && stat.mtime || 0) / 1000); const key = `${path}-${storageMTime}`; if (this.recentProcessedInternalFiles.contains(key)) { // If recently processed, it may caused by self. + // return true to prevent pass the event to the next. return true; } this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100); @@ -1319,18 +1353,18 @@ export class ConfigSync extends LiveSyncCommands { scheduleTask(keySchedule, 100, async () => { await this.storeCustomizationFiles(path); }) + // Okay, it may handled after 100ms. + // This was my own job. + return true; } - - - async scanAllConfigFiles(showMessage: boolean) { await shareRunningResult("scanAllConfigFiles", async () => { const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; - Logger("Scanning customizing files.", logLevel, "scan-all-config"); + this._log("Scanning customizing files.", logLevel, "scan-all-config"); const term = this.plugin.deviceAndVaultName; if (term == "") { - Logger("We have to configure the device name", LOG_LEVEL_NOTICE); + this._log("We have to configure the device name", LOG_LEVEL_NOTICE); return; } const filesAll = await this.scanInternalFiles(); @@ -1358,8 +1392,8 @@ export class ConfigSync extends LiveSyncCommands { await this.deleteConfigOnDatabase(unifiedFilenameWithKey); } } catch (ex) { - Logger(`scanAllConfigFiles - Error: ${item._id}`, LOG_LEVEL_VERBOSE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`scanAllConfigFiles - Error: ${item._id}`, LOG_LEVEL_VERBOSE); + this._log(ex, LOG_LEVEL_VERBOSE); } finally { releaser(); } @@ -1374,8 +1408,8 @@ export class ConfigSync extends LiveSyncCommands { try { await this.storeCustomisationFileV2(filePath, term); } catch (ex) { - Logger(`scanAllConfigFiles - Error: ${filePath}`, LOG_LEVEL_VERBOSE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`scanAllConfigFiles - Error: ${filePath}`, LOG_LEVEL_VERBOSE); + this._log(ex, LOG_LEVEL_VERBOSE); } finally { releaser(); @@ -1383,8 +1417,7 @@ export class ConfigSync extends LiveSyncCommands { }) } await Promise.all(taskExtra.map(e => e())); - - this.updatePluginList(false).then(/* fire and forget */); + fireAndForget(() => this.updatePluginList(false)); } else { const files = filesAll.filter(e => this.isTargetPath(e)).map(e => ({ key: this.filenameToUnifiedKey(e), file: e })); const virtualPathsOfLocalFiles = [...new Set(files.map(e => e.key))]; @@ -1393,7 +1426,7 @@ export class ConfigSync extends LiveSyncCommands { for (const vp of virtualPathsOfLocalFiles) { const p = files.find(e => e.key == vp)?.file; if (!p) { - Logger(`scanAllConfigFiles - File not found: ${vp}`, LOG_LEVEL_VERBOSE); + this._log(`scanAllConfigFiles - File not found: ${vp}`, LOG_LEVEL_VERBOSE); continue; } await this.storeCustomizationFiles(p); @@ -1402,7 +1435,7 @@ export class ConfigSync extends LiveSyncCommands { for (const vp of deleteCandidate) { await this.deleteConfigOnDatabase(vp); } - this.updatePluginList(false).then(/* fire and forget */); + fireAndForget(() => this.updatePluginList(false)) } }); } @@ -1416,11 +1449,11 @@ export class ConfigSync extends LiveSyncCommands { const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, undefined, false) as InternalFileEntry | false; let saveData: InternalFileEntry; if (old === false) { - Logger(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted (Not found on database)`); + this._log(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted (Not found on database)`); return true; } else { if (old.deleted) { - Logger(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted`); + this._log(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted`); return true; } saveData = @@ -1435,11 +1468,11 @@ export class ConfigSync extends LiveSyncCommands { } await this.localDatabase.putRaw(saveData); await this.updatePluginList(false, prefixedFileName); - Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Done`); + this._log(`STORAGE -x> DB:${prefixedFileName}: (config) Done`); return true; } catch (ex) { - Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`STORAGE -x> DB:${prefixedFileName}: (config) Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } }); @@ -1450,7 +1483,99 @@ export class ConfigSync extends LiveSyncCommands { return filenames as FilePath[]; } + async $allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean, enableOverwrite?: boolean }): Promise { + await this._askHiddenFileConfiguration(opt); + return true; + } + async _askHiddenFileConfiguration(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) { + const message = `Would you like to enable **Customization sync**? +> [!DETAILS]- +> This feature allows you to sync your customisations -- such as configurations, themes, snippets, and plugins -- across your devices in a fully controlled manner, unlike the fully automatic behaviour of hidden file synchronisation. +> +> You may use this feature alongside hidden file synchronisation. When both features are enabled, items configured as \`Automatic\` in this feature will be managed by **hidden file synchronisation**. +> Do not worry, you will be prompted to enable or keep disabled **hidden file synchronisation** after this dialogue. +` + const CHOICE_CUSTOMIZE = "Yes, Enable it"; + const CHOICE_DISABLE = "No, Disable it"; + const CHOICE_DISMISS = "Later"; + const choices = []; + + choices.push(CHOICE_CUSTOMIZE); + choices.push(CHOICE_DISABLE); + choices.push(CHOICE_DISMISS); + + const ret = await this.plugin.confirm.confirmWithMessage("Customisation sync", message, choices, CHOICE_DISMISS, 40); + if (ret == CHOICE_CUSTOMIZE) { + await this.configureHiddenFileSync("CUSTOMIZE"); + } else if (ret == CHOICE_DISABLE) { + await this.configureHiddenFileSync("DISABLE_CUSTOM"); + } + + } + + $anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise { + if (isPluginMetadata(path)) { + return Promise.resolve("newer") + } + if (isCustomisationSyncMetadata(path)) { + return Promise.resolve("newer"); + } + return Promise.resolve(false); + } + + $allSuspendExtraSync(): Promise { + if (this.plugin.settings.usePluginSync || this.plugin.settings.autoSweepPlugins) { + this._log("Customisation sync have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) + this.plugin.settings.usePluginSync = false; + this.plugin.settings.autoSweepPlugins = false; + } + return Promise.resolve(true); + } + + async $anyConfigureOptionalSyncFeature(mode: "CUSTOMIZE" | "DISABLE" | "DISABLE_CUSTOM") { + await this.configureHiddenFileSync(mode); + } + async configureHiddenFileSync(mode: "CUSTOMIZE" | "DISABLE" | "DISABLE_CUSTOM") { + if (mode == "DISABLE") { + this.plugin.settings.usePluginSync = false; + await this.plugin.saveSettings(); + return; + } + + if (mode == "CUSTOMIZE") { + if (!this.plugin.deviceAndVaultName) { + let name = await this.plugin.confirm.askString("Device name", "Please set this device name", `desktop`); + if (!name) { + if (Platform.isAndroidApp) { + name = "android-app" + } else if (Platform.isIosApp) { + name = "ios" + } else if (Platform.isMacOS) { + name = "macos" + } else if (Platform.isMobileApp) { + name = "mobile-app" + } else if (Platform.isMobile) { + name = "mobile" + } else if (Platform.isSafari) { + name = "safari" + } else if (Platform.isDesktop) { + name = "desktop" + } else if (Platform.isDesktopApp) { + name = "desktop-app" + } else { + name = "unknown" + } + name = name + Math.random().toString(36).slice(-4); + } + this.plugin.deviceAndVaultName = name; + } + this.plugin.settings.usePluginSync = true; + this.plugin.settings.useAdvancedMode = true; + await this.plugin.saveSettings(); + await this.scanAllConfigFiles(true); + } + } async getFiles( path: string, @@ -1461,8 +1586,8 @@ export class ConfigSync extends LiveSyncCommands { try { w = await this.app.vault.adapter.list(path); } catch (ex) { - Logger(`Could not traverse(ConfigSync):${path}`, LOG_LEVEL_INFO); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Could not traverse(ConfigSync):${path}`, LOG_LEVEL_INFO); + this._log(ex, LOG_LEVEL_VERBOSE); return []; } let files = [ diff --git a/src/ui/components/PluginCombo.svelte b/src/features/ConfigSync/PluginCombo.svelte similarity index 96% rename from src/ui/components/PluginCombo.svelte rename to src/features/ConfigSync/PluginCombo.svelte index 3624061..bd07303 100644 --- a/src/ui/components/PluginCombo.svelte +++ b/src/features/ConfigSync/PluginCombo.svelte @@ -1,10 +1,10 @@ + +

TESTING BENCH: Self-hosted LiveSync

+ +

Module Checks

+ + + + + +{#each resultLines as [result, line, message]} +
+ [{result ? "PASS" : "FAILED"}] {line} +
{message}
+
+{/each} + +

Synchronisation Result Status

+
{syncStatus.join("\n")}
+ +

Performance test

+ + + +
+ + diff --git a/src/tests/TestPaneView.ts b/src/modules/extras/devUtil/TestPaneView.ts similarity index 75% rename from src/tests/TestPaneView.ts rename to src/modules/extras/devUtil/TestPaneView.ts index e69997c..a0b9b17 100644 --- a/src/tests/TestPaneView.ts +++ b/src/modules/extras/devUtil/TestPaneView.ts @@ -3,13 +3,15 @@ import { WorkspaceLeaf } from "obsidian"; import TestPaneComponent from "./TestPane.svelte" -import type ObsidianLiveSyncPlugin from "../main" +import type ObsidianLiveSyncPlugin from "../../../main.ts" +import type { ModuleDev } from "../ModuleDev.ts"; export const VIEW_TYPE_TEST = "ols-pane-test"; //Log view export class TestPaneView extends ItemView { component?: TestPaneComponent; plugin: ObsidianLiveSyncPlugin; + moduleDev: ModuleDev; icon = "view-log"; title: string = "Self-hosted LiveSync Test and Results" navigation = true; @@ -18,9 +20,10 @@ export class TestPaneView extends ItemView { return "view-log"; } - constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin) { + constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin, moduleDev: ModuleDev) { super(leaf); this.plugin = plugin; + this.moduleDev = moduleDev; } @@ -37,13 +40,16 @@ export class TestPaneView extends ItemView { this.component = new TestPaneComponent({ target: this.contentEl, props: { - plugin: this.plugin + plugin: this.plugin, + moduleDev: this.moduleDev }, }); + await Promise.resolve(); } // eslint-disable-next-line require-await async onClose() { this.component?.$destroy(); + await Promise.resolve(); } } diff --git a/src/tests/testUtils.ts b/src/modules/extras/devUtil/testUtils.ts similarity index 80% rename from src/tests/testUtils.ts rename to src/modules/extras/devUtil/testUtils.ts index 84b9e8d..869d1fa 100644 --- a/src/tests/testUtils.ts +++ b/src/modules/extras/devUtil/testUtils.ts @@ -1,6 +1,6 @@ -import { fireAndForget } from "src/lib/src/common/utils"; -import { serialized } from "src/lib/src/concurrency/lock"; -import type ObsidianLiveSyncPlugin from "src/main"; +import { fireAndForget } from "../../../lib/src/common/utils.ts"; +import { serialized } from "../../../lib/src/concurrency/lock.ts"; +import type ObsidianLiveSyncPlugin from "../../../main.ts"; let plugin: ObsidianLiveSyncPlugin; export function enableTestFunction(plugin_: ObsidianLiveSyncPlugin) { @@ -37,8 +37,9 @@ export function addDebugFileLog(message: any, stackLog = false) { // const out = "--" + timestamp + "--\n" + messageContent + " " + (stack || ""); // const out try { - await plugin.vaultAccess.adapterAppend(plugin.app.vault.configDir + "/ls-debug/" + outFile, JSON.stringify(out) + "\n") - } catch (ex) { + await plugin.storageAccess.appendHiddenFile(plugin.app.vault.configDir + "/ls-debug/" + outFile, JSON.stringify(out) + "\n") + } catch { + //NO OP } })); diff --git a/src/tests/tests.ts b/src/modules/extras/devUtil/tests.ts similarity index 83% rename from src/tests/tests.ts rename to src/modules/extras/devUtil/tests.ts index 1fae9fa..f4c5119 100644 --- a/src/tests/tests.ts +++ b/src/modules/extras/devUtil/tests.ts @@ -1,5 +1,5 @@ -import { Trench } from "../lib/src/memory/memutil.ts"; -import type ObsidianLiveSyncPlugin from "../main.ts"; +import { Trench } from "../../../lib/src/memory/memutil.ts"; +import type ObsidianLiveSyncPlugin from "../../../main.ts"; type MeasureResult = [times: number, spent: number]; type NamedMeasureResult = [name: string, result: MeasureResult]; const measures = new Map(); @@ -30,9 +30,10 @@ async function measure(name: string, proc: () => (void | Promise), times: return [name, measures.get(name) as MeasureResult]; } -// eslint-disable-next-line require-await +// eslint-disable-next-line require-await, @typescript-eslint/require-await async function formatPerfResults(items: NamedMeasureResult[]) { return `| Name | Runs | Each | Total |\n| --- | --- | --- | --- | \n` + items.map(e => `| ${e[0]} | ${e[1][0]} | ${e[1][0] != 0 ? formatNumber(e[1][1] / e[1][0]) : "-"} | ${formatNumber(e[1][0])} |`).join("\n"); + } export async function perf_trench(plugin: ObsidianLiveSyncPlugin) { clearResult("trench"); @@ -43,7 +44,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) { await p(); })); { - const testBinary = await plugin.vaultAccess.adapterReadBinary("testdata/10kb.png"); + const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/10kb.png"); const uint8Array = new Uint8Array(testBinary); result.push(await measure("trench-binary-10kb", async () => { const p = trench.evacuate(uint8Array); @@ -51,7 +52,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) { })); } { - const testBinary = await plugin.vaultAccess.adapterReadBinary("testdata/100kb.jpeg"); + const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/100kb.jpeg"); const uint8Array = new Uint8Array(testBinary); result.push(await measure("trench-binary-100kb", async () => { const p = trench.evacuate(uint8Array); @@ -59,7 +60,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) { })); } { - const testBinary = await plugin.vaultAccess.adapterReadBinary("testdata/1mb.png"); + const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/1mb.png"); const uint8Array = new Uint8Array(testBinary); result.push(await measure("trench-binary-1mb", async () => { const p = trench.evacuate(uint8Array); diff --git a/src/ui/DocumentHistoryModal.ts b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts similarity index 82% rename from src/ui/DocumentHistoryModal.ts rename to src/modules/features/DocumentHistory/DocumentHistoryModal.ts index 7a830aa..9d71e60 100644 --- a/src/ui/DocumentHistoryModal.ts +++ b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts @@ -1,12 +1,13 @@ -import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "../deps.ts"; -import { getPathFromTFile, isValidPath } from "../common/utils.ts"; -import { decodeBinary, escapeStringToHTML, readString } from "../lib/src/string_and_binary/convert.ts"; -import ObsidianLiveSyncPlugin from "../main.ts"; -import { type DocumentID, type FilePathWithPrefix, type LoadedEntry, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../lib/src/common/types.ts"; -import { Logger } from "../lib/src/common/logger.ts"; -import { isErrorOfMissingDoc } from "../lib/src/pouchdb/utils_couchdb.ts"; -import { getDocData, readContent } from "../lib/src/common/utils.ts"; -import { isPlainText, stripPrefix } from "../lib/src/string_and_binary/path.ts"; +import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "../../../deps.ts"; +import { getPathFromTFile, isValidPath } from "../../../common/utils.ts"; +import { decodeBinary, escapeStringToHTML, readString } from "../../../lib/src/string_and_binary/convert.ts"; +import ObsidianLiveSyncPlugin from "../../../main.ts"; +import { type DocumentID, type FilePathWithPrefix, type LoadedEntry, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../../lib/src/common/types.ts"; +import { Logger } from "../../../lib/src/common/logger.ts"; +import { isErrorOfMissingDoc } from "../../../lib/src/pouchdb/utils_couchdb.ts"; +import { fireAndForget, getDocData, readContent } from "../../../lib/src/common/utils.ts"; +import { isPlainText, stripPrefix } from "../../../lib/src/string_and_binary/path.ts"; +import { scheduleOnceIfDuplicated } from "octagonal-wheels/concurrency/lock"; function isImage(path: string) { const ext = path.split(".").splice(-1)[0].toLowerCase(); @@ -31,6 +32,7 @@ function readDocument(w: LoadedEntry) { try { return readString(new Uint8Array(decodeBinary(w.data))); } catch (ex) { + Logger(ex, LOG_LEVEL_VERBOSE); // NO OP. } return getDocData(w.data); @@ -59,7 +61,7 @@ export class DocumentHistoryModal extends Modal { this.id = id; this.initialRev = revision; if (!file && id) { - this.file = this.plugin.id2path(id); + this.file = this.plugin.$$id2path(id); } if (localStorage.getItem("ols-history-highlightdiff") == "1") { this.showDiff = true; @@ -68,7 +70,7 @@ export class DocumentHistoryModal extends Modal { async loadFile(initialRev?: string) { if (!this.id) { - this.id = await this.plugin.path2id(this.file); + this.id = await this.plugin.$$path2id(this.file); } const db = this.plugin.localDatabase; try { @@ -207,10 +209,10 @@ export class DocumentHistoryModal extends Modal { divView.createEl("input", { type: "range" }, (e) => { this.range = e; e.addEventListener("change", (e) => { - this.loadRevs(); + void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs()); }); e.addEventListener("input", (e) => { - this.loadRevs(); + void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs()); }); }); contentEl @@ -224,7 +226,7 @@ export class DocumentHistoryModal extends Modal { checkbox.addEventListener("input", (evt: any) => { this.showDiff = checkbox.checked; localStorage.setItem("ols-history-highlightdiff", this.showDiff == true ? "1" : ""); - this.loadRevs(); + void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs()); }); }) ); @@ -234,7 +236,7 @@ export class DocumentHistoryModal extends Modal { .addClass("op-info"); this.info = contentEl.createDiv(""); this.info.addClass("op-info"); - this.loadFile(this.initialRev); + fireAndForget(async () => await this.loadFile(this.initialRev)); const div = contentEl.createDiv({ text: "Loading old revisions..." }); this.contentView = div; div.addClass("op-scrollable"); @@ -242,9 +244,11 @@ export class DocumentHistoryModal extends Modal { const buttons = contentEl.createDiv(""); buttons.createEl("button", { text: "Copy to clipboard" }, (e) => { e.addClass("mod-cta"); - e.addEventListener("click", async () => { - await navigator.clipboard.writeText(this.currentText); - Logger(`Old content copied to clipboard`, LOG_LEVEL_NOTICE); + e.addEventListener("click", () => { + fireAndForget(async () => { + await navigator.clipboard.writeText(this.currentText); + Logger(`Old content copied to clipboard`, LOG_LEVEL_NOTICE); + }); }); }); const focusFile = async (path: string) => { @@ -258,21 +262,23 @@ export class DocumentHistoryModal extends Modal { } buttons.createEl("button", { text: "Back to this revision" }, (e) => { e.addClass("mod-cta"); - e.addEventListener("click", async () => { - // const pathToWrite = this.plugin.id2path(this.id, true); - const pathToWrite = stripPrefix(this.file); - if (!isValidPath(pathToWrite)) { - Logger("Path is not valid to write content.", LOG_LEVEL_INFO); - return; - } - if (!this.currentDoc) { - Logger("No active file loaded.", LOG_LEVEL_INFO); - return; - } - const d = readContent(this.currentDoc); - await this.plugin.vaultAccess.adapterWrite(pathToWrite, d); - await focusFile(pathToWrite); - this.close(); + e.addEventListener("click", () => { + fireAndForget(async () => { + // const pathToWrite = this.plugin.id2path(this.id, true); + const pathToWrite = stripPrefix(this.file); + if (!isValidPath(pathToWrite)) { + Logger("Path is not valid to write content.", LOG_LEVEL_INFO); + return; + } + if (!this.currentDoc) { + Logger("No active file loaded.", LOG_LEVEL_INFO); + return; + } + const d = readContent(this.currentDoc); + await this.plugin.storageAccess.writeHiddenFileAuto(pathToWrite, d); + await focusFile(pathToWrite); + this.close(); + }); }); }); } diff --git a/src/ui/GlobalHistory.svelte b/src/modules/features/GlobalHistory/GlobalHistory.svelte similarity index 91% rename from src/ui/GlobalHistory.svelte rename to src/modules/features/GlobalHistory/GlobalHistory.svelte index 0b6659a..30806d9 100644 --- a/src/ui/GlobalHistory.svelte +++ b/src/modules/features/GlobalHistory/GlobalHistory.svelte @@ -1,12 +1,13 @@ - -

TESTBENCH: Self-hosted LiveSync

- -

Function check

-
{functionCheckResult}
- -

Performance test

- - - -
- - diff --git a/terser.config.mjs b/terser.config.mjs index 7554bfe..79ce9ae 100644 --- a/terser.config.mjs +++ b/terser.config.mjs @@ -12,8 +12,8 @@ const terserOption = { } : {}, format: { - indent_level: 2, - beautify: true, + // indent_level: 2, + // beautify: true, comments: "some", ecma: 2018, preamble: banner, diff --git a/tsconfig.json b/tsconfig.json index 19442a2..e0da357 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,30 +8,16 @@ "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", - "types": [ - "svelte", - "node" - ], + "types": ["svelte", "node"], // "importsNotUsedAsValues": "error", "importHelpers": false, "alwaysStrict": true, "allowImportingTsExtensions": true, "noEmit": true, - "lib": [ - "es2018", - "DOM", - "ES5", - "ES6", - "ES7", - "es2019.array", - "ES2020.BigInt", - "ESNext.Intl" - ] + "lib": ["es2018", "DOM", "ES5", "ES6", "ES7", "es2019.array", "ES2021.WeakRef", "ES2020.BigInt", "ESNext.Intl"], + "strictBindCallApply": true, + "strictFunctionTypes": true }, - "include": [ - "**/*.ts" - ], - "exclude": [ - "pouchdb-browser-webpack" - ] -} \ No newline at end of file + "include": ["**/*.ts"], + "exclude": ["pouchdb-browser-webpack", "utils"] +} diff --git a/updates.md b/updates.md index b0bd4b5..0a6eca6 100644 --- a/updates.md +++ b/updates.md @@ -1,97 +1,123 @@ -### 0.23.0 -Incredibly new features! +## 0.24.0 RC Release Note -Now, we can use object storage (MinIO, S3, R2 or anything you like) for synchronising! Moreover, despite that, we can use all the features as if we were using CouchDB. -Note: As this is a pretty experimental feature, hence we have some limitations. -- This is built on the append-only architecture. It will not shrink used storage if we do not perform a rebuild. -- A bit fragile. However, our version x.yy.0 is always so. -- When the first synchronisation, the entire history to date is transferred. For this reason, it is preferable to do this under the WiFi network. -- Do not worry, from the second synchronisation, we always transfer only differences. +**Note:** This will be rewritten with the stable release. I confess, before you take the time, this is quite long. -I hope this feature empowers users to maintain independence and self-host their data, offering an alternative for those who prefer to manage their own storage solutions and avoid being stuck on the right side of a sudden change in business model. +Over the past three years since the inception of the plugin, various features have been implemented to address diverse user needs. This is so honourable and I am grateful for your years of support. +However, However, this process has resulted in a codebase that has become increasingly disorganised, with features becoming entangled. -Of course, I use Self-hosted MinIO for testing and recommend this. It is for the same reason as using CouchDB. -- open, controllable, auditable and indeed already audited by numerous eyes. +Consequently, this has led to a situation where bugs can go unnoticed or resolving one issue may inadvertently introduce another. -Let me write one more acknowledgement. +In 0.24.0, I reorganised the previously disjointed main codebase into clearly defined modules. Although I anticipated that the overall volume of code would not increase, I discovered that it has, in fact, expanded. While the complexity may still be considerable, the refactoring has enhanced clarity regarding the current structure of the code. (The next focus may involve a review of dependencies). -I have a lot of respect for that plugin, even though it is sometimes treated as if it is a competitor, remotely-save. I think it is a great architecture that embodies a different approach to my approach of recreating history. This time, with all due respect, I have used some of its code as a reference. -Hooray for open source, and generous licences, and the sharing of knowledge by experts. +Throughout this process, a significant number of bugs have been resolved. And it may be worth mentioning that these bugs may had given rise to other bugs. I kindly request that you verify whether your issues have been addressed. At least conflict resolution and related issues have improved significantly. -#### Version history -- 0.23.23: - - Refined: - - Setting dialogue very slightly refined. - - The hodgepodge inside the `Hatch` pane has been sorted into more explicit categorised panes. - - Now we have new panes for: - - `Selector` - - `Advanced` - - `Power users` - - `Patches (Edge case)` - - Applying the settings will now be more informative. - - The header bar will be shown for applying the settings which needs a database rebuild. - - Applying methods are now more clearly navigated. - - Definitely, drastic change. I hope this will be more user-friendly. However, if you notice any issues, please let me know. I hope that nothing missed. - - New features: - - Word-segmented chunk building on users language - - Chunks can now be built with word-segmented data, enhancing efficiency for markdown files which contains the multiple sentences in a single line. - - This feature is enabled by default through `Use Segmented-splitter`. - - (Default: Disabled, Please be relived, I have learnt). - - Fixed: - - Sending chunks on `Send chunk in bulk` are now buffered to avoid the out-of-memory error. - - `Send chunk in bulk` is back to default disabled. (Sorry, not applied to the migrated users; I did not think we should deepen the wound any further "automatically"). - - Merging conflicts of JSON files are now works fine even if it contains `null`. - - Development: - - Implemented the logic for automatically generating the stub of document for the setting dialogue. -- 0.23.22: - - Fixed: - - Case-insensitive file handling - - Full-lower-case files are no longer created during database checking. - - Bulk chunk transfer - - The default value will automatically adjust to an acceptable size when using IBM Cloudant. -- 0.23.21: - - New Features: - - Case-insensitive file handling - - Files can now be handled case-insensitively. - - This behaviour can be modified in the settings under `Handle files as Case-Sensitive` (Default: Prompt, Enabled for previous behaviour). - - Improved chunk revision fixing - - Revisions for chunks can now be fixed for faster chunk creation. - - This can be adjusted in the settings under `Compute revisions for chunks` (Default: Prompt, Enabled for previous behaviour). - - Bulk chunk transfer - - Chunks can now be transferred in bulk during uploads. - - This feature is enabled by default through `Send chunks in bulk`. - - Creation of missing chunks without - - Missing chunks can be created without storing notes, enhancing efficiency for first synchronisation or after prolonged periods without synchronisation. - - Improvements: - - File status scanning on the startup - - Quite significant performance improvements. - - No more missing scans of some files. - - Status in editor enhancements - - Significant performance improvements in the status display within the editor. - - Notifications for files that will not be synchronised will now be properly communicated. - - Encryption and Decryption - - These processes are now performed in background threads to ensure fast and stable transfers. - - Verify and repair all files - - Got faster through parallel checking. - - Migration on update - - Migration messages and wizards have become more helpful. - - Behavioural changes: - - Chunk size adjustments - - Large chunks will no longer be created for older, stable files, addressing storage consumption issues. - - Flag file automation - - Confirmation will be shown and we can cancel it. - - Fixed: - - Database File Scanning - - All files in the database will now be enumerated correctly. - - Miscellaneous - - Dependency updated. - - Now, tree shaking is left to terser, from esbuild. -- 0.23.20: - - Fixed: - - Customisation Sync now checks the difference while storing or applying the configuration. - - No longer storing the same configuration multiple times. - - Time difference in the dialogue has been fixed. - - Remote Storage Limit Notification dialogue has been fixed, now the chosen value is saved. - - Improved: - - The Enlarging button on the enlarging threshold dialogue now displays the new value. +It is also the first step towards a fully-fledged-fancy LiveSync, not just a plug-in from Obsidian. Of course, it will still be a plug-in as a first class and foremost, but this development marks a significant step towards the self-hosting concept. -Older notes is in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). \ No newline at end of file +This dev release is very close to the beta version that I had previously indicated would not be released. As a result, I have faced challenges in maintaining the main branch while working on this dev release. Regrettably, I have not been able to make any commits to the main branch in the last three weeks. Thus, the dev branch will remain reserved for major changes only. + +The Release Candidate will be available for a few days and will only be officially released once users, including myself, have confirmed that there are no issues. + +Finally, I would like to once again express my respect and gratitude to all of you once again. Thank you for your interest in the development version. Your contributions and dedication are greatly appreciated through testing. + +Thank you, and I hope your troubles will be resolved! + +--- + +## 0.24.0.dev-rc8 + +### Fixed + +- Now the deletion of hidden files are correctly synchronised. + +## 0.24.0.dev-rc7 + +### Fixed + +- Verifying files between the local database and storage is now working correctly. + +### New Features + +- We can verify and resolve also the hidden files now. + +## 0.24.0.dev-rc6 + +### Fixed + +- We can resolve the conflict of the JSON file correctly now. +- This would be the final Release Candidate. + +## 0.24.0.dev-rc5 + +### Improved + +- A note relating to device names has been added to Customisation Sync on the setting dialogue. +- Logs of Hidden File Sync and Customisation Sync have been prefixed with the respective feature names. + +### Fixed + +- Hidden file sync is now working correctly. +- Customisation Sync is now working correctly together with hidden file sync +- No longer database suffix is stored in the setting sharing markdown. + +## 0.24.0.dev-rc4 + +### Improved + +- The welcome message is now more simple to encourage the use of the Setup-URI. + - And the secondary message is also simpler to guide users to Minimal Setup. + - But Setup-URI will be recommended again, due to its importance. + - These dialogues contain a link to the documentation which can be clicked. +- The minimal setup is more minimal now. And, the setup is more user-friendly. + - Now the Configuration of the remote database is checked more robust, but we can ignore the warning and proceed with the setup. +- Before we are asked about each feature, we are asked if we want to use optional features in the first place. + - This is to prevent the user from being overwhelmed by the features. + - And made it clear that it is not recommended for new users. +- Many messages have been improved for better understanding. + - Ridiculous messages have been (carefully) refined. + - Dialogues are more informative and friendly. + - A lot of messages have been mostly rewritten, leveraging Markdown. + - Especially auto-closing dialogues are now explicitly labelled: `To stop the countdown, tap anywhere on the dialogue`. +- Now if the is plugin configured to ignore some events, we will get a chance to fix it, in addition to the warning. + - And why that has happened is also explained in the dialogue. + +### Fixed + +- While restarting the plug-in, the shown dialogues will be automatically closed to avoid unexpected behaviour. +- Replicated documents that the local device has configured to ignore are now correctly ignored. +- The chunks of the document on the local device during the first transfer will be created correctly. + - And why we should create them is now explained in the dialogue. +- If optional features have been enabled in the wizard, `Enable advanced features` will be toggled correctly. + +### Changed + +- Some default settings have been changed for easier new user experience. + - Preventing the meaningless migration of the settings. + +### Tidied + +- Commented-out codes have been gradually removed. + +## 0.24.0.dev-rc3 + +### Fixed + +- No longer Missing Translation Warning is shown in the console. +- Fixed the issue where some functions were not working properly (`_` started functions). + +## 0.24.0.dev-rc2 + +### Fixed + +- Some status icons is now shown correctly. + +## 0.24.0-rc1 + +### Fixed + +- A fair numbers of bugs have been fixed. + +### Tiding + +- The codebase has been reorganised into clearly defined modules. + +Older notes is in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). diff --git a/updates_old.md b/updates_old.md index 91e9ea0..8961b01 100644 --- a/updates_old.md +++ b/updates_old.md @@ -18,6 +18,81 @@ I have a lot of respect for that plugin, even though it is sometimes treated as Hooray for open source, and generous licences, and the sharing of knowledge by experts. #### Version history +- 0.23.23: + - Refined: + - Setting dialogue very slightly refined. + - The hodgepodge inside the `Hatch` pane has been sorted into more explicit categorised panes. + - Now we have new panes for: + - `Selector` + - `Advanced` + - `Power users` + - `Patches (Edge case)` + - Applying the settings will now be more informative. + - The header bar will be shown for applying the settings which needs a database rebuild. + - Applying methods are now more clearly navigated. + - Definitely, drastic change. I hope this will be more user-friendly. However, if you notice any issues, please let me know. I hope that nothing missed. + - New features: + - Word-segmented chunk building on users language + - Chunks can now be built with word-segmented data, enhancing efficiency for markdown files which contains the multiple sentences in a single line. + - This feature is enabled by default through `Use Segmented-splitter`. + - (Default: Disabled, Please be relived, I have learnt). + - Fixed: + - Sending chunks on `Send chunk in bulk` are now buffered to avoid the out-of-memory error. + - `Send chunk in bulk` is back to default disabled. (Sorry, not applied to the migrated users; I did not think we should deepen the wound any further "automatically"). + - Merging conflicts of JSON files are now works fine even if it contains `null`. + - Development: + - Implemented the logic for automatically generating the stub of document for the setting dialogue. +- 0.23.22: + - Fixed: + - Case-insensitive file handling + - Full-lower-case files are no longer created during database checking. + - Bulk chunk transfer + - The default value will automatically adjust to an acceptable size when using IBM Cloudant. +- 0.23.21: + - New Features: + - Case-insensitive file handling + - Files can now be handled case-insensitively. + - This behaviour can be modified in the settings under `Handle files as Case-Sensitive` (Default: Prompt, Enabled for previous behaviour). + - Improved chunk revision fixing + - Revisions for chunks can now be fixed for faster chunk creation. + - This can be adjusted in the settings under `Compute revisions for chunks` (Default: Prompt, Enabled for previous behaviour). + - Bulk chunk transfer + - Chunks can now be transferred in bulk during uploads. + - This feature is enabled by default through `Send chunks in bulk`. + - Creation of missing chunks without + - Missing chunks can be created without storing notes, enhancing efficiency for first synchronisation or after prolonged periods without synchronisation. + - Improvements: + - File status scanning on the startup + - Quite significant performance improvements. + - No more missing scans of some files. + - Status in editor enhancements + - Significant performance improvements in the status display within the editor. + - Notifications for files that will not be synchronised will now be properly communicated. + - Encryption and Decryption + - These processes are now performed in background threads to ensure fast and stable transfers. + - Verify and repair all files + - Got faster through parallel checking. + - Migration on update + - Migration messages and wizards have become more helpful. + - Behavioural changes: + - Chunk size adjustments + - Large chunks will no longer be created for older, stable files, addressing storage consumption issues. + - Flag file automation + - Confirmation will be shown and we can cancel it. + - Fixed: + - Database File Scanning + - All files in the database will now be enumerated correctly. + - Miscellaneous + - Dependency updated. + - Now, tree shaking is left to terser, from esbuild. +- 0.23.20: + - Fixed: + - Customisation Sync now checks the difference while storing or applying the configuration. + - No longer storing the same configuration multiple times. + - Time difference in the dialogue has been fixed. + - Remote Storage Limit Notification dialogue has been fixed, now the chosen value is saved. + - Improved: + - The Enlarging button on the enlarging threshold dialogue now displays the new value. - 0.23.19: - Not released. - 0.23.18: