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..f9ca43f 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": ["src/lib/src/API/*.ts"], "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..aa5ee51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.23.23", + "version": "0.24.0.rc1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.23.23", + "version": "0.24.0.rc1", "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..4e3b261 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.23.23", + "version": "0.24.0.rc1", "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,6 +69,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" } diff --git a/src/common/events.ts b/src/common/events.ts index 15baf5d..24930b7 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -3,9 +3,21 @@ 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_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/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 81% rename from src/features/CmdConfigSync.ts rename to src/features/ConfigSync/CmdConfigSync.ts index 13ae532..2c94712 100644 --- a/src/features/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -1,25 +1,27 @@ import { writable } from 'svelte/store'; -import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles, diff_match_patch } 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 { 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 '../../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 +186,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 +324,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 +343,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 +373,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 +388,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 +416,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 $everyOnDatabaseInitialized(showNotice: boolean) { + if (!this.$isThisModuleEnabled()) return true; + try { + Logger("Scanning customizations..."); + await this.scanAllConfigFiles(showNotice); + Logger("Scanning customizations : done"); + } catch (ex) { + Logger("Scanning customizations : failed"); + Logger(ex, LOG_LEVEL_VERBOSE); } + return true; } - async beforeReplicate(showNotice: boolean) { - if (this.settings.autoSweepPlugins && this.settings.usePluginSync) { + async $everyBeforeReplicate(showNotice: boolean) { + if (!this.$isThisModuleEnabled()) return true; + if (this.settings.autoSweepPlugins) { await this.scanAllConfigFiles(showNotice); + return true; } + return true; } - async onResume() { - if (this.plugin.suspended) { - return; + async $everyOnResumeProcess(): Promise { + if (!this.$isThisModuleEnabled()) return true; + if (this.$isMainSuspended()) { + return true; } - if (this.settings.autoSweepPlugins && this.settings.usePluginSync) { + 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 = []; @@ -699,7 +724,7 @@ export class ConfigSync extends LiveSyncCommands { 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); + const newId = await this.plugin.$$path2id(v2Path); // const buf = const data = createBlob([DUMMY_HEAD, DUMMY_END, ...getDocDataAsArray(f.data)]); @@ -729,7 +754,7 @@ export class ConfigSync extends LiveSyncCommands { } async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise { - if (!this.settings.usePluginSync) { + if (!this.$isThisModuleEnabled()) { this.pluginScanProcessor.clearQueue(); this.pluginList = []; pluginList.set(this.pluginList) @@ -843,9 +868,9 @@ export class ConfigSync extends LiveSyncCommands { const filename = data.files[0].filename; Logger(`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 { @@ -856,13 +881,15 @@ export class ConfigSync extends LiveSyncCommands { const path = `${baseDir}/${f.filename}` as FilePath; Logger(`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) { + Logger(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE); + Logger(ex, LOG_LEVEL_VERBOSE); oldData = new ArrayBuffer(0); } const content = base64ToArrayBuffer(f.data); @@ -870,12 +897,14 @@ export class ConfigSync extends LiveSyncCommands { Logger(`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) { + Logger(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE); + Logger(ex, LOG_LEVEL_VERBOSE); oldData = ""; } const content = getDocData(f.data); @@ -883,7 +912,7 @@ export class ConfigSync extends LiveSyncCommands { Logger(`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}..`); await this.storeCustomisationFileV2(path, this.plugin.deviceAndVaultName); @@ -916,12 +945,12 @@ export class ConfigSync extends LiveSyncCommands { 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`); @@ -951,7 +980,7 @@ export class ConfigSync extends LiveSyncCommands { Logger(`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) { @@ -983,79 +1012,76 @@ export class ConfigSync extends LiveSyncCommands { return true; } catch (ex) { Logger(`Failed to delete: ${data.documentPath}`, LOG_LEVEL_NOTICE); + Logger(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(); - }); - })); - - a.appendText(` to open the config sync dialog , or press elsewhere to dismiss this message.`); - }); + 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.`); }); + }); - 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); @@ -1071,6 +1097,7 @@ export class ConfigSync extends LiveSyncCommands { } } catch (ex) { Logger(`Configuration sync data: ${path} looks like manifest, but could not read the version`, LOG_LEVEL_INFO); + Logger(ex, LOG_LEVEL_VERBOSE); } } } catch (ex) { @@ -1096,12 +1123,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 { @@ -1258,7 +1285,7 @@ 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) { @@ -1291,10 +1318,16 @@ export class ConfigSync extends LiveSyncCommands { }) } + async $anyProcessOptionalFileEvent(path: FilePath): Promise { + return await this.watchVaultRawEventsAsync(path); + } + async watchVaultRawEventsAsync(path: FilePath) { - if (!this.settings.usePluginSync) return false; + // if (!this.$isMainReady) return true; + // if (!this.$isMainSuspended()) return true; + if (!this.$isThisModuleEnabled()) return true; if (!this.isTargetPath(path)) return false; - const stat = await this.vaultAccess.adapterStat(path); + const stat = await this.plugin.storageAccess.statHidden(path); // Make sure that target is a file. if (stat && stat.type != "file") return false; @@ -1305,12 +1338,14 @@ export class ConfigSync extends LiveSyncCommands { ).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 file could be handled by the other module. + return false; } 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,6 +1354,9 @@ 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; } @@ -1383,8 +1421,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))]; @@ -1402,7 +1439,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)) } }); } @@ -1450,7 +1487,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\`? +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. + +Of course, you can enable or disable this feature at any time. +` + 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) { + Logger("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; + await this.plugin.saveSettings(); + await this.scanAllConfigFiles(true); + } + } async getFiles( path: string, 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..0433089 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", "src/modules/coreObsidian/devUtil/tests.ts", "src/lib/src/API/**"] +}