Compare commits

...

8 Commits

Author SHA1 Message Date
vorotamoroz 4f987e7c2b ### Fixed
- Hidden file synchronisation now works!
- Now Hidden file synchronisation respects `.ignore` files.
- Replicator initialisation during rebuilding now works correctly.

### Refactored

- Some methods naming have been changed for better clarity, i.e., `_isTargetFileByLocalDB` is now `_isTargetAcceptedByLocalDB`.
2026-02-21 14:05:32 +09:00
vorotamoroz 556ce471f8 ## 0.25.43-patched-8 2026-02-20 14:28:28 +00:00
vorotamoroz 32b6717114 keep a note 2026-02-19 10:38:42 +00:00
vorotamoroz e0e72fae72 Fixed: saving device name 2026-02-19 10:37:19 +00:00
vorotamoroz 203dd17421 for 0.25.43-patched-7, please refer to the updates.md 2026-02-19 10:23:45 +00:00
vorotamoroz 1bde2b2ff1 Fixed an issue where the StorageEventManager
Build by Vite is now testing
2026-02-19 04:18:18 +00:00
vorotamoroz 2bf1c775ee ## 0.25.43-patched-6
### Fixed

- Unlocking the remote database after rebuilding has been fixed.

### Refactored
- Now `StorageEventManagerBase` is separated from `StorageEventManagerObsidian` following their concerns.
- Now `FileAccessBase` is separated from `FileAccessObsidian` following their concerns.
2026-02-18 12:13:05 +00:00
vorotamoroz 4658e3735d Fix Shim 2026-02-17 10:56:05 +00:00
80 changed files with 1840 additions and 2174 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.25.43-patched-5",
"version": "0.25.43-patched-9",
"minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",
+395 -2
View File
@@ -1,12 +1,12 @@
{
"name": "obsidian-livesync",
"version": "0.25.43-patched-5",
"version": "0.25.43-patched-9",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "0.25.43-patched-5",
"version": "0.25.43-patched-9",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
@@ -74,6 +74,7 @@
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"prettier": "3.5.2",
"rollup-plugin-copy": "^3.5.0",
"svelte": "5.41.1",
"svelte-check": "^4.3.3",
"svelte-preprocess": "^6.0.3",
@@ -4701,6 +4702,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/fs-extra": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz",
"integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/minimatch": "*",
"@types/node": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -4729,6 +4751,13 @@
"@types/lodash": "*"
}
},
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
@@ -6312,6 +6341,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/array.prototype.findlastindex": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
@@ -6939,6 +6978,13 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/colorette": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
"dev": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -7398,6 +7444,19 @@
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-type": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/dns-over-http-resolver": {
"version": "3.0.16",
"resolved": "https://registry.npmjs.org/dns-over-http-resolver/-/dns-over-http-resolver-3.0.16.tgz",
@@ -8761,6 +8820,38 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/fs-extra/node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -9059,6 +9150,61 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/globby": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz",
"integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/glob": "^7.1.1",
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.0.3",
"glob": "^7.1.3",
"ignore": "^5.1.1",
"merge2": "^1.2.3",
"slash": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/globby/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/globby/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -9375,6 +9521,18 @@
"node": ">=0.8.19"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -9709,6 +9867,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-plain-object": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
"integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
@@ -10270,6 +10438,16 @@
"json5": "lib/cli.js"
}
},
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -11511,6 +11689,16 @@
"node": ">=8"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -11554,6 +11742,16 @@
"node": "20 || >=22"
}
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -12646,6 +12844,23 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-copy": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz",
"integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/fs-extra": "^8.0.1",
"colorette": "^1.1.0",
"fs-extra": "^8.1.0",
"globby": "10.0.1",
"is-plain-object": "^3.0.0"
},
"engines": {
"node": ">=8.3"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -13031,6 +13246,16 @@
"node": ">=18"
}
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@@ -18646,6 +18871,25 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true
},
"@types/fs-extra": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz",
"integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
"dev": true,
"requires": {
"@types/minimatch": "*",
"@types/node": "*"
}
},
"@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -18671,6 +18915,12 @@
"@types/lodash": "*"
}
},
"@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
"@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
@@ -19807,6 +20057,12 @@
"math-intrinsics": "^1.1.0"
}
},
"array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true
},
"array.prototype.findlastindex": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
@@ -20217,6 +20473,12 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"colorette": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
"dev": true
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -20523,6 +20785,15 @@
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"requires": {
"path-type": "^4.0.0"
}
},
"dns-over-http-resolver": {
"version": "3.0.16",
"resolved": "https://registry.npmjs.org/dns-over-http-resolver/-/dns-over-http-resolver-3.0.16.tgz",
@@ -21483,6 +21754,31 @@
"signal-exit": "^4.0.1"
}
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"dependencies": {
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
}
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -21670,6 +21966,47 @@
"gopd": "^1.0.1"
}
},
"globby": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz",
"integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==",
"dev": true,
"requires": {
"@types/glob": "^7.1.1",
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.0.3",
"glob": "^7.1.3",
"ignore": "^5.1.1",
"merge2": "^1.2.3",
"slash": "^3.0.0"
},
"dependencies": {
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
}
}
},
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -21873,6 +22210,16 @@
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -22077,6 +22424,12 @@
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="
},
"is-plain-object": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
"integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
"dev": true
},
"is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
@@ -22480,6 +22833,15 @@
"minimist": "^1.2.0"
}
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6"
}
},
"jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -23364,6 +23726,12 @@
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -23394,6 +23762,12 @@
}
}
},
"path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
"pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -24185,6 +24559,19 @@
"fsevents": "~2.3.2"
}
},
"rollup-plugin-copy": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz",
"integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==",
"dev": true,
"requires": {
"@types/fs-extra": "^8.0.1",
"colorette": "^1.1.0",
"fs-extra": "^8.1.0",
"globby": "10.0.1",
"is-plain-object": "^3.0.0"
}
},
"run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -24421,6 +24808,12 @@
"totalist": "^3.0.0"
}
},
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
},
"smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.25.43-patched-5",
"version": "0.25.43-patched-9",
"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",
@@ -16,6 +16,8 @@
"dev": "node --env-file=.env esbuild.config.mjs",
"prebuild": "npm run bakei18n",
"build": "node esbuild.config.mjs production",
"buildVite": "npx dotenv-cli -e .env -- vite build --mode production",
"buildViteOriginal": "npx dotenv-cli -e .env -- vite build --mode original",
"buildDev": "node esbuild.config.mjs dev",
"lint": "eslint src",
"svelte-check": "svelte-check --tsconfig ./tsconfig.json",
@@ -106,6 +108,7 @@
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"prettier": "3.5.2",
"rollup-plugin-copy": "^3.5.0",
"svelte": "5.41.1",
"svelte-check": "^4.3.3",
"svelte-preprocess": "^6.0.3",
+11 -12
View File
@@ -34,8 +34,11 @@ import { TrysteroReplicator } from "@lib/replication/trystero/TrysteroReplicator
import { SETTING_KEY_P2P_DEVICE_NAME } from "@lib/common/types";
import { ServiceContext } from "@lib/services/base/ServiceBase";
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
import { Menu } from "@/lib/src/services/implements/browser/Menu";
import type { InjectableVaultServiceCompat } from "@/lib/src/services/implements/injectable/InjectableVaultService";
import { Menu } from "@lib/services/implements/browser/Menu";
import type { InjectableVaultServiceCompat } from "@lib/services/implements/injectable/InjectableVaultService";
import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
import type { InjectableAPIService } from "@/lib/src/services/implements/injectable/InjectableAPIService";
import type { BrowserAPIService } from "@/lib/src/services/implements/browser/BrowserAPIService";
function addToList(item: string, list: string) {
return unique(
@@ -80,13 +83,10 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
constructor() {
const browserServiceHub = new BrowserServiceHub<ServiceContext>();
this.services = browserServiceHub;
(this.services.vault as InjectableVaultServiceCompat<ServiceContext>).vaultName.setHandler(
(this.services.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
() => "p2p-livesync-web-peer"
);
this.services.setting.currentSettings.setHandler(() => {
return this.settings as any;
});
}
async init() {
// const { simpleStoreAPI } = await getWrappedSynchromesh();
@@ -102,11 +102,10 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
Logger(ex, LOG_LEVEL_VERBOSE);
}
}
const repStore = this.services.database.openSimpleStore<any>("p2p-livesync-web-peer");
const repStore = SimpleStoreIDBv2.open<any>("p2p-livesync-web-peer");
this._simpleStore = repStore;
let _settings = (await repStore.get("settings")) || ({ ...P2P_DEFAULT_SETTINGS } as P2PSyncSetting);
this.services.setting.settings = _settings as any;
this.plugin = {
saveSettings: async () => {
await repStore.set("settings", _settings);
@@ -148,9 +147,9 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
simpleStore(): SimpleStore<any> {
return this._simpleStore;
}
handleReplicatedDocuments(docs: EntryDoc[]): Promise<void> {
handleReplicatedDocuments(docs: EntryDoc[]): Promise<boolean> {
// No op. This is a client and does not need to process the docs
return Promise.resolve();
return Promise.resolve(true);
}
getPluginShim() {
+2 -2
View File
@@ -4,7 +4,7 @@ import { type mount, unmount } from "svelte";
export abstract class SvelteItemView extends ItemView {
abstract instantiateComponent(target: HTMLElement): ReturnType<typeof mount> | Promise<ReturnType<typeof mount>>;
component?: ReturnType<typeof mount>;
async onOpen() {
override async onOpen() {
await super.onOpen();
this.contentEl.empty();
await this._dismountComponent();
@@ -17,7 +17,7 @@ export abstract class SvelteItemView extends ItemView {
this.component = undefined;
}
}
async onClose() {
override async onClose() {
await super.onClose();
if (this.component) {
await unmount(this.component);
+1 -46
View File
@@ -31,7 +31,6 @@ import { sameChangePairs } from "./stores.ts";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { EVENT_PLUGIN_UNLOADED, eventHub } from "./events.ts";
import { promiseWithResolver, type PromiseWithResolvers } from "octagonal-wheels/promises";
import { AuthorizationHeaderGenerator } from "../lib/src/replication/httplib.ts";
import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts";
@@ -152,7 +151,7 @@ export class PeriodicProcessor {
() =>
fireAndForget(async () => {
await this.process();
if (this._plugin.services?.appLifecycle?.hasUnloaded()) {
if (this._plugin.services?.control?.hasUnloaded()) {
this.disable();
}
}),
@@ -459,47 +458,3 @@ export function onlyInNTimes(n: number, proc: (progress: number) => any) {
}
};
}
const waitingTasks = {} as Record<string, { task?: PromiseWithResolvers<any>; previous: number; leastNext: number }>;
export function rateLimitedSharedExecution<T>(key: string, interval: number, proc: () => Promise<T>): Promise<T> {
if (!(key in waitingTasks)) {
waitingTasks[key] = { task: undefined, previous: 0, leastNext: 0 };
}
if (waitingTasks[key].task) {
// Extend the previous execution time.
waitingTasks[key].leastNext = Date.now() + interval;
return waitingTasks[key].task.promise;
}
const previous = waitingTasks[key].previous;
const delay = previous == 0 ? 0 : Math.max(interval - (Date.now() - previous), 0);
const task = promiseWithResolver<T>();
void task.promise.finally(() => {
if (waitingTasks[key].task === task) {
waitingTasks[key].task = undefined;
waitingTasks[key].previous = Math.max(Date.now(), waitingTasks[key].leastNext);
}
});
waitingTasks[key] = {
task,
previous: Date.now(),
leastNext: Date.now() + interval,
};
void scheduleTask("thin-out-" + key, delay, async () => {
try {
task.resolve(await proc());
} catch (ex) {
task.reject(ex);
}
});
return task.promise;
}
export function updatePreviousExecutionTime(key: string, timeDelta: number = 0) {
if (!(key in waitingTasks)) {
waitingTasks[key] = { task: undefined, previous: 0, leastNext: 0 };
}
waitingTasks[key].leastNext = Math.max(Date.now() + timeDelta, waitingTasks[key].leastNext);
}
+1
View File
@@ -40,6 +40,7 @@ export type {
MarkdownFileInfo,
ListedFiles,
ValueComponent,
Stat,
} from "obsidian";
import { normalizePath as normalizePath_ } from "obsidian";
const normalizePath = normalizePath_ as <T extends string | FilePath>(from: T) => T;
+1 -1
View File
@@ -1802,7 +1802,7 @@ export class ConfigSync extends LiveSyncCommands {
}
return files;
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.fileProcessing.processOptionalFileEvent.addHandler(this._anyProcessOptionalFileEvent.bind(this));
services.conflict.getOptionalConflictCheckMethod.addHandler(this._anyGetOptionalConflictCheckMethod.bind(this));
services.replication.processVirtualDocument.addHandler(this._anyModuleParsedReplicationResultItem.bind(this));
+2 -2
View File
@@ -14,7 +14,7 @@ export class PluginDialogModal extends Modal {
this.plugin = plugin;
}
onOpen() {
override onOpen() {
const { contentEl } = this;
this.contentEl.style.overflow = "auto";
this.contentEl.style.display = "flex";
@@ -28,7 +28,7 @@ export class PluginDialogModal extends Modal {
}
}
onClose() {
override onClose() {
if (this.component) {
void unmount(this.component);
this.component = undefined;
@@ -50,7 +50,7 @@ export class JsonResolveModal extends Modal {
this.callback = undefined;
}
onOpen() {
override onOpen() {
const { contentEl } = this;
this.titleEl.setText(this.title);
contentEl.empty();
@@ -74,7 +74,7 @@ export class JsonResolveModal extends Modal {
return;
}
onClose() {
override onClose() {
const { contentEl } = this;
contentEl.empty();
// contentEl.empty();
@@ -1934,7 +1934,7 @@ ${messageFetch}${messageOverwrite}${messageMerge}
*/
// <-- Local Storage SubFunctions
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
override onBindFunction(core: LiveSyncCore, services: typeof core.services) {
// No longer needed on initialisation
// services.databaseEvents.handleOnDatabaseInitialisation(this._everyOnInitializeDatabase.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
+1 -4
View File
@@ -40,9 +40,6 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
getSettings(): P2PSyncSetting {
return this.plugin.settings;
}
get settings() {
return this.plugin.settings;
}
getDB() {
return this.plugin.localDatabase.localDatabase;
}
@@ -65,7 +62,7 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
// this.onBindFunction(plugin, plugin.services);
}
async handleReplicatedDocuments(docs: EntryDoc[]): Promise<void> {
async handleReplicatedDocuments(docs: EntryDoc[]): Promise<boolean> {
// console.log("Processing Replicated Docs", docs);
return await this.services.replication.parseSynchroniseResult(
docs as PouchDB.Core.ExistingDocument<EntryDoc>[]
@@ -35,11 +35,11 @@ function removeFromList(item: string, list: string) {
export class P2PReplicatorPaneView extends SvelteItemView {
plugin: ObsidianLiveSyncPlugin;
icon = "waypoints";
override icon = "waypoints";
title: string = "";
navigation = false;
override navigation = false;
getIcon(): string {
override getIcon(): string {
return "waypoints";
}
get replicator() {
+1 -1
Submodule src/lib updated: 56fc24e001...d038ee5149
+122 -62
View File
@@ -2,7 +2,6 @@ import { Plugin, type App, type PluginManifest } from "./deps";
import {
type EntryDoc,
type ObsidianLiveSyncSettings,
type DatabaseConnectingStatus,
type HasSettings,
LOG_LEVEL_INFO,
} from "./lib/src/common/types.ts";
@@ -12,7 +11,6 @@ import { type LiveSyncReplicatorEnv } from "./lib/src/replication/LiveSyncAbstra
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
import { reactiveSource, type ReactiveValue } from "octagonal-wheels/dataobject/reactive";
import { type LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicator.js";
import { type LiveSyncCouchDBReplicatorEnv } from "./lib/src/replication/couchdb/LiveSyncReplicator.js";
import type { CheckPointInfo } from "./lib/src/replication/journal/JournalSyncTypes.js";
@@ -24,7 +22,6 @@ import { ModuleCheckRemoteSize } from "./modules/essentialObsidian/ModuleCheckRe
import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver.ts";
import { ModuleInteractiveConflictResolver } from "./modules/features/ModuleInteractiveConflictResolver.ts";
import { ModuleLog } from "./modules/features/ModuleLog.ts";
import { ModuleObsidianSettings } from "./modules/features/ModuleObsidianSetting.ts";
import { ModuleRedFlag } from "./modules/coreFeatures/ModuleRedFlag.ts";
import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts";
@@ -46,7 +43,6 @@ import { ModuleReplicatorCouchDB } from "./modules/core/ModuleReplicatorCouchDB.
import { ModuleReplicatorMinIO } from "./modules/core/ModuleReplicatorMinIO.ts";
import { ModuleTargetFilter } from "./modules/core/ModuleTargetFilter.ts";
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess.ts";
import { ModuleRemoteGovernor } from "./modules/coreFeatures/ModuleRemoteGovernor.ts";
import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChecker.ts";
import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks.ts";
import { ModuleIntegratedTest } from "./modules/extras/ModuleIntegratedTest.ts";
@@ -60,12 +56,14 @@ import type { ServiceContext } from "./lib/src/services/base/ServiceBase.ts";
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder.ts";
import type { IFileHandler } from "@lib/interfaces/FileHandler.ts";
import { ServiceDatabaseFileAccess } from "@/serviceModules/DatabaseFileAccess.ts";
import { ServiceFileAccessObsidian } from "@/serviceModules/ServiceFileAccessObsidian.ts";
import { StorageEventManagerObsidian } from "@/modules/coreObsidian/storageLib/StorageEventManager.ts";
import { ObsidianFileAccess } from "@/modules/coreObsidian/storageLib/SerializedFileAccess.ts";
import { ServiceFileAccessObsidian } from "@/serviceModules/ServiceFileAccessImpl.ts";
import { StorageAccessManager } from "@lib/managers/StorageProcessingManager.ts";
import { __$checkInstanceBinding } from "./lib/src/dev/checks.ts";
import { ServiceFileHandler } from "./serviceModules/FileHandler.ts";
import { FileAccessObsidian } from "./serviceModules/FileAccessObsidian.ts";
import { StorageEventManagerObsidian } from "./managers/StorageEventManagerObsidian.ts";
import { onLayoutReadyFeatures } from "./serviceFeatures/onLayoutReady.ts";
import type { ServiceModules } from "./types.ts";
export default class ObsidianLiveSyncPlugin
extends Plugin
@@ -88,12 +86,27 @@ export default class ObsidianLiveSyncPlugin
return this._services;
}
private initialiseServices() {
this._services = new ObsidianServiceHub(this);
/**
* Service Modules
*/
protected _serviceModules: ServiceModules;
get serviceModules() {
return this._serviceModules;
}
/**
* addOns: Non-essential and graphically features
*/
addOns = [] as LiveSyncCommands[];
/**
* The modules of the plug-in. Modules are responsible for specific features or functionalities of the plug-in, such as file handling, conflict resolution, replication, etc.
*/
private modules = [
// Move to registerModules
] as (IObsidianModule | AbstractModule)[];
/**
* register an add-onn to the plug-in.
* Add-ons are features that are not essential to the core functionality of the plugin,
@@ -101,6 +114,7 @@ export default class ObsidianLiveSyncPlugin
*/
private _registerAddOn(addOn: LiveSyncCommands) {
this.addOns.push(addOn);
this.services.appLifecycle.onUnload.addHandler(() => Promise.resolve(addOn.onunload()).then(() => true));
}
private registerAddOns() {
@@ -122,13 +136,6 @@ export default class ObsidianLiveSyncPlugin
return undefined;
}
/**
* The modules of the plug-in. Modules are responsible for specific features or functionalities of the plug-in, such as file handling, conflict resolution, replication, etc.
*/
private modules = [
// Move to registerModules
] as (IObsidianModule | AbstractModule)[];
/**
* Get a module by its class. Throws an error if not found.
* Mostly used for getting SetupManager.
@@ -156,13 +163,11 @@ export default class ObsidianLiveSyncPlugin
this._registerModule(new ModuleReplicatorCouchDB(this));
this._registerModule(new ModuleReplicator(this));
this._registerModule(new ModuleConflictResolver(this));
this._registerModule(new ModuleRemoteGovernor(this));
this._registerModule(new ModuleTargetFilter(this));
this._registerModule(new ModulePeriodicProcess(this));
this._registerModule(new ModuleInitializerFile(this));
this._registerModule(new ModuleObsidianAPI(this, this));
this._registerModule(new ModuleObsidianEvents(this, this));
this._registerModule(new ModuleObsidianSettings(this));
this._registerModule(new ModuleResolvingMismatchedTweaks(this));
this._registerModule(new ModuleObsidianSettingsAsMarkdown(this));
this._registerModule(new ModuleObsidianSettingDialogue(this, this));
@@ -206,9 +211,24 @@ export default class ObsidianLiveSyncPlugin
return this.services.UI.confirm;
}
// This property will be changed from outside often, so will be set later.
settings!: ObsidianLiveSyncSettings;
/**
* @obsolete Use services.setting.currentSettings instead. The current settings of the plug-in.
*/
get settings() {
return this.services.setting.settings;
}
/**
* @obsolete Use services.setting.settings instead. Set the settings of the plug-in.
*/
set settings(value: ObsidianLiveSyncSettings) {
this.services.setting.settings = value;
}
/**
* @obsolete Use services.setting.currentSettings instead. Get the settings of the plug-in.
* @returns The current settings of the plug-in.
*/
getSettings(): ObsidianLiveSyncSettings {
return this.settings;
}
@@ -259,51 +279,79 @@ export default class ObsidianLiveSyncPlugin
/// Modules which were relied on services
/**
* Storage Accessor for handling file operations.
* @obsolete Use serviceModules.storageAccess instead.
*/
storageAccess: StorageAccess;
get storageAccess(): StorageAccess {
return this.serviceModules.storageAccess;
}
/**
* Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc.
* @obsolete Use serviceModules.databaseFileAccess instead.
*/
databaseFileAccess: DatabaseFileAccess;
get databaseFileAccess(): DatabaseFileAccess {
return this.serviceModules.databaseFileAccess;
}
/**
* File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc.
* @obsolete Use serviceModules.fileHandler instead.
*/
fileHandler: IFileHandler;
get fileHandler(): IFileHandler {
return this.serviceModules.fileHandler;
}
/**
* Rebuilder for handling database rebuilding operations.
* @obsolete Use serviceModules.rebuilder instead.
*/
rebuilder: Rebuilder;
get rebuilder(): Rebuilder {
return this.serviceModules.rebuilder;
}
requestCount = reactiveSource(0);
responseCount = reactiveSource(0);
totalQueued = reactiveSource(0);
batched = reactiveSource(0);
processing = reactiveSource(0);
databaseQueueCount = reactiveSource(0);
storageApplyingCount = reactiveSource(0);
replicationResultCount = reactiveSource(0);
conflictProcessQueueCount = reactiveSource(0);
pendingFileEventCount = reactiveSource(0);
processingFileEventCount = reactiveSource(0);
// requestCount = reactiveSource(0);
// responseCount = reactiveSource(0);
// totalQueued = reactiveSource(0);
// batched = reactiveSource(0);
// processing = reactiveSource(0);
// databaseQueueCount = reactiveSource(0);
// storageApplyingCount = reactiveSource(0);
// replicationResultCount = reactiveSource(0);
_totalProcessingCount?: ReactiveValue<number>;
// pendingFileEventCount = reactiveSource(0);
// processingFileEventCount = reactiveSource(0);
replicationStat = reactiveSource({
sent: 0,
arrived: 0,
maxPullSeq: 0,
maxPushSeq: 0,
lastSyncPullSeq: 0,
lastSyncPushSeq: 0,
syncStatus: "CLOSED" as DatabaseConnectingStatus,
});
// _totalProcessingCount?: ReactiveValue<number>;
// replicationStat = reactiveSource({
// sent: 0,
// arrived: 0,
// maxPullSeq: 0,
// maxPushSeq: 0,
// lastSyncPullSeq: 0,
// lastSyncPushSeq: 0,
// syncStatus: "CLOSED" as DatabaseConnectingStatus,
// });
private initialiseServices() {
this._services = new ObsidianServiceHub(this);
}
/**
* Initialise service modules.
*/
private initialiseServiceModules() {
const storageAccessManager = new StorageAccessManager();
// If we want to implement to the other platform, implement ObsidianXXXXXService.
const vaultAccess = new ObsidianFileAccess(this.app, this, storageAccessManager);
const storageEventManager = new StorageEventManagerObsidian(this, this, storageAccessManager);
const vaultAccess = new FileAccessObsidian(this.app, {
storageAccessManager: storageAccessManager,
vaultService: this.services.vault,
settingService: this.services.setting,
APIService: this.services.API,
});
const storageEventManager = new StorageEventManagerObsidian(this, this, {
fileProcessing: this.services.fileProcessing,
setting: this.services.setting,
vaultService: this.services.vault,
storageAccessManager: storageAccessManager,
APIService: this.services.API,
});
const storageAccess = new ServiceFileAccessObsidian({
API: this.services.API,
setting: this.services.setting,
@@ -347,6 +395,7 @@ export default class ObsidianLiveSyncPlugin
vault: this.services.vault,
fileHandler: fileHandler,
storageAccess: storageAccess,
control: this.services.control,
});
return {
rebuilder,
@@ -356,34 +405,45 @@ export default class ObsidianLiveSyncPlugin
};
}
/**
* @obsolete Use services.setting.saveSettingData instead. Save the settings to the disk. This is usually called after changing the settings in the code, to persist the changes.
*/
async saveSettings() {
await this.services.setting.saveSettingData();
}
/**
* Initialise ServiceFeatures.
* (Please refer `serviceFeatures` for more details)
*/
initialiseServiceFeatures() {
for (const feature of onLayoutReadyFeatures) {
const curriedFeature = () => feature(this);
this.services.appLifecycle.onLayoutReady.addHandler(curriedFeature);
}
}
constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
this.initialiseServices();
this.registerModules();
this.registerAddOns();
const instances = this.initialiseServiceModules();
this.rebuilder = instances.rebuilder;
this.fileHandler = instances.fileHandler;
this.databaseFileAccess = instances.databaseFileAccess;
this.storageAccess = instances.storageAccess;
this._serviceModules = this.initialiseServiceModules();
this.initialiseServiceFeatures();
this.bindModuleFunctions();
}
private async _startUp() {
await this.services.appLifecycle.onLoad();
const onReady = this.services.appLifecycle.onReady.bind(this.services.appLifecycle);
if (!(await this.services.control.onLoad())) return;
const onReady = this.services.control.onReady.bind(this.services.control);
this.app.workspace.onLayoutReady(onReady);
}
onload() {
override onload() {
void this._startUp();
}
async saveSettings() {
await this.services.setting.saveSettingData();
override onunload() {
return void this.services.control.onUnload();
}
onunload() {
return void this.services.appLifecycle.onAppUnload();
}
// <-- Plug-in's overrideable functions
}
// For now,
+210
View File
@@ -0,0 +1,210 @@
import type { FileEventItem } from "@/common/types";
import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync";
import type { FilePath, UXFileInfoStub, UXFolderInfo, UXInternalFileInfoStub } from "@lib/common/types";
import type { FileEvent } from "@lib/interfaces/StorageEventManager";
import { TFile, type TAbstractFile, TFolder } from "@/deps";
import { LOG_LEVEL_DEBUG } from "octagonal-wheels/common/logger";
import type ObsidianLiveSyncPlugin from "@/main";
import type { LiveSyncCore } from "@/main";
import {
StorageEventManagerBase,
type FileEventItemSentinel,
type StorageEventManagerBaseDependencies,
} from "@lib/managers/StorageEventManager";
import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
export class StorageEventManagerObsidian extends StorageEventManagerBase {
plugin: ObsidianLiveSyncPlugin;
core: LiveSyncCore;
// Necessary evil.
cmdHiddenFileSync: HiddenFileSync;
override isFile(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | TFile): boolean {
if (file instanceof TFile) {
return true;
}
if (super.isFile(file)) {
return true;
}
return !file.isFolder;
}
override isFolder(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | TFolder): boolean {
if (file instanceof TFolder) {
return true;
}
if (super.isFolder(file)) {
return true;
}
return !!file.isFolder;
}
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, dependencies: StorageEventManagerBaseDependencies) {
super(dependencies);
this.plugin = plugin;
this.core = core;
this.cmdHiddenFileSync = this.plugin.getAddOn(HiddenFileSync.name) as HiddenFileSync;
}
async beginWatch() {
await this.snapShotRestored;
const plugin = this.plugin;
this.watchVaultChange = this.watchVaultChange.bind(this);
this.watchVaultCreate = this.watchVaultCreate.bind(this);
this.watchVaultDelete = this.watchVaultDelete.bind(this);
this.watchVaultRename = this.watchVaultRename.bind(this);
this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this);
this.watchEditorChange = this.watchEditorChange.bind(this);
plugin.registerEvent(plugin.app.vault.on("modify", this.watchVaultChange));
plugin.registerEvent(plugin.app.vault.on("delete", this.watchVaultDelete));
plugin.registerEvent(plugin.app.vault.on("rename", this.watchVaultRename));
plugin.registerEvent(plugin.app.vault.on("create", this.watchVaultCreate));
//@ts-ignore : Internal API
plugin.registerEvent(plugin.app.vault.on("raw", this.watchVaultRawEvents));
plugin.registerEvent(plugin.app.workspace.on("editor-change", this.watchEditorChange));
}
watchEditorChange(editor: any, info: any) {
if (!("path" in info)) {
return;
}
if (!this.shouldBatchSave) {
return;
}
const file = info?.file as TFile;
if (!file) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// this._log(`Editor change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
if (!this.isWaiting(file.path as FilePath)) {
return;
}
const data = info?.data as string;
const fi: FileEvent = {
type: "CHANGED",
file: TFileToUXFileInfoStub(file),
cachedData: data,
};
void this.appendQueue([fi]);
}
watchVaultCreate(file: TAbstractFile, ctx?: any) {
if (file instanceof TFolder) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// this._log(`File create skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
const fileInfo = TFileToUXFileInfoStub(file);
void this.appendQueue([{ type: "CREATE", file: fileInfo }], ctx);
}
watchVaultChange(file: TAbstractFile, ctx?: any) {
if (file instanceof TFolder) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// this._log(`File change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
const fileInfo = TFileToUXFileInfoStub(file);
void this.appendQueue([{ type: "CHANGED", file: fileInfo }], ctx);
}
watchVaultDelete(file: TAbstractFile, ctx?: any) {
if (file instanceof TFolder) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// this._log(`File delete skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
const fileInfo = TFileToUXFileInfoStub(file, true);
void this.appendQueue([{ type: "DELETE", file: fileInfo }], ctx);
}
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
// vault Rename will not be raised for self-events (Self-hosted LiveSync will not handle 'rename').
if (file instanceof TFile) {
const fileInfo = TFileToUXFileInfoStub(file);
void this.appendQueue(
[
{
type: "DELETE",
file: {
path: oldFile as FilePath,
name: file.name,
stat: {
mtime: file.stat.mtime,
ctime: file.stat.ctime,
size: file.stat.size,
type: "file",
},
deleted: true,
},
skipBatchWait: true,
},
{ type: "CREATE", file: fileInfo, skipBatchWait: true },
],
ctx
);
}
}
// Watch raw events (Internal API)
watchVaultRawEvents(path: FilePath) {
if (this.storageAccess.isFileProcessing(path)) {
// this._log(`Raw file event skipped because the file is being processed: ${path}`, LOG_LEVEL_VERBOSE);
return;
}
// Only for internal files.
if (!this.settings) return;
// if (this.plugin.settings.useIgnoreFiles && this.plugin.ignoreFiles.some(e => path.endsWith(e.trim()))) {
if (this.settings.useIgnoreFiles) {
// If it is one of ignore files, refresh the cached one.
// (Calling$$isTargetFile will refresh the cache)
void this.vaultService.isTargetFile(path).then(() => this._watchVaultRawEvents(path));
} else {
void this._watchVaultRawEvents(path);
}
}
async _watchVaultRawEvents(path: FilePath) {
if (!this.settings.syncInternalFiles && !this.settings.usePluginSync) return;
if (!this.settings.watchInternalFileChanges) return;
if (!path.startsWith(this.plugin.app.vault.configDir)) return;
if (path.endsWith("/")) {
// Folder
return;
}
const isTargetFile = await this.cmdHiddenFileSync.isTargetFile(path);
if (!isTargetFile) return;
void this.appendQueue(
[
{
type: "INTERNAL",
file: InternalFileToUXFileInfoStub(path),
skipBatchWait: true, // Internal files should be processed immediately.
},
],
null
);
}
async _saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]) {
await this.core.kvDB.set("storage-event-manager-snapshot", snapshot);
this._log(`Storage operation snapshot saved: ${snapshot.length} items`, LOG_LEVEL_DEBUG);
}
async _loadSnapshot() {
const snapShot = await this.core.kvDB.get<(FileEventItem | FileEventItemSentinel)[]>(
"storage-event-manager-snapshot"
);
return snapShot;
}
updateStatus() {
const allFileEventItems = this.bufferedQueuedItems.filter((e): e is FileEventItem => "args" in e);
const allItems = allFileEventItems.filter((e) => !e.cancelled);
const totalItems = allItems.length + this.concurrentProcessing.waiting;
const processing = this.processingCount;
const batchedCount = this._waitingMap.size;
this.fileProcessing.batched.value = batchedCount;
this.fileProcessing.processing.value = processing;
this.fileProcessing.totalQueued.value = totalItems + batchedCount + processing;
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/co
import type { AnyEntry, FilePathWithPrefix } from "@lib/common/types";
import type { LiveSyncCore } from "@/main";
import { stripAllPrefixes } from "@lib/string_and_binary/path";
import { createInstanceLogFunction } from "@/lib/src/services/lib/logUtils";
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
export abstract class AbstractModule {
_log = createInstanceLogFunction(this.constructor.name, this.services.API);
+1 -1
View File
@@ -16,7 +16,7 @@ export abstract class AbstractObsidianModule extends AbstractModule {
constructor(
public plugin: ObsidianLiveSyncPlugin,
public core: LiveSyncCore
core: LiveSyncCore
) {
super(core);
}
+1 -1
View File
@@ -31,7 +31,7 @@ export class ModulePeriodicProcess extends AbstractModule {
return this.resumePeriodic();
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onUnload.addHandler(this._allOnUnload.bind(this));
services.setting.onBeforeRealiseSetting.addHandler(this._everyBeforeRealizeSetting.bind(this));
services.setting.onSettingRealised.addHandler(this._everyAfterRealizeSetting.bind(this));
+151 -171
View File
@@ -1,22 +1,58 @@
import { fireAndForget } from "octagonal-wheels/promises";
import { AbstractModule } from "../AbstractModule";
import { Logger, LOG_LEVEL_NOTICE, LOG_LEVEL_INFO, LEVEL_NOTICE, type LOG_LEVEL } from "octagonal-wheels/common/logger";
import { isLockAcquired, shareRunningResult, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
import { balanceChunkPurgedDBs } from "@/lib/src/pouchdb/chunks";
import { purgeUnreferencedChunks } from "@/lib/src/pouchdb/chunks";
import { Logger, LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
import { skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
import { balanceChunkPurgedDBs } from "@lib/pouchdb/chunks";
import { purgeUnreferencedChunks } from "@lib/pouchdb/chunks";
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
import { type EntryDoc, type RemoteType } from "../../lib/src/common/types";
import { rateLimitedSharedExecution, scheduleTask, updatePreviousExecutionTime } from "../../common/utils";
import { scheduleTask } from "../../common/utils";
import { EVENT_FILE_SAVED, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
import { $msg } from "../../lib/src/common/i18n";
import type { LiveSyncCore } from "../../main";
import { ReplicateResultProcessor } from "./ReplicateResultProcessor";
import { UnresolvedErrorManager } from "@/lib/src/services/base/UnresolvedErrorManager";
import { clearHandlers } from "@/lib/src/replication/SyncParamsHandler";
import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager";
import { clearHandlers } from "@lib/replication/SyncParamsHandler";
import type { NecessaryServices } from "@/serviceFeatures/types";
const KEY_REPLICATION_ON_EVENT = "replicationOnEvent";
const REPLICATION_ON_EVENT_FORECASTED_TIME = 5000;
function isOnlineAndCanReplicate(
errorManager: UnresolvedErrorManager,
host: NecessaryServices<"database", any>,
showMessage: boolean
): Promise<boolean> {
const errorMessage = "Network is offline";
const manager = host.services.database.managers.networkManager;
if (!manager.isOnline) {
errorManager.showError(errorMessage, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
return Promise.resolve(false);
}
errorManager.clearError(errorMessage);
return Promise.resolve(true);
}
async function canReplicateWithPBKDF2(
errorManager: UnresolvedErrorManager,
host: NecessaryServices<"replicator" | "setting", any>,
showMessage: boolean
): Promise<boolean> {
const currentSettings = host.services.setting.currentSettings();
// TODO: check using PBKDF2 salt?
const errorMessage = $msg("Replicator.Message.InitialiseFatalError");
const replicator = host.services.replicator.getActiveReplicator();
if (!replicator) {
errorManager.showError(errorMessage, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
return false;
}
errorManager.clearError(errorMessage);
const ensureMessage = "Failed to initialise the encryption key, preventing replication.";
const ensureResult = await replicator.ensurePBKDF2Salt(currentSettings, showMessage, true);
if (!ensureResult) {
errorManager.showError(ensureMessage, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
return false;
}
errorManager.clearError(ensureMessage);
return ensureResult; // is true.
}
export class ModuleReplicator extends AbstractModule {
_replicatorType?: RemoteType;
@@ -26,9 +62,6 @@ export class ModuleReplicator extends AbstractModule {
this.core.services.appLifecycle
);
showError(msg: string, max_log_level: LOG_LEVEL = LEVEL_NOTICE) {
this._unresolvedErrorManager.showError(msg, max_log_level);
}
clearErrors() {
this._unresolvedErrorManager.clearErrors();
}
@@ -40,10 +73,6 @@ export class ModuleReplicator extends AbstractModule {
}
});
eventHub.onEvent(EVENT_SETTING_SAVED, (setting) => {
// ReplicatorService responds to `settingService.onRealiseSetting`.
// if (this._replicatorType !== setting.remoteType) {
// void this.setReplicator();
// }
if (this.core.settings.suspendParseReplicationResult) {
this.processor.suspend();
} else {
@@ -65,41 +94,12 @@ export class ModuleReplicator extends AbstractModule {
return Promise.resolve(true);
}
async ensureReplicatorPBKDF2Salt(showMessage: boolean = false): Promise<boolean> {
// Checking salt
const replicator = this.services.replicator.getActiveReplicator();
if (!replicator) {
this.showError($msg("Replicator.Message.InitialiseFatalError"), LOG_LEVEL_NOTICE);
return false;
}
return await replicator.ensurePBKDF2Salt(this.settings, showMessage, true);
}
async _everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
// Checking salt
if (!this.core.managers.networkManager.isOnline) {
this.showError("Network is offline", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
return false;
}
// Showing message is false: that because be shown here. (And it is a fatal error, no way to hide it).
if (!(await this.ensureReplicatorPBKDF2Salt(false))) {
this.showError("Failed to initialise the encryption key, preventing replication.");
return false;
}
await this.processor.restoreFromSnapshotOnce();
this.clearErrors();
return true;
}
private async _replicate(showMessage: boolean = false): Promise<boolean | void> {
try {
updatePreviousExecutionTime(KEY_REPLICATION_ON_EVENT, REPLICATION_ON_EVENT_FORECASTED_TIME);
return await this.$$_replicate(showMessage);
} finally {
updatePreviousExecutionTime(KEY_REPLICATION_ON_EVENT);
}
}
/**
* obsolete method. No longer maintained and will be removed in the future.
* @deprecated v0.24.17
@@ -159,149 +159,129 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
});
}
async _canReplicate(showMessage: boolean = false): Promise<boolean> {
if (!this.services.appLifecycle.isReady()) {
Logger(`Not ready`);
private async onReplicationFailed(showMessage: boolean = false): Promise<boolean> {
const activeReplicator = this.services.replicator.getActiveReplicator();
if (!activeReplicator) {
Logger(`No active replicator found`, LOG_LEVEL_INFO);
return false;
}
if (isLockAcquired("cleanup")) {
Logger($msg("Replicator.Message.Cleaned"), LOG_LEVEL_NOTICE);
return false;
}
if (this.settings.versionUpFlash != "") {
Logger($msg("Replicator.Message.VersionUpFlash"), LOG_LEVEL_NOTICE);
return false;
}
if (!(await this.services.fileProcessing.commitPendingFileEvents())) {
this.showError($msg("Replicator.Message.Pending"), LOG_LEVEL_NOTICE);
return false;
}
if (!this.core.managers.networkManager.isOnline) {
this.showError("Network is offline", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
return false;
}
if (!(await this.services.replication.onBeforeReplicate(showMessage))) {
this.showError($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
return false;
}
this.clearErrors();
return true;
}
async $$_replicate(showMessage: boolean = false): Promise<boolean | void> {
const checkBeforeReplicate = await this.services.replication.isReplicationReady(showMessage);
if (!checkBeforeReplicate) return false;
//<-- Here could be an module.
const ret = await this.core.replicator.openReplication(this.settings, false, showMessage, false);
if (!ret) {
if (this.core.replicator.tweakSettingsMismatched && this.core.replicator.preferredTweakValue) {
await this.services.tweakValue.askResolvingMismatched(this.core.replicator.preferredTweakValue);
} else {
if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) {
if (this.core.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
await this.cleaned(showMessage);
} else {
const message = $msg("Replicator.Dialogue.Locked.Message");
const CHOICE_FETCH = $msg("Replicator.Dialogue.Locked.Action.Fetch");
const CHOICE_DISMISS = $msg("Replicator.Dialogue.Locked.Action.Dismiss");
const CHOICE_UNLOCK = $msg("Replicator.Dialogue.Locked.Action.Unlock");
const ret = await this.core.confirm.askSelectStringDialogue(
message,
[CHOICE_FETCH, CHOICE_UNLOCK, CHOICE_DISMISS],
{
title: $msg("Replicator.Dialogue.Locked.Title"),
defaultAction: CHOICE_DISMISS,
timeout: 60,
}
);
if (ret == CHOICE_FETCH) {
this._log($msg("Replicator.Dialogue.Locked.Message.Fetch"), LOG_LEVEL_NOTICE);
await this.core.rebuilder.scheduleFetch();
this.services.appLifecycle.scheduleRestart();
return;
} else if (ret == CHOICE_UNLOCK) {
await this.core.replicator.markRemoteResolved(this.settings);
this._log($msg("Replicator.Dialogue.Locked.Message.Unlocked"), LOG_LEVEL_NOTICE);
return;
if (activeReplicator.tweakSettingsMismatched && activeReplicator.preferredTweakValue) {
await this.services.tweakValue.askResolvingMismatched(activeReplicator.preferredTweakValue);
} else {
if (activeReplicator.remoteLockedAndDeviceNotAccepted) {
if (activeReplicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
await this.cleaned(showMessage);
} else {
const message = $msg("Replicator.Dialogue.Locked.Message");
const CHOICE_FETCH = $msg("Replicator.Dialogue.Locked.Action.Fetch");
const CHOICE_DISMISS = $msg("Replicator.Dialogue.Locked.Action.Dismiss");
const CHOICE_UNLOCK = $msg("Replicator.Dialogue.Locked.Action.Unlock");
const ret = await this.core.confirm.askSelectStringDialogue(
message,
[CHOICE_FETCH, CHOICE_UNLOCK, CHOICE_DISMISS],
{
title: $msg("Replicator.Dialogue.Locked.Title"),
defaultAction: CHOICE_DISMISS,
timeout: 60,
}
);
if (ret == CHOICE_FETCH) {
this._log($msg("Replicator.Dialogue.Locked.Message.Fetch"), LOG_LEVEL_NOTICE);
await this.core.rebuilder.scheduleFetch();
this.services.appLifecycle.scheduleRestart();
return false;
} else if (ret == CHOICE_UNLOCK) {
await activeReplicator.markRemoteResolved(this.settings);
this._log($msg("Replicator.Dialogue.Locked.Message.Unlocked"), LOG_LEVEL_NOTICE);
return false;
}
}
}
}
return ret;
// TODO: Check again and true/false return. This will be the result for performReplication.
return false;
}
private async _replicateByEvent(): Promise<boolean | void> {
const least = this.settings.syncMinimumInterval;
if (least > 0) {
return rateLimitedSharedExecution(KEY_REPLICATION_ON_EVENT, least, async () => {
return await this.services.replication.replicate();
});
}
return await shareRunningResult(`replication`, () => this.services.replication.replicate());
}
// private async _replicateByEvent(): Promise<boolean | void> {
// const least = this.settings.syncMinimumInterval;
// if (least > 0) {
// return rateLimitedSharedExecution(KEY_REPLICATION_ON_EVENT, least, async () => {
// return await this.services.replication.replicate();
// });
// }
// return await shareRunningResult(`replication`, () => this.services.replication.replicate());
// }
_parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): void {
_parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<boolean> {
this.processor.enqueueAll(docs);
}
_everyBeforeSuspendProcess(): Promise<boolean> {
this.core.replicator?.closeReplication();
return Promise.resolve(true);
}
private async _replicateAllToServer(
showingNotice: boolean = false,
sendChunksInBulkDisabled: boolean = false
): Promise<boolean> {
if (!this.services.appLifecycle.isReady()) return false;
if (!(await this.services.replication.onBeforeReplicate(showingNotice))) {
Logger($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
return false;
}
if (!sendChunksInBulkDisabled) {
if (this.core.replicator instanceof LiveSyncCouchDBReplicator) {
if (
(await this.core.confirm.askYesNoDialog("Do you want to send all chunks before replication?", {
defaultOption: "No",
timeout: 20,
})) == "yes"
) {
await this.core.replicator.sendChunks(this.core.settings, undefined, true, 0);
}
}
}
const ret = await this.core.replicator.replicateAllToServer(this.settings, showingNotice);
if (ret) return true;
const checkResult = await this.services.replication.checkConnectionFailure();
if (checkResult == "CHECKAGAIN") return await this.services.remote.replicateAllToRemote(showingNotice);
return !checkResult;
}
async _replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> {
if (!this.services.appLifecycle.isReady()) return false;
const ret = await this.core.replicator.replicateAllFromServer(this.settings, showingNotice);
if (ret) return true;
const checkResult = await this.services.replication.checkConnectionFailure();
if (checkResult == "CHECKAGAIN") return await this.services.remote.replicateAllFromRemote(showingNotice);
return !checkResult;
}
// _everyBeforeSuspendProcess(): Promise<boolean> {
// this.core.replicator?.closeReplication();
// return Promise.resolve(true);
// }
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
// private async _replicateAllToServer(
// showingNotice: boolean = false,
// sendChunksInBulkDisabled: boolean = false
// ): Promise<boolean> {
// if (!this.services.appLifecycle.isReady()) return false;
// if (!(await this.services.replication.onBeforeReplicate(showingNotice))) {
// Logger($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
// return false;
// }
// if (!sendChunksInBulkDisabled) {
// if (this.core.replicator instanceof LiveSyncCouchDBReplicator) {
// if (
// (await this.core.confirm.askYesNoDialog("Do you want to send all chunks before replication?", {
// defaultOption: "No",
// timeout: 20,
// })) == "yes"
// ) {
// await this.core.replicator.sendChunks(this.core.settings, undefined, true, 0);
// }
// }
// }
// const ret = await this.core.replicator.replicateAllToServer(this.settings, showingNotice);
// if (ret) return true;
// const checkResult = await this.services.replication.checkConnectionFailure();
// if (checkResult == "CHECKAGAIN") return await this.services.remote.replicateAllToRemote(showingNotice);
// return !checkResult;
// }
// async _replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> {
// if (!this.services.appLifecycle.isReady()) return false;
// const ret = await this.core.replicator.replicateAllFromServer(this.settings, showingNotice);
// if (ret) return true;
// const checkResult = await this.services.replication.checkConnectionFailure();
// if (checkResult == "CHECKAGAIN") return await this.services.remote.replicateAllFromRemote(showingNotice);
// return !checkResult;
// }
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.onReplicatorInitialised.addHandler(this._onReplicatorInitialised.bind(this));
services.databaseEvents.onDatabaseInitialised.addHandler(this._everyOnDatabaseInitialized.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.replication.parseSynchroniseResult.setHandler(this._parseReplicationResult.bind(this));
services.appLifecycle.onSuspending.addHandler(this._everyBeforeSuspendProcess.bind(this));
services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this));
services.replication.isReplicationReady.setHandler(this._canReplicate.bind(this));
services.replication.replicate.setHandler(this._replicate.bind(this));
services.replication.replicateByEvent.setHandler(this._replicateByEvent.bind(this));
services.remote.replicateAllToRemote.setHandler(this._replicateAllToServer.bind(this));
services.remote.replicateAllFromRemote.setHandler(this._replicateAllFromServer.bind(this));
services.replication.parseSynchroniseResult.addHandler(this._parseReplicationResult.bind(this));
// --> These handlers can be separated.
const isOnlineAndCanReplicateWithHost = isOnlineAndCanReplicate.bind(null, this._unresolvedErrorManager, {
services: {
database: services.database,
},
serviceModules: {},
});
const canReplicateWithPBKDF2WithHost = canReplicateWithPBKDF2.bind(null, this._unresolvedErrorManager, {
services: {
replicator: services.replicator,
setting: services.setting,
},
serviceModules: {},
});
services.replication.onBeforeReplicate.addHandler(isOnlineAndCanReplicateWithHost, 10);
services.replication.onBeforeReplicate.addHandler(canReplicateWithPBKDF2WithHost, 20);
// <-- End of handlers that can be separated.
services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this), 100);
services.replication.onReplicationFailed.addHandler(this.onReplicationFailed.bind(this));
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ export class ModuleReplicatorCouchDB extends AbstractModule {
return Promise.resolve(true);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.getNewReplicator.addHandler(this._anyNewReplicator.bind(this));
services.appLifecycle.onResumed.addHandler(this._everyAfterResumeProcess.bind(this));
}
+1 -1
View File
@@ -12,7 +12,7 @@ export class ModuleReplicatorMinIO extends AbstractModule {
}
return Promise.resolve(false);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.getNewReplicator.addHandler(this._anyNewReplicator.bind(this));
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export class ModuleReplicatorP2P extends AbstractModule {
return Promise.resolve(true);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.getNewReplicator.addHandler(this._anyNewReplicator.bind(this));
services.appLifecycle.onResumed.addHandler(this._everyAfterResumeProcess.bind(this));
}
+14 -9
View File
@@ -47,7 +47,7 @@ export class ModuleTargetFilter extends AbstractModule {
totalFileEventCount = 0;
private async _isTargetFileByFileNameDuplication(file: string | UXFileInfoStub) {
private async _isTargetAcceptedByFileNameDuplication(file: string | UXFileInfoStub) {
await this.fileCountMap.updateValue(this.totalFileEventCount);
const fileCountMap = this.fileCountMap.value;
if (!fileCountMap) {
@@ -107,7 +107,7 @@ export class ModuleTargetFilter extends AbstractModule {
}
}
private async _isTargetFileByLocalDB(file: string | UXFileInfoStub) {
private async _isTargetAcceptedByLocalDB(file: string | UXFileInfoStub) {
const filepath = getStoragePathFromUXFileInfo(file);
if (!this.localDatabase?.isTargetFile(filepath)) {
this._log("File is not target by local DB: " + filepath);
@@ -117,12 +117,12 @@ export class ModuleTargetFilter extends AbstractModule {
return await Promise.resolve(true);
}
private async _isTargetFileFinal(file: string | UXFileInfoStub) {
private async _isTargetAcceptedFinally(file: string | UXFileInfoStub) {
this._log("File is target finally: " + getStoragePathFromUXFileInfo(file), LOG_LEVEL_DEBUG);
return await Promise.resolve(true);
}
private async _isTargetIgnoredByIgnoreFiles(file: string | UXFileInfoStub): Promise<boolean> {
private async _isTargetAcceptedByIgnoreFiles(file: string | UXFileInfoStub): Promise<boolean> {
if (!this.settings.useIgnoreFiles) {
return true;
}
@@ -137,14 +137,19 @@ export class ModuleTargetFilter extends AbstractModule {
return true;
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
private async _isTargetIgnoredByIgnoreFiles(file: string | UXFileInfoStub) {
const result = await this._isTargetAcceptedByIgnoreFiles(file);
return !result;
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.vault.markFileListPossiblyChanged.setHandler(this._markFileListPossiblyChanged.bind(this));
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
services.vault.isIgnoredByIgnoreFile.setHandler(this._isTargetIgnoredByIgnoreFiles.bind(this));
services.vault.isTargetFile.addHandler(this._isTargetFileByFileNameDuplication.bind(this));
services.vault.isTargetFile.addHandler(this._isTargetIgnoredByIgnoreFiles.bind(this));
services.vault.isTargetFile.addHandler(this._isTargetFileByLocalDB.bind(this));
services.vault.isTargetFile.addHandler(this._isTargetFileFinal.bind(this));
services.vault.isTargetFile.addHandler(this._isTargetAcceptedByFileNameDuplication.bind(this), 10);
services.vault.isTargetFile.addHandler(this._isTargetAcceptedByIgnoreFiles.bind(this), 20);
services.vault.isTargetFile.addHandler(this._isTargetAcceptedByLocalDB.bind(this), 30);
services.vault.isTargetFile.addHandler(this._isTargetAcceptedFinally.bind(this), 100);
services.setting.onSettingRealised.addHandler(this.refreshSettings.bind(this));
}
}
+7 -6
View File
@@ -6,7 +6,7 @@ import {
type EntryLeaf,
type LoadedEntry,
type MetaEntry,
} from "@/lib/src/common/types";
} from "@lib/common/types";
import type { ModuleReplicator } from "./ModuleReplicator";
import { isChunk, isValidPath } from "@/common/utils";
import type { LiveSyncCore } from "@/main";
@@ -17,8 +17,8 @@ import {
LOG_LEVEL_VERBOSE,
Logger,
type LOG_LEVEL,
} from "@/lib/src/common/logger";
import { fireAndForget, isAnyNote, throttle } from "@/lib/src/common/utils";
} from "@lib/common/logger";
import { fireAndForget, isAnyNote, throttle } from "@lib/common/utils";
import { Semaphore } from "octagonal-wheels/concurrency/semaphore_v2";
import { serialized } from "octagonal-wheels/concurrency/lock";
import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
@@ -162,7 +162,8 @@ export class ReplicateResultProcessor {
* Report the current status.
*/
protected reportStatus() {
this.core.replicationResultCount.value = this._queuedChanges.length + this._processingChanges.length;
this.services.replication.replicationResultCount.value =
this._queuedChanges.length + this._processingChanges.length;
}
/**
@@ -381,7 +382,7 @@ export class ReplicateResultProcessor {
releaser();
}
}
}, this.replicator.core.databaseQueueCount);
}, this.services.replication.databaseQueueCount);
}
// Phase 2.1: process the document and apply to storage
// This function is serialized per document to avoid race-condition for the same document.
@@ -432,7 +433,7 @@ export class ReplicateResultProcessor {
protected applyToStorage(entry: MetaEntry) {
return this.withCounting(async () => {
await this.services.replication.processSynchroniseResult(entry);
}, this.replicator.core.storageApplyingCount);
}, this.services.replication.storageApplyingCount);
}
/**
@@ -71,10 +71,10 @@ export class ModuleConflictChecker extends AbstractModule {
delay: 0,
keepResultUntilDownstreamConnected: true,
pipeTo: this.conflictResolveQueue,
totalRemainingReactiveSource: this.core.conflictProcessQueueCount,
totalRemainingReactiveSource: this.services.conflict.conflictProcessQueueCount,
}
);
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.conflict.queueCheckForIfOpen.setHandler(this._queueConflictCheckIfOpen.bind(this));
services.conflict.queueCheckFor.setHandler(this._queueConflictCheck.bind(this));
services.conflict.ensureAllProcessed.setHandler(this._waitForAllConflictProcessed.bind(this));
@@ -229,7 +229,7 @@ export class ModuleConflictResolver extends AbstractModule {
this._log(`Done!`, LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes");
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.conflict.resolveByDeletingRevision.setHandler(this._resolveConflictByDeletingRev.bind(this));
services.conflict.resolve.setHandler(this._resolveConflict.bind(this));
services.conflict.resolveByNewest.setHandler(this._anyResolveConflictByNewest.bind(this));
+3 -3
View File
@@ -12,8 +12,8 @@ import type { LiveSyncCore } from "../../main.ts";
import FetchEverything from "../features/SetupWizard/dialogs/FetchEverything.svelte";
import RebuildEverything from "../features/SetupWizard/dialogs/RebuildEverything.svelte";
import { extractObject } from "octagonal-wheels/object";
import { SvelteDialogManagerBase } from "@/lib/src/UI/svelteDialog.ts";
import type { ServiceContext } from "@/lib/src/services/base/ServiceBase.ts";
import { SvelteDialogManagerBase } from "@lib/UI/svelteDialog.ts";
import type { ServiceContext } from "@lib/services/base/ServiceBase.ts";
export class ModuleRedFlag extends AbstractModule {
async isFlagFileExist(path: string) {
@@ -324,7 +324,7 @@ export class ModuleRedFlag extends AbstractModule {
}
return true;
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
super.onBindFunction(core, services);
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
}
@@ -1,22 +0,0 @@
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
import type { LiveSyncCore } from "../../main.ts";
import { AbstractModule } from "../AbstractModule.ts";
export class ModuleRemoteGovernor extends AbstractModule {
private async _markRemoteLocked(lockByClean: boolean = false): Promise<void> {
return await this.core.replicator.markRemoteLocked(this.settings, true, lockByClean);
}
private async _markRemoteUnlocked(): Promise<void> {
return await this.core.replicator.markRemoteLocked(this.settings, false, false);
}
private async _markRemoteResolved(): Promise<void> {
return await this.core.replicator.markRemoteResolved(this.settings);
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.remote.markLocked.setHandler(this._markRemoteLocked.bind(this));
services.remote.markUnlocked.setHandler(this._markRemoteUnlocked.bind(this));
services.remote.markResolved.setHandler(this._markRemoteResolved.bind(this));
}
}
@@ -284,7 +284,7 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
return { result: false, requireFetch: false };
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.tweakValue.fetchRemotePreferred.setHandler(this._fetchRemotePreferredTweakValues.bind(this));
services.tweakValue.checkAndAskResolvingMismatched.setHandler(
this._checkAndAskResolvingMismatchedTweaks.bind(this)
+8 -8
View File
@@ -13,7 +13,7 @@ class AutoClosableModal extends Modal {
this._closeByUnload = this._closeByUnload.bind(this);
eventHub.once(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
}
onClose() {
override onClose() {
eventHub.off(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
}
}
@@ -43,7 +43,7 @@ export class InputStringDialog extends AutoClosableModal {
this.isPassword = isPassword;
}
onOpen() {
override onOpen() {
const { contentEl } = this;
this.titleEl.setText(this.title);
const formEl = contentEl.createDiv();
@@ -75,7 +75,7 @@ export class InputStringDialog extends AutoClosableModal {
);
}
onClose() {
override onClose() {
super.onClose();
const { contentEl } = this;
contentEl.empty();
@@ -87,7 +87,7 @@ export class InputStringDialog extends AutoClosableModal {
}
}
export class PopoverSelectString extends FuzzySuggestModal<string> {
app: App;
_app: App;
callback: ((e: string) => void) | undefined = () => {};
getItemsFun: () => string[] = () => {
return ["yes", "no"];
@@ -101,7 +101,7 @@ export class PopoverSelectString extends FuzzySuggestModal<string> {
callback: (e: string) => void
) {
super(app);
this.app = app;
this._app = app;
this.setPlaceholder((placeholder ?? "y/n) ") + note);
if (getItemsFun) this.getItemsFun = getItemsFun;
this.callback = callback;
@@ -120,7 +120,7 @@ export class PopoverSelectString extends FuzzySuggestModal<string> {
this.callback?.(item);
this.callback = undefined;
}
onClose(): void {
override onClose(): void {
setTimeout(() => {
if (this.callback) {
this.callback("");
@@ -184,7 +184,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
}
}
onOpen() {
override onOpen() {
const { contentEl } = this;
this.titleEl.setText(this.title);
const div = contentEl.createDiv();
@@ -242,7 +242,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
}
}
onClose() {
override onClose() {
super.onClose();
const { contentEl } = this;
contentEl.empty();
@@ -1,264 +0,0 @@
import { type App, TFile, type DataWriteOptions, TFolder, TAbstractFile } from "../../../deps.ts";
import { Logger } from "../../../lib/src/common/logger.ts";
import { isPlainText } from "../../../lib/src/string_and_binary/path.ts";
import type { FilePath, UXFileInfoStub } from "../../../lib/src/common/types.ts";
import { createBinaryBlob, isDocContentSame } from "../../../lib/src/common/utils.ts";
import type { InternalFileInfo } from "../../../common/types.ts";
import { markChangesAreSame } from "../../../common/utils.ts";
import type { IStorageAccessManager } from "@lib/interfaces/StorageAccess.ts";
import type { LiveSyncCore } from "@/main.ts";
function toArrayBuffer(arr: Uint8Array<ArrayBuffer> | ArrayBuffer | DataView<ArrayBuffer>): ArrayBuffer {
if (arr instanceof Uint8Array) {
return arr.buffer;
}
if (arr instanceof DataView) {
return arr.buffer;
}
return arr;
}
// TODO: add abstraction for the file access (as wrapping TFile or something similar)
export abstract class FileAccessBase<TNativeFile> {
storageAccessManager: IStorageAccessManager;
constructor(storageAccessManager: IStorageAccessManager) {
this.storageAccessManager = storageAccessManager;
}
abstract getPath(file: TNativeFile | string): FilePath;
}
export class ObsidianFileAccess extends FileAccessBase<TFile> {
app: App;
plugin: LiveSyncCore;
getPath(file: string | TFile): FilePath {
return (typeof file === "string" ? file : file.path) as FilePath;
}
constructor(app: App, plugin: LiveSyncCore, storageAccessManager: IStorageAccessManager) {
super(storageAccessManager);
this.app = app;
this.plugin = plugin;
}
async tryAdapterStat(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
return await this.storageAccessManager.processReadFile(path as FilePath, async () => {
if (!(await this.app.vault.adapter.exists(path))) return null;
return this.app.vault.adapter.stat(path);
});
}
async adapterStat(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
return await this.storageAccessManager.processReadFile(path as FilePath, () =>
this.app.vault.adapter.stat(path)
);
}
async adapterExists(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
return await this.storageAccessManager.processReadFile(path as FilePath, () =>
this.app.vault.adapter.exists(path)
);
}
async adapterRemove(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
return await this.storageAccessManager.processWriteFile(path as FilePath, () =>
this.app.vault.adapter.remove(path)
);
}
async adapterRead(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
return await this.storageAccessManager.processReadFile(path as FilePath, () =>
this.app.vault.adapter.read(path)
);
}
async adapterReadBinary(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
return await this.storageAccessManager.processReadFile(path as FilePath, () =>
this.app.vault.adapter.readBinary(path)
);
}
async adapterReadAuto(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
if (isPlainText(path)) {
return await this.storageAccessManager.processReadFile(path as FilePath, () =>
this.app.vault.adapter.read(path)
);
}
return await this.storageAccessManager.processReadFile(path as FilePath, () =>
this.app.vault.adapter.readBinary(path)
);
}
async adapterWrite(
file: TFile | string,
data: string | ArrayBuffer | Uint8Array<ArrayBuffer>,
options?: DataWriteOptions
) {
const path = file instanceof TFile ? file.path : file;
if (typeof data === "string") {
return await this.storageAccessManager.processWriteFile(path as FilePath, () =>
this.app.vault.adapter.write(path, data, options)
);
} else {
return await this.storageAccessManager.processWriteFile(path as FilePath, () =>
this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options)
);
}
}
adapterList(basePath: string): Promise<{ files: string[]; folders: string[] }> {
return Promise.resolve(this.app.vault.adapter.list(basePath));
}
async vaultCacheRead(file: TFile) {
return await this.storageAccessManager.processReadFile(file.path as FilePath, () =>
this.app.vault.cachedRead(file)
);
}
async vaultRead(file: TFile) {
return await this.storageAccessManager.processReadFile(file.path as FilePath, () => this.app.vault.read(file));
}
async vaultReadBinary(file: TFile) {
return await this.storageAccessManager.processReadFile(file.path as FilePath, () =>
this.app.vault.readBinary(file)
);
}
async vaultReadAuto(file: TFile) {
const path = file.path;
if (isPlainText(path)) {
return await this.storageAccessManager.processReadFile(path as FilePath, () => this.app.vault.read(file));
}
return await this.storageAccessManager.processReadFile(path as FilePath, () => this.app.vault.readBinary(file));
}
async vaultModify(file: TFile, data: string | ArrayBuffer | Uint8Array<ArrayBuffer>, options?: DataWriteOptions) {
if (typeof data === "string") {
return await this.storageAccessManager.processWriteFile(file.path as FilePath, async () => {
const oldData = await this.app.vault.read(file);
if (data === oldData) {
if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime);
return true;
}
await this.app.vault.modify(file, data, options);
return true;
});
} else {
return await this.storageAccessManager.processWriteFile(file.path as FilePath, async () => {
const oldData = await this.app.vault.readBinary(file);
if (await isDocContentSame(createBinaryBlob(oldData), createBinaryBlob(data))) {
if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime);
return true;
}
await this.app.vault.modifyBinary(file, toArrayBuffer(data), options);
return true;
});
}
}
async vaultCreate(
path: string,
data: string | ArrayBuffer | Uint8Array<ArrayBuffer>,
options?: DataWriteOptions
): Promise<TFile> {
if (typeof data === "string") {
return await this.storageAccessManager.processWriteFile(path as FilePath, () =>
this.app.vault.create(path, data, options)
);
} else {
return await this.storageAccessManager.processWriteFile(path as FilePath, () =>
this.app.vault.createBinary(path, toArrayBuffer(data), options)
);
}
}
trigger(name: string, ...data: any[]) {
return this.app.vault.trigger(name, ...data);
}
async reconcileInternalFile(path: string) {
await (this.app.vault.adapter as any)?.reconcileInternalFile(path);
}
async adapterAppend(normalizedPath: string, data: string, options?: DataWriteOptions) {
return await this.app.vault.adapter.append(normalizedPath, data, options);
}
async delete(file: TFile | TFolder, force = false) {
return await this.storageAccessManager.processWriteFile(file.path as FilePath, () =>
this.app.vault.delete(file, force)
);
}
async trash(file: TFile | TFolder, force = false) {
return await this.storageAccessManager.processWriteFile(file.path as FilePath, () =>
this.app.vault.trash(file, force)
);
}
isStorageInsensitive(): boolean {
return this.plugin.services.vault.isStorageInsensitive();
}
getAbstractFileByPathInsensitive(path: FilePath | string): TAbstractFile | null {
//@ts-ignore
return this.app.vault.getAbstractFileByPathInsensitive(path);
}
getAbstractFileByPath(path: FilePath | string): TAbstractFile | null {
if (!this.plugin.settings.handleFilenameCaseSensitive || this.isStorageInsensitive()) {
return this.getAbstractFileByPathInsensitive(path);
}
return this.app.vault.getAbstractFileByPath(path);
}
getFiles() {
return this.app.vault.getFiles();
}
async ensureDirectory(fullPath: string) {
const pathElements = fullPath.split("/");
pathElements.pop();
let c = "";
for (const v of pathElements) {
c += v;
try {
await this.app.vault.adapter.mkdir(c);
} catch (ex: any) {
if (ex?.message == "Folder already exists.") {
// Skip if already exists.
} else {
Logger("Folder Create Error");
Logger(ex);
}
}
c += "/";
}
}
touchedFiles: string[] = [];
_statInternal(file: FilePath) {
return this.app.vault.adapter.stat(file);
}
async touch(file: TFile | FilePath) {
const path = file instanceof TFile ? (file.path as FilePath) : file;
const statOrg = file instanceof TFile ? file.stat : await this._statInternal(path);
const stat = statOrg || { mtime: 0, size: 0 };
const key = `${path}-${stat.mtime}-${stat.size}`;
this.touchedFiles.unshift(key);
this.touchedFiles = this.touchedFiles.slice(0, 100);
}
recentlyTouched(file: TFile | InternalFileInfo | UXFileInfoStub) {
const key =
"stat" in file
? `${file.path}-${file.stat.mtime}-${file.stat.size}`
: `${file.path}-${file.mtime}-${file.size}`;
if (this.touchedFiles.indexOf(key) == -1) return false;
return true;
}
clearTouched() {
this.touchedFiles = [];
}
}
@@ -1,631 +0,0 @@
import { TAbstractFile, TFile, TFolder } from "../../../deps.ts";
import { Logger } from "../../../lib/src/common/logger.ts";
import { shouldBeIgnored } from "../../../lib/src/string_and_binary/path.ts";
import {
DEFAULT_SETTINGS,
LOG_LEVEL_DEBUG,
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
type FileEventType,
type FilePath,
type UXFileInfoStub,
} from "../../../lib/src/common/types.ts";
import { delay, fireAndForget, throttle } from "../../../lib/src/common/utils.ts";
import { type FileEventItem } from "../../../common/types.ts";
import { serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
import { isWaitingForTimeout } from "octagonal-wheels/concurrency/task";
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
import type { LiveSyncCore } from "../../../main.ts";
import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "./utilObsidian.ts";
import ObsidianLiveSyncPlugin from "../../../main.ts";
import type { IStorageAccessManager } from "@lib/interfaces/StorageAccess.ts";
import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts";
import { promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises";
import { StorageEventManager, type FileEvent } from "@lib/interfaces/StorageEventManager.ts";
type WaitInfo = {
since: number;
type: FileEventType;
canProceed: PromiseWithResolvers<boolean>;
timerHandler: ReturnType<typeof setTimeout>;
event: FileEventItem;
};
const TYPE_SENTINEL_FLUSH = "SENTINEL_FLUSH";
type FileEventItemSentinelFlush = {
type: typeof TYPE_SENTINEL_FLUSH;
};
type FileEventItemSentinel = FileEventItemSentinelFlush;
export class StorageEventManagerObsidian extends StorageEventManager {
plugin: ObsidianLiveSyncPlugin;
core: LiveSyncCore;
storageAccess: IStorageAccessManager;
get services() {
return this.core.services;
}
get shouldBatchSave() {
return this.core.settings?.batchSave && this.core.settings?.liveSync != true;
}
get batchSaveMinimumDelay(): number {
return this.core.settings?.batchSaveMinimumDelay ?? DEFAULT_SETTINGS.batchSaveMinimumDelay;
}
get batchSaveMaximumDelay(): number {
return this.core.settings?.batchSaveMaximumDelay ?? DEFAULT_SETTINGS.batchSaveMaximumDelay;
}
// Necessary evil.
cmdHiddenFileSync: HiddenFileSync;
/**
* Snapshot restoration promise.
* Snapshot will be restored before starting to watch vault changes.
* In designed time, this has been called from Initialisation process, which has been implemented on `ModuleInitializerFile.ts`.
*/
snapShotRestored: Promise<void> | null = null;
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, storageAccessManager: IStorageAccessManager) {
super();
this.storageAccess = storageAccessManager;
this.plugin = plugin;
this.core = core;
this.cmdHiddenFileSync = this.plugin.getAddOn(HiddenFileSync.name) as HiddenFileSync;
}
/**
* Restore the previous snapshot if exists.
* @returns
*/
restoreState(): Promise<void> {
this.snapShotRestored = this._restoreFromSnapshot();
return this.snapShotRestored;
}
async beginWatch() {
await this.snapShotRestored;
const plugin = this.plugin;
this.watchVaultChange = this.watchVaultChange.bind(this);
this.watchVaultCreate = this.watchVaultCreate.bind(this);
this.watchVaultDelete = this.watchVaultDelete.bind(this);
this.watchVaultRename = this.watchVaultRename.bind(this);
this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this);
this.watchEditorChange = this.watchEditorChange.bind(this);
plugin.registerEvent(plugin.app.vault.on("modify", this.watchVaultChange));
plugin.registerEvent(plugin.app.vault.on("delete", this.watchVaultDelete));
plugin.registerEvent(plugin.app.vault.on("rename", this.watchVaultRename));
plugin.registerEvent(plugin.app.vault.on("create", this.watchVaultCreate));
//@ts-ignore : Internal API
plugin.registerEvent(plugin.app.vault.on("raw", this.watchVaultRawEvents));
plugin.registerEvent(plugin.app.workspace.on("editor-change", this.watchEditorChange));
}
watchEditorChange(editor: any, info: any) {
if (!("path" in info)) {
return;
}
if (!this.shouldBatchSave) {
return;
}
const file = info?.file as TFile;
if (!file) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// Logger(`Editor change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
if (!this.isWaiting(file.path as FilePath)) {
return;
}
const data = info?.data as string;
const fi: FileEvent = {
type: "CHANGED",
file: TFileToUXFileInfoStub(file),
cachedData: data,
};
void this.appendQueue([fi]);
}
watchVaultCreate(file: TAbstractFile, ctx?: any) {
if (file instanceof TFolder) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// Logger(`File create skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
const fileInfo = TFileToUXFileInfoStub(file);
void this.appendQueue([{ type: "CREATE", file: fileInfo }], ctx);
}
watchVaultChange(file: TAbstractFile, ctx?: any) {
if (file instanceof TFolder) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// Logger(`File change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
const fileInfo = TFileToUXFileInfoStub(file);
void this.appendQueue([{ type: "CHANGED", file: fileInfo }], ctx);
}
watchVaultDelete(file: TAbstractFile, ctx?: any) {
if (file instanceof TFolder) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// Logger(`File delete skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
const fileInfo = TFileToUXFileInfoStub(file, true);
void this.appendQueue([{ type: "DELETE", file: fileInfo }], ctx);
}
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
// vault Rename will not be raised for self-events (Self-hosted LiveSync will not handle 'rename').
if (file instanceof TFile) {
const fileInfo = TFileToUXFileInfoStub(file);
void this.appendQueue(
[
{
type: "DELETE",
file: {
path: oldFile as FilePath,
name: file.name,
stat: {
mtime: file.stat.mtime,
ctime: file.stat.ctime,
size: file.stat.size,
type: "file",
},
deleted: true,
},
skipBatchWait: true,
},
{ type: "CREATE", file: fileInfo, skipBatchWait: true },
],
ctx
);
}
}
// Watch raw events (Internal API)
watchVaultRawEvents(path: FilePath) {
if (this.storageAccess.isFileProcessing(path)) {
// Logger(`Raw file event skipped because the file is being processed: ${path}`, LOG_LEVEL_VERBOSE);
return;
}
// Only for internal files.
if (!this.plugin.settings) return;
// if (this.plugin.settings.useIgnoreFiles && this.plugin.ignoreFiles.some(e => path.endsWith(e.trim()))) {
if (this.plugin.settings.useIgnoreFiles) {
// If it is one of ignore files, refresh the cached one.
// (Calling$$isTargetFile will refresh the cache)
void this.services.vault.isTargetFile(path).then(() => this._watchVaultRawEvents(path));
} else {
void this._watchVaultRawEvents(path);
}
}
async _watchVaultRawEvents(path: FilePath) {
if (!this.plugin.settings.syncInternalFiles && !this.plugin.settings.usePluginSync) return;
if (!this.plugin.settings.watchInternalFileChanges) return;
if (!path.startsWith(this.plugin.app.vault.configDir)) return;
if (path.endsWith("/")) {
// Folder
return;
}
const isTargetFile = await this.cmdHiddenFileSync.isTargetFile(path);
if (!isTargetFile) return;
void this.appendQueue(
[
{
type: "INTERNAL",
file: InternalFileToUXFileInfoStub(path),
skipBatchWait: true, // Internal files should be processed immediately.
},
],
null
);
}
// Cache file and waiting to can be proceed.
async appendQueue(params: FileEvent[], ctx?: any) {
if (!this.core.settings.isConfigured) return;
if (this.core.settings.suspendFileWatching) return;
if (this.core.settings.maxMTimeForReflectEvents > 0) {
return;
}
this.core.services.vault.markFileListPossiblyChanged();
// Flag up to be reload
for (const param of params) {
if (shouldBeIgnored(param.file.path)) {
continue;
}
const atomicKey = [0, 0, 0, 0, 0, 0].map((e) => `${Math.floor(Math.random() * 100000)}`).join("-");
const type = param.type;
const file = param.file;
const oldPath = param.oldPath;
if (type !== "INTERNAL") {
const size = (file as UXFileInfoStub).stat.size;
if (this.services.vault.isFileSizeTooLarge(size) && (type == "CREATE" || type == "CHANGED")) {
Logger(
`The storage file has been changed but exceeds the maximum size. Skipping: ${param.file.path}`,
LOG_LEVEL_NOTICE
);
continue;
}
}
if (file instanceof TFolder) continue;
// TODO: Confirm why only the TFolder skipping
// Possibly following line is needed...
// if (file?.isFolder) continue;
if (!(await this.services.vault.isTargetFile(file.path))) continue;
// Stop cache using to prevent the corruption;
// let cache: null | string | ArrayBuffer;
// new file or something changed, cache the changes.
// if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
if (file instanceof TFile || !file.isFolder) {
if (type == "CREATE" || type == "CHANGED") {
// Wait for a bit while to let the writer has marked `touched` at the file.
await delay(10);
if (this.core.storageAccess.recentlyTouched(file.path)) {
continue;
}
}
}
let cache: string | undefined = undefined;
if (param.cachedData) {
cache = param.cachedData;
}
void this.enqueue({
type,
args: {
file: file,
oldPath,
cache,
ctx,
},
skipBatchWait: param.skipBatchWait,
key: atomicKey,
});
}
}
private bufferedQueuedItems = [] as (FileEventItem | FileEventItemSentinel)[];
/**
* Immediately take snapshot.
*/
private _triggerTakeSnapshot() {
void this._takeSnapshot();
}
/**
* Trigger taking snapshot after throttled period.
*/
triggerTakeSnapshot = throttle(() => this._triggerTakeSnapshot(), 100);
enqueue(newItem: FileEventItem) {
if (newItem.type == "DELETE") {
// If the sentinel pushed, the runQueuedEvents will wait for idle before processing delete.
this.bufferedQueuedItems.push({
type: TYPE_SENTINEL_FLUSH,
});
}
this.updateStatus();
this.bufferedQueuedItems.push(newItem);
fireAndForget(() => this._takeSnapshot().then(() => this.runQueuedEvents()));
}
// Limit concurrent processing to reduce the IO load. file-processing + scheduler (1), so file events can be processed in 4 slots.
concurrentProcessing = Semaphore(5);
private _waitingMap = new Map<string, WaitInfo>();
private _waitForIdle: Promise<void> | null = null;
/**
* Wait until all queued events are processed.
* Subsequent new events will not be waited, but new events will not be added.
* @returns
*/
waitForIdle(): Promise<void> {
if (this._waitingMap.size === 0) {
return Promise.resolve();
}
if (this._waitForIdle) {
return this._waitForIdle;
}
const promises = [...this._waitingMap.entries()].map(([key, waitInfo]) => {
return new Promise<void>((resolve) => {
waitInfo.canProceed.promise
.then(() => {
Logger(`Processing ${key}: Wait for idle completed`, LOG_LEVEL_DEBUG);
// No op
})
.catch((e) => {
Logger(`Processing ${key}: Wait for idle error`, LOG_LEVEL_INFO);
Logger(e, LOG_LEVEL_VERBOSE);
//no op
})
.finally(() => {
resolve();
});
this._proceedWaiting(key);
});
});
const waitPromise = Promise.all(promises).then(() => {
this._waitForIdle = null;
Logger(`All wait for idle completed`, LOG_LEVEL_VERBOSE);
});
this._waitForIdle = waitPromise;
return waitPromise;
}
/**
* Proceed waiting for the given key immediately.
*/
private _proceedWaiting(key: string) {
const waitInfo = this._waitingMap.get(key);
if (waitInfo) {
waitInfo.canProceed.resolve(true);
clearTimeout(waitInfo.timerHandler);
this._waitingMap.delete(key);
}
this.triggerTakeSnapshot();
}
/**
* Cancel waiting for the given key.
*/
private _cancelWaiting(key: string) {
const waitInfo = this._waitingMap.get(key);
if (waitInfo) {
waitInfo.canProceed.resolve(false);
clearTimeout(waitInfo.timerHandler);
this._waitingMap.delete(key);
}
this.triggerTakeSnapshot();
}
/**
* Add waiting for the given key.
* @param key
* @param event
* @param waitedSince Optional waited since timestamp to calculate the remaining delay.
*/
private _addWaiting(key: string, event: FileEventItem, waitedSince?: number): WaitInfo {
if (this._waitingMap.has(key)) {
// Already waiting
throw new Error(`Already waiting for key: ${key}`);
}
const resolver = promiseWithResolvers<boolean>();
const now = Date.now();
const since = waitedSince ?? now;
const elapsed = now - since;
const maxDelay = this.batchSaveMaximumDelay * 1000;
const remainingDelay = Math.max(0, maxDelay - elapsed);
const nextDelay = Math.min(remainingDelay, this.batchSaveMinimumDelay * 1000);
// x*<------- maxDelay --------->*
// x*<-- minDelay -->*
// x* x<-- nextDelay -->*
// x* x<-- Capped-->*
// x* x.......*
// x: event
// *: save
// When at event (x) At least, save (*) within maxDelay, but maintain minimum delay between saves.
if (elapsed >= maxDelay) {
// Already exceeded maximum delay, do not wait.
Logger(`Processing ${key}: Batch save maximum delay already exceeded: ${event.type}`, LOG_LEVEL_DEBUG);
} else {
Logger(`Processing ${key}: Adding waiting for batch save: ${event.type} (${nextDelay}ms)`, LOG_LEVEL_DEBUG);
}
const waitInfo: WaitInfo = {
since: since,
type: event.type,
event: event,
canProceed: resolver,
timerHandler: setTimeout(() => {
Logger(`Processing ${key}: Batch save timeout reached: ${event.type}`, LOG_LEVEL_DEBUG);
this._proceedWaiting(key);
}, nextDelay),
};
this._waitingMap.set(key, waitInfo);
this.triggerTakeSnapshot();
return waitInfo;
}
/**
* Process the given file event.
*/
async processFileEvent(fei: FileEventItem) {
const releaser = await this.concurrentProcessing.acquire();
try {
this.updateStatus();
const filename = fei.args.file.path;
const waitingKey = `${filename}`;
const previous = this._waitingMap.get(waitingKey);
let isShouldBeCancelled = fei.skipBatchWait || false;
let previousPromise: Promise<boolean> = Promise.resolve(true);
let waitPromise: Promise<boolean> = Promise.resolve(true);
// 1. Check if there is previous waiting for the same file
if (previous) {
previousPromise = previous.canProceed.promise;
if (isShouldBeCancelled) {
Logger(
`Processing ${filename}: Requested to perform immediately, cancelling previous waiting: ${fei.type}`,
LOG_LEVEL_DEBUG
);
}
if (!isShouldBeCancelled && fei.type === "DELETE") {
// For DELETE, cancel any previous waiting and proceed immediately
// That because when deleting, we cannot read the file anymore.
Logger(
`Processing ${filename}: DELETE requested, cancelling previous waiting: ${fei.type}`,
LOG_LEVEL_DEBUG
);
isShouldBeCancelled = true;
}
if (!isShouldBeCancelled && previous.type === fei.type) {
// For the same type, we can cancel the previous waiting and proceed immediately.
Logger(`Processing ${filename}: Cancelling previous waiting: ${fei.type}`, LOG_LEVEL_DEBUG);
isShouldBeCancelled = true;
}
// 2. wait for the previous to complete
if (isShouldBeCancelled) {
this._cancelWaiting(waitingKey);
Logger(`Processing ${filename}: Previous cancelled: ${fei.type}`, LOG_LEVEL_DEBUG);
isShouldBeCancelled = true;
}
if (!isShouldBeCancelled) {
Logger(`Processing ${filename}: Waiting for previous to complete: ${fei.type}`, LOG_LEVEL_DEBUG);
this._proceedWaiting(waitingKey);
Logger(`Processing ${filename}: Previous completed: ${fei.type}`, LOG_LEVEL_DEBUG);
}
}
await previousPromise;
// 3. Check if shouldBatchSave is true
if (this.shouldBatchSave && !fei.skipBatchWait) {
// if type is CREATE or CHANGED, set waiting
if (fei.type == "CREATE" || fei.type == "CHANGED") {
// 3.2. If true, set the queue, and wait for the waiting, or until timeout
// (since is copied from previous waiting if exists to limit the maximum wait time)
// console.warn(`Since:`, previous?.since);
const info = this._addWaiting(waitingKey, fei, previous?.since);
waitPromise = info.canProceed.promise;
} else if (fei.type == "DELETE") {
// For DELETE, cancel any previous waiting and proceed immediately
}
Logger(`Processing ${filename}: Waiting for batch save: ${fei.type}`, LOG_LEVEL_DEBUG);
const canProceed = await waitPromise;
if (!canProceed) {
// 3.2.1. If cancelled by new queue, cancel subsequent process.
Logger(`Processing ${filename}: Cancelled by new queue: ${fei.type}`, LOG_LEVEL_DEBUG);
return;
}
}
// await this.handleFileEvent(fei);
await this.requestProcessQueue(fei);
} finally {
await this._takeSnapshot();
releaser();
}
}
async _takeSnapshot() {
const processingEvents = [...this._waitingMap.values()].map((e) => e.event);
const waitingEvents = this.bufferedQueuedItems;
const snapShot = [...processingEvents, ...waitingEvents];
await this.core.kvDB.set("storage-event-manager-snapshot", snapShot);
Logger(`Storage operation snapshot taken: ${snapShot.length} items`, LOG_LEVEL_DEBUG);
this.updateStatus();
}
async _restoreFromSnapshot() {
const snapShot = await this.core.kvDB.get<(FileEventItem | FileEventItemSentinel)[]>(
"storage-event-manager-snapshot"
);
if (snapShot && Array.isArray(snapShot) && snapShot.length > 0) {
// console.warn(`Restoring snapshot: ${snapShot.length} items`);
Logger(`Restoring storage operation snapshot: ${snapShot.length} items`, LOG_LEVEL_VERBOSE);
// Restore the snapshot
// Note: Mark all items as skipBatchWait to prevent apply the off-line batch saving.
this.bufferedQueuedItems = snapShot.map((e) => ({ ...e, skipBatchWait: true }));
this.updateStatus();
await this.runQueuedEvents();
} else {
Logger(`No snapshot to restore`, LOG_LEVEL_VERBOSE);
// console.warn(`No snapshot to restore`);
}
}
runQueuedEvents() {
return skipIfDuplicated("storage-event-manager-run-queued-events", async () => {
do {
if (this.bufferedQueuedItems.length === 0) {
break;
}
// 1. Get the first queued item
const fei = this.bufferedQueuedItems.shift()!;
await this._takeSnapshot();
this.updateStatus();
// 2. Consume 1 semaphore slot to enqueue processing. Then release immediately.
// (Just to limit the total concurrent processing count, because skipping batch handles at processFileEvent).
const releaser = await this.concurrentProcessing.acquire();
releaser();
this.updateStatus();
// 3. Check if sentinel flush
// If sentinel, wait for idle and continue.
if (fei.type === TYPE_SENTINEL_FLUSH) {
Logger(`Waiting for idle`, LOG_LEVEL_VERBOSE);
// Flush all waiting batch queues
await this.waitForIdle();
this.updateStatus();
continue;
}
// 4. Process the event, this should be fire-and-forget to not block the queue processing in each file.
fireAndForget(() => this.processFileEvent(fei));
} while (this.bufferedQueuedItems.length > 0);
});
}
processingCount = 0;
async requestProcessQueue(fei: FileEventItem) {
try {
this.processingCount++;
// this.bufferedQueuedItems.remove(fei);
this.updateStatus();
// this.waitedSince.delete(fei.args.file.path);
await this.handleFileEvent(fei);
await this._takeSnapshot();
} finally {
this.processingCount--;
this.updateStatus();
}
}
isWaiting(filename: FilePath) {
return isWaitingForTimeout(`storage-event-manager-batchsave-${filename}`);
}
updateStatus() {
const allFileEventItems = this.bufferedQueuedItems.filter((e): e is FileEventItem => "args" in e);
const allItems = allFileEventItems.filter((e) => !e.cancelled);
const totalItems = allItems.length + this.concurrentProcessing.waiting;
const processing = this.processingCount;
const batchedCount = this._waitingMap.size;
this.core.batched.value = batchedCount;
this.core.processing.value = processing;
this.core.totalQueued.value = totalItems + batchedCount + processing;
}
async handleFileEvent(queue: FileEventItem): Promise<any> {
const file = queue.args.file;
const lockKey = `handleFile:${file.path}`;
const ret = await serialized(lockKey, async () => {
if (queue.cancelled) {
Logger(`File event cancelled before processing: ${file.path}`, LOG_LEVEL_INFO);
return;
}
if (queue.type == "INTERNAL" || file.isInternal) {
await this.core.services.fileProcessing.processOptionalFileEvent(file.path as unknown as FilePath);
} else {
const key = `file-last-proc-${queue.type}-${file.path}`;
const last = Number((await this.core.kvDB.get(key)) || 0);
if (queue.type == "DELETE") {
await this.core.services.fileProcessing.processFileEvent(queue);
} else {
if (file.stat.mtime == last) {
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL_VERBOSE);
// Should Cancel the relative operations? (e.g. rename)
// this.cancelRelativeEvent(queue);
return;
}
if (!(await this.core.services.fileProcessing.processFileEvent(queue))) {
Logger(
`STORAGE -> DB: Handler failed, cancel the relative operations: ${file.path}`,
LOG_LEVEL_INFO
);
// cancel running queues and remove one of atomic operation (e.g. rename)
this.cancelRelativeEvent(queue);
return;
}
}
}
});
this.updateStatus();
return ret;
}
cancelRelativeEvent(item: FileEventItem): void {
this._cancelWaiting(item.args.file.path);
}
}
@@ -2,7 +2,6 @@
import { TFile, type TAbstractFile, type TFolder } from "../../../deps.ts";
import { ICHeader } from "../../../common/types.ts";
import type { ObsidianFileAccess } from "./SerializedFileAccess.ts";
import { addPrefix, isPlainText } from "../../../lib/src/string_and_binary/path.ts";
import { LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
import { createBlob } from "../../../lib/src/common/utils.ts";
@@ -15,6 +14,7 @@ import type {
UXInternalFileInfoStub,
} from "../../../lib/src/common/types.ts";
import type { LiveSyncCore } from "../../../main.ts";
import type { FileAccessObsidian } from "@/serviceModules/FileAccessObsidian.ts";
export async function TFileToUXFileInfo(
core: LiveSyncCore,
@@ -51,7 +51,7 @@ export async function TFileToUXFileInfo(
export async function InternalFileToUXFileInfo(
fullPath: string,
vaultAccess: ObsidianFileAccess,
vaultAccess: FileAccessObsidian,
prefix: string = ICHeader
): Promise<UXFileInfo> {
const name = fullPath.split("/").pop() as string;
@@ -421,7 +421,7 @@ export class ModuleInitializerFile extends AbstractModule {
private _reportDetectedErrors(): Promise<string[]> {
return Promise.resolve(Array.from(this._detectedErrors));
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.appLifecycle.getUnresolvedMessages.addHandler(this._reportDetectedErrors.bind(this));
services.databaseEvents.initialiseDatabase.setHandler(this._initializeDatabase.bind(this));
services.vault.scanVault.setHandler(this._performFullScan.bind(this));
+1 -1
View File
@@ -353,7 +353,7 @@ export class ModuleMigration extends AbstractModule {
});
return Promise.resolve(true);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
super.onBindFunction(core, services);
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
services.appLifecycle.onFirstInitialise.addHandler(this._everyOnFirstInitialize.bind(this));
@@ -28,7 +28,10 @@ export class ObsHttpHandler extends FetchHttpHandler {
this.reverseProxyNoSignUrl = reverseProxyNoSignUrl;
}
// eslint-disable-next-line require-await
async handle(request: HttpRequest, { abortSignal }: HttpHandlerOptions = {}): Promise<{ response: HttpResponse }> {
override async handle(
request: HttpRequest,
{ abortSignal }: HttpHandlerOptions = {}
): Promise<{ response: HttpResponse }> {
if (abortSignal?.aborted) {
const abortError = new Error("Request aborted");
abortError.name = "AbortError";
@@ -127,7 +127,7 @@ export class ModuleCheckRemoteSize extends AbstractModule {
eventHub.onEvent(EVENT_REQUEST_CHECK_REMOTE_SIZE, () => this.checkRemoteSize());
return Promise.resolve(true);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onScanningStartupIssues.addHandler(this._allScanStat.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
@@ -10,9 +10,9 @@ import {
import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from "../../deps.ts";
import { type CouchDBCredentials, type EntryDoc } from "../../lib/src/common/types.ts";
import { isCloudantURI, isValidRemoteCouchDBURI } from "../../lib/src/pouchdb/utils_couchdb.ts";
import { replicationFilter } from "@/lib/src/pouchdb/compress.ts";
import { disableEncryption } from "@/lib/src/pouchdb/encryption.ts";
import { enableEncryption } from "@/lib/src/pouchdb/encryption.ts";
import { replicationFilter } from "@lib/pouchdb/compress.ts";
import { disableEncryption } from "@lib/pouchdb/encryption.ts";
import { enableEncryption } from "@lib/pouchdb/encryption.ts";
import { setNoticeClass } from "../../lib/src/mock_and_interop/wrapper.ts";
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.ts";
import { AuthorizationHeaderGenerator } from "../../lib/src/replication/httplib.ts";
@@ -96,7 +96,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
const size = body ? ` (${body.length})` : "";
try {
const r = await this.__fetchByAPI(url, authHeader, opts);
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
this.services.API.requestCount.value = this.services.API.requestCount.value + 1;
if (method == "POST" || method == "PUT") {
this.last_successful_post = r.status - (r.status % 100) == 200;
} else {
@@ -113,7 +113,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
this._log(ex);
throw ex;
} finally {
this.plugin.responseCount.value = this.plugin.responseCount.value + 1;
this.services.API.responseCount.value = this.services.API.responseCount.value + 1;
}
}
@@ -171,7 +171,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
headers.append("authorization", authHeader);
}
try {
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
this.services.API.requestCount.value = this.services.API.requestCount.value + 1;
const response: Response = await (useRequestAPI
? this.__fetchByAPI(url.toString(), authHeader, { ...opts, headers })
: fetch(url, { ...opts, headers }));
@@ -245,7 +245,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
this._log(ex);
throw ex;
} finally {
this.plugin.responseCount.value = this.plugin.responseCount.value + 1;
this.services.API.responseCount.value = this.services.API.responseCount.value + 1;
}
// return await fetch(url, opts);
@@ -282,7 +282,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
return Promise.resolve([...this._previousErrors]);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
override onBindFunction(core: LiveSyncCore, services: typeof core.services) {
services.API.isLastPostFailedDueToPayloadSize.setHandler(this._getLastPostFailedBySize.bind(this));
services.remote.connect.setHandler(this._connectRemoteCouchDB.bind(this));
services.appLifecycle.getUnresolvedMessages.addHandler(this._reportUnresolvedMessages.bind(this));
@@ -5,7 +5,7 @@ import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { type TFile } from "../../deps.ts";
import { fireAndForget } from "octagonal-wheels/promises";
import { type FilePathWithPrefix } from "../../lib/src/common/types.ts";
import { reactive, reactiveSource } from "octagonal-wheels/dataobject/reactive";
import { reactive, reactiveSource, type ReactiveSource } from "octagonal-wheels/dataobject/reactive";
import {
collectingChunks,
pluginScanningCount,
@@ -45,7 +45,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
this.initialCallback = save;
saveCommandDefinition.callback = () => {
scheduleTask("syncOnEditorSave", 250, () => {
if (this.services.appLifecycle.hasUnloaded()) {
if (this.services.control.hasUnloaded()) {
this._log("Unload and remove the handler.", LOG_LEVEL_VERBOSE);
saveCommandDefinition.callback = this.initialCallback;
this.initialCallback = undefined;
@@ -188,20 +188,25 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
}
});
}
// TODO: separate
// Process counting for app reload scheduling
_totalProcessingCount?: ReactiveSource<number> = undefined;
private _scheduleAppReload() {
if (!this.core._totalProcessingCount) {
if (!this._totalProcessingCount) {
const __tick = reactiveSource(0);
this.core._totalProcessingCount = reactive(() => {
const dbCount = this.core.databaseQueueCount.value;
const replicationCount = this.core.replicationResultCount.value;
const storageApplyingCount = this.core.storageApplyingCount.value;
this._totalProcessingCount = reactive(() => {
const dbCount = this.services.replication.databaseQueueCount.value;
const replicationCount = this.services.replication.replicationResultCount.value;
const storageApplyingCount = this.services.replication.storageApplyingCount.value;
const chunkCount = collectingChunks.value;
const pluginScanCount = pluginScanningCount.value;
const hiddenFilesCount = hiddenFilesEventCount.value + hiddenFilesProcessingCount.value;
const conflictProcessCount = this.core.conflictProcessQueueCount.value;
const e = this.core.pendingFileEventCount.value;
const proc = this.core.processingFileEventCount.value;
const conflictProcessCount = this.services.conflict.conflictProcessQueueCount.value;
// Now no longer `pendingFileEventCount` and `processingFileEventCount` is used
// const e = this.core.pendingFileEventCount.value;
// const proc = this.core.processingFileEventCount.value;
const e = 0;
const proc = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const __ = __tick.value;
return (
@@ -223,7 +228,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
);
let stableCheck = 3;
this.core._totalProcessingCount.onChanged((e) => {
this._totalProcessingCount.onChanged((e) => {
if (e.value == 0) {
if (stableCheck-- <= 0) {
this.__performAppReload();
@@ -239,10 +244,14 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
});
}
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
_isReloadingScheduled(): boolean {
return this._totalProcessingCount !== undefined;
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.askRestart.setHandler(this._askReload.bind(this));
services.appLifecycle.scheduleRestart.setHandler(this._scheduleAppReload.bind(this));
services.appLifecycle.isReloadingScheduled.setHandler(this._isReloadingScheduled.bind(this));
}
}
@@ -59,7 +59,7 @@ export class ModuleObsidianMenu extends AbstractModule {
this.settings.liveSync = true;
this._log("LiveSync Enabled.", LOG_LEVEL_NOTICE);
}
await this.services.setting.realiseSetting();
await this.services.control.applySettings();
await this.services.setting.saveSettingData();
},
});
@@ -74,7 +74,7 @@ export class ModuleObsidianMenu extends AbstractModule {
this.services.appLifecycle.setSuspended(true);
this._log("Self-hosted LiveSync suspended", LOG_LEVEL_NOTICE);
}
await this.services.setting.realiseSetting();
await this.services.control.applySettings();
await this.services.setting.saveSettingData();
},
});
@@ -106,7 +106,7 @@ export class ModuleObsidianMenu extends AbstractModule {
return Promise.resolve(true);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}
+1 -1
View File
@@ -156,7 +156,7 @@ export class ModuleDev extends AbstractObsidianModule {
// this.addTestResult("Test of test3", true);
return this.testDone();
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
+1 -1
View File
@@ -440,7 +440,7 @@ Line4:D`;
return Promise.resolve(true);
}
onBindFunction(core: typeof this.core, services: typeof core.services): void {
override onBindFunction(core: typeof this.core, services: typeof core.services): void {
services.test.testMultiDevice.addHandler(this._everyModuleTestMultiDevice.bind(this));
}
}
+1 -1
View File
@@ -581,7 +581,7 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
await this._test("Conflict resolution", async () => await this.checkConflictResolution());
return this.testDone();
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this));
services.test.testMultiDevice.addHandler(this._everyModuleTestMultiDevice.bind(this));
+5 -5
View File
@@ -8,11 +8,11 @@ export class TestPaneView extends ItemView {
component?: TestPaneComponent;
plugin: ObsidianLiveSyncPlugin;
moduleDev: ModuleDev;
icon = "view-log";
override icon = "view-log";
title: string = "Self-hosted LiveSync Test and Results";
navigation = true;
override navigation = true;
getIcon(): string {
override getIcon(): string {
return "view-log";
}
@@ -30,7 +30,7 @@ export class TestPaneView extends ItemView {
return "Self-hosted LiveSync Test and Results";
}
async onOpen() {
override async onOpen() {
this.component = new TestPaneComponent({
target: this.contentEl,
props: {
@@ -41,7 +41,7 @@ export class TestPaneView extends ItemView {
await Promise.resolve();
}
async onClose() {
override async onClose() {
this.component?.$destroy();
await Promise.resolve();
}
@@ -214,7 +214,7 @@ export class DocumentHistoryModal extends Modal {
}
}
onOpen() {
override onOpen() {
const { contentEl } = this;
this.titleEl.setText("Document History");
contentEl.empty();
@@ -299,7 +299,7 @@ export class DocumentHistoryModal extends Modal {
});
});
}
onClose() {
override onClose() {
const { contentEl } = this;
contentEl.empty();
this.BlobURLs.forEach((value) => {
@@ -16,11 +16,11 @@ export class GlobalHistoryView extends SvelteItemView {
}
plugin: ObsidianLiveSyncPlugin;
icon = "clock";
override icon = "clock";
title: string = "";
navigation = true;
override navigation = true;
getIcon(): string {
override getIcon(): string {
return "clock";
}
@@ -44,7 +44,7 @@ export class ConflictResolveModal extends Modal {
// sendValue("close-resolve-conflict:" + this.filename, false);
}
onOpen() {
override onOpen() {
const { contentEl } = this;
// Send cancel signal for the previous merge dialogue
// if not there, simply be ignored.
@@ -119,7 +119,7 @@ export class ConflictResolveModal extends Modal {
this.close();
}
onClose() {
override onClose() {
const { contentEl } = this;
contentEl.empty();
if (this.offEvent) {
+3 -3
View File
@@ -19,11 +19,11 @@ export class LogPaneView extends SvelteItemView {
}
plugin: ObsidianLiveSyncPlugin;
icon = "view-log";
override icon = "view-log";
title: string = "";
navigation = false;
override navigation = false;
getIcon(): string {
override getIcon(): string {
return "view-log";
}
+1 -1
View File
@@ -19,7 +19,7 @@ export class ModuleObsidianGlobalHistory extends AbstractObsidianModule {
showGlobalHistory() {
void this.services.API.showWindow(VIEW_TYPE_GLOBAL_HISTORY);
}
onBindFunction(core: typeof this.core, services: typeof core.services): void {
override onBindFunction(core: typeof this.core, services: typeof core.services): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}
@@ -169,7 +169,7 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
}
return true;
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onScanningStartupIssues.addHandler(this._allScanStat.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.conflict.resolveByUserInteraction.addHandler(this._anyResolveConflictByUI.bind(this));
+13 -13
View File
@@ -32,14 +32,14 @@ import { serialized } from "octagonal-wheels/concurrency/lock";
import { $msg } from "src/lib/src/common/i18n.ts";
import { P2PLogCollector } from "../../lib/src/replication/trystero/P2PReplicatorCore.ts";
import type { LiveSyncCore } from "../../main.ts";
import { LiveSyncError } from "@/lib/src/common/LSError.ts";
import { LiveSyncError } from "@lib/common/LSError.ts";
import { isValidPath } from "@/common/utils.ts";
import {
isValidFilenameInAndroid,
isValidFilenameInDarwin,
isValidFilenameInWidows,
} from "@/lib/src/string_and_binary/path.ts";
import { MARK_LOG_SEPARATOR } from "@/lib/src/services/lib/logUtils.ts";
} from "@lib/string_and_binary/path.ts";
import { MARK_LOG_SEPARATOR } from "@lib/services/lib/logUtils.ts";
// This module cannot be a core module because it depends on the Obsidian UI.
@@ -102,12 +102,12 @@ export class ModuleLog extends AbstractObsidianModule {
});
return computed(() => formatted.value);
}
const labelReplication = padLeftSpComputed(this.core.replicationResultCount, `📥`);
const labelDBCount = padLeftSpComputed(this.core.databaseQueueCount, `📄`);
const labelStorageCount = padLeftSpComputed(this.core.storageApplyingCount, `💾`);
const labelReplication = padLeftSpComputed(this.services.replication.replicationResultCount, `📥`);
const labelDBCount = padLeftSpComputed(this.services.replication.databaseQueueCount, `📄`);
const labelStorageCount = padLeftSpComputed(this.services.replication.storageApplyingCount, `💾`);
const labelChunkCount = padLeftSpComputed(collectingChunks, `🧩`);
const labelPluginScanCount = padLeftSpComputed(pluginScanningCount, `🔌`);
const labelConflictProcessCount = padLeftSpComputed(this.core.conflictProcessQueueCount, `🔩`);
const labelConflictProcessCount = padLeftSpComputed(this.services.conflict.conflictProcessQueueCount, `🔩`);
const hiddenFilesCount = reactive(() => hiddenFilesEventCount.value - hiddenFilesProcessingCount.value);
const labelHiddenFilesCount = padLeftSpComputed(hiddenFilesCount, `⚙️`);
const queueCountLabelX = reactive(() => {
@@ -116,12 +116,12 @@ export class ModuleLog extends AbstractObsidianModule {
const queueCountLabel = () => queueCountLabelX.value;
const requestingStatLabel = computed(() => {
const diff = this.core.requestCount.value - this.core.responseCount.value;
const diff = this.services.API.requestCount.value - this.services.API.responseCount.value;
return diff != 0 ? "📲 " : "";
});
const replicationStatLabel = computed(() => {
const e = this.core.replicationStat.value;
const e = this.services.replicator.replicationStatics.value;
const sent = e.sent;
const arrived = e.arrived;
const maxPullSeq = e.maxPullSeq;
@@ -173,9 +173,9 @@ export class ModuleLog extends AbstractObsidianModule {
}
return { w, sent, pushLast, arrived, pullLast };
});
const labelProc = padLeftSpComputed(this.core.processing, ``);
const labelPend = padLeftSpComputed(this.core.totalQueued, `🛫`);
const labelInBatchDelay = padLeftSpComputed(this.core.batched, `📬`);
const labelProc = padLeftSpComputed(this.services.fileProcessing.processing, ``);
const labelPend = padLeftSpComputed(this.services.fileProcessing.totalQueued, `🛫`);
const labelInBatchDelay = padLeftSpComputed(this.services.fileProcessing.batched, `📬`);
const waitingLabel = computed(() => {
return `${labelProc()}${labelPend()}${labelInBatchDelay()}`;
});
@@ -495,7 +495,7 @@ export class ModuleLog extends AbstractObsidianModule {
}
}
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.API.addLog.setHandler(globalLogFunction);
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
@@ -50,7 +50,7 @@ export class ModuleObsidianDocumentHistory extends AbstractObsidianModule {
this.showHistory(targetId.path, targetId.id);
}
}
onBindFunction(core: typeof this.core, services: typeof core.services): void {
override onBindFunction(core: typeof this.core, services: typeof core.services): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}
@@ -1,356 +0,0 @@
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED, eventHub } from "../../common/events.ts";
import {
type BucketSyncSetting,
ChunkAlgorithmNames,
type ConfigPassphraseStore,
type CouchDBConnection,
DEFAULT_SETTINGS,
type ObsidianLiveSyncSettings,
SALT_OF_PASSPHRASE,
SETTING_KEY_P2P_DEVICE_NAME,
} from "../../lib/src/common/types";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT } from "octagonal-wheels/common/logger";
import { $msg, setLang } from "../../lib/src/common/i18n.ts";
import { isCloudantURI } from "../../lib/src/pouchdb/utils_couchdb.ts";
import { getLanguage } from "@/deps.ts";
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../../lib/src/common/rosetta.ts";
import { decryptString, encryptString } from "@/lib/src/encryption/stringEncryption.ts";
import type { LiveSyncCore } from "../../main.ts";
import { AbstractModule } from "../AbstractModule.ts";
export class ModuleObsidianSettings extends AbstractModule {
async _everyOnLayoutReady(): Promise<boolean> {
let isChanged = false;
if (this.settings.displayLanguage == "") {
const obsidianLanguage = getLanguage();
if (
SUPPORTED_I18N_LANGS.indexOf(obsidianLanguage) !== -1 && // Check if the language is supported
obsidianLanguage != this.settings.displayLanguage // Check if the language is different from the current setting
) {
// Check if the current setting is not empty (Means migrated or installed).
this.settings.displayLanguage = obsidianLanguage as I18N_LANGS;
isChanged = true;
setLang(this.settings.displayLanguage);
} else if (this.settings.displayLanguage == "") {
this.settings.displayLanguage = "def";
setLang(this.settings.displayLanguage);
await this.services.setting.saveSettingData();
}
}
if (isChanged) {
const revert = $msg("dialog.yourLanguageAvailable.btnRevertToDefault");
if (
(await this.core.confirm.askSelectStringDialogue($msg(`dialog.yourLanguageAvailable`), ["OK", revert], {
defaultAction: "OK",
title: $msg(`dialog.yourLanguageAvailable.Title`),
})) == revert
) {
this.settings.displayLanguage = "def";
setLang(this.settings.displayLanguage);
}
await this.services.setting.saveSettingData();
}
return true;
}
getPassphrase(settings: ObsidianLiveSyncSettings) {
const methods: Record<ConfigPassphraseStore, () => Promise<string | false>> = {
"": () => Promise.resolve("*"),
LOCALSTORAGE: () => Promise.resolve(localStorage.getItem("ls-setting-passphrase") ?? false),
ASK_AT_LAUNCH: () => this.core.confirm.askString("Passphrase", "passphrase", ""),
};
const method = settings.configPassphraseStore;
const methodFunc = method in methods ? methods[method] : methods[""];
return methodFunc();
}
_saveDeviceAndVaultName(): void {
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.services.vault.getVaultName();
localStorage.setItem(lsKey, this.services.setting.getDeviceAndVaultName() || "");
}
usedPassphrase = "";
private _clearUsedPassphrase(): void {
this.usedPassphrase = "";
}
async decryptConfigurationItem(encrypted: string, passphrase: string) {
const dec = await decryptString(encrypted, passphrase + SALT_OF_PASSPHRASE);
if (dec) {
this.usedPassphrase = passphrase;
return dec;
}
return false;
}
async encryptConfigurationItem(src: string, settings: ObsidianLiveSyncSettings) {
if (this.usedPassphrase != "") {
return await encryptString(src, this.usedPassphrase + SALT_OF_PASSPHRASE);
}
const passphrase = await this.getPassphrase(settings);
if (passphrase === false) {
this._log(
"Failed to obtain passphrase when saving data.json! Please verify the configuration.",
LOG_LEVEL_URGENT
);
return "";
}
const dec = await encryptString(src, passphrase + SALT_OF_PASSPHRASE);
if (dec) {
this.usedPassphrase = passphrase;
return dec;
}
return "";
}
get appId() {
return this.services.API.getAppID();
}
async _saveSettingData() {
this.services.setting.saveDeviceAndVaultName();
const settings = { ...this.settings };
settings.deviceAndVaultName = "";
if (settings.P2P_DevicePeerName && settings.P2P_DevicePeerName.trim() !== "") {
console.log("Saving device peer name to small config");
this.services.config.setSmallConfig(SETTING_KEY_P2P_DEVICE_NAME, settings.P2P_DevicePeerName.trim());
settings.P2P_DevicePeerName = "";
}
if (this.usedPassphrase == "" && !(await this.getPassphrase(settings))) {
this._log("Failed to retrieve passphrase. data.json contains unencrypted items!", LOG_LEVEL_NOTICE);
} else {
if (
settings.couchDB_PASSWORD != "" ||
settings.couchDB_URI != "" ||
settings.couchDB_USER != "" ||
settings.couchDB_DBNAME
) {
const connectionSetting: CouchDBConnection & BucketSyncSetting = {
couchDB_DBNAME: settings.couchDB_DBNAME,
couchDB_PASSWORD: settings.couchDB_PASSWORD,
couchDB_URI: settings.couchDB_URI,
couchDB_USER: settings.couchDB_USER,
accessKey: settings.accessKey,
bucket: settings.bucket,
endpoint: settings.endpoint,
region: settings.region,
secretKey: settings.secretKey,
useCustomRequestHandler: settings.useCustomRequestHandler,
bucketCustomHeaders: settings.bucketCustomHeaders,
couchDB_CustomHeaders: settings.couchDB_CustomHeaders,
useJWT: settings.useJWT,
jwtKey: settings.jwtKey,
jwtAlgorithm: settings.jwtAlgorithm,
jwtKid: settings.jwtKid,
jwtExpDuration: settings.jwtExpDuration,
jwtSub: settings.jwtSub,
useRequestAPI: settings.useRequestAPI,
bucketPrefix: settings.bucketPrefix,
forcePathStyle: settings.forcePathStyle,
};
settings.encryptedCouchDBConnection = await this.encryptConfigurationItem(
JSON.stringify(connectionSetting),
settings
);
settings.couchDB_PASSWORD = "";
settings.couchDB_DBNAME = "";
settings.couchDB_URI = "";
settings.couchDB_USER = "";
settings.accessKey = "";
settings.bucket = "";
settings.region = "";
settings.secretKey = "";
settings.endpoint = "";
}
if (settings.encrypt && settings.passphrase != "") {
settings.encryptedPassphrase = await this.encryptConfigurationItem(settings.passphrase, settings);
settings.passphrase = "";
}
}
await this.core.saveData(settings);
eventHub.emitEvent(EVENT_SETTING_SAVED, settings);
}
tryDecodeJson(encoded: string | false): object | false {
try {
if (!encoded) return false;
return JSON.parse(encoded);
} catch {
return false;
}
}
async _decryptSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
const passphrase = await this.getPassphrase(settings);
if (passphrase === false) {
this._log("No passphrase found for data.json! Verify configuration before syncing.", LOG_LEVEL_URGENT);
} else {
if (settings.encryptedCouchDBConnection) {
const keys = [
"couchDB_URI",
"couchDB_USER",
"couchDB_PASSWORD",
"couchDB_DBNAME",
"accessKey",
"bucket",
"endpoint",
"region",
"secretKey",
] as (keyof CouchDBConnection | keyof BucketSyncSetting)[];
const decrypted = this.tryDecodeJson(
await this.decryptConfigurationItem(settings.encryptedCouchDBConnection, passphrase)
) as CouchDBConnection & BucketSyncSetting;
if (decrypted) {
for (const key of keys) {
if (key in decrypted) {
//@ts-ignore
settings[key] = decrypted[key];
}
}
} else {
this._log(
"Failed to decrypt passphrase from data.json! Ensure configuration is correct before syncing with remote.",
LOG_LEVEL_URGENT
);
for (const key of keys) {
//@ts-ignore
settings[key] = "";
}
}
}
if (settings.encrypt && settings.encryptedPassphrase) {
const encrypted = settings.encryptedPassphrase;
const decrypted = await this.decryptConfigurationItem(encrypted, passphrase);
if (decrypted) {
settings.passphrase = decrypted;
} else {
this._log(
"Failed to decrypt passphrase from data.json! Ensure configuration is correct before syncing with remote.",
LOG_LEVEL_URGENT
);
settings.passphrase = "";
}
}
}
return settings;
}
/**
* This method mutates the settings object.
* @param settings
* @returns
*/
_adjustSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
// Adjust settings as needed
// Delete this feature to avoid problems on mobile.
settings.disableRequestURI = true;
// GC is disabled.
settings.gcDelay = 0;
// So, use history is always enabled.
settings.useHistory = true;
if ("workingEncrypt" in settings) delete settings.workingEncrypt;
if ("workingPassphrase" in settings) delete settings.workingPassphrase;
// Splitter configurations have been replaced with chunkSplitterVersion.
if (settings.chunkSplitterVersion == "") {
if (settings.enableChunkSplitterV2) {
if (settings.useSegmenter) {
settings.chunkSplitterVersion = "v2-segmenter";
} else {
settings.chunkSplitterVersion = "v2";
}
} else {
settings.chunkSplitterVersion = "";
}
} else if (!(settings.chunkSplitterVersion in ChunkAlgorithmNames)) {
settings.chunkSplitterVersion = "";
}
return Promise.resolve(settings);
}
async _loadSettings(): Promise<void> {
const settings = Object.assign({}, DEFAULT_SETTINGS, await this.core.loadData()) as ObsidianLiveSyncSettings;
if (typeof settings.isConfigured == "undefined") {
// If migrated, mark true
if (JSON.stringify(settings) !== JSON.stringify(DEFAULT_SETTINGS)) {
settings.isConfigured = true;
} else {
settings.additionalSuffixOfDatabaseName = this.appId;
settings.isConfigured = false;
}
}
this.settings = await this.services.setting.decryptSettings(settings);
setLang(this.settings.displayLanguage);
await this.services.setting.adjustSettings(this.settings);
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.services.vault.getVaultName();
if (this.settings.deviceAndVaultName != "") {
if (!localStorage.getItem(lsKey)) {
this.services.setting.setDeviceAndVaultName(this.settings.deviceAndVaultName);
this.services.setting.saveDeviceAndVaultName();
this.settings.deviceAndVaultName = "";
}
}
if (isCloudantURI(this.settings.couchDB_URI) && this.settings.customChunkSize != 0) {
this._log(
"Configuration issues detected and automatically resolved. However, unsynchronized data may exist. Consider rebuilding if necessary.",
LOG_LEVEL_NOTICE
);
this.settings.customChunkSize = 0;
}
this.services.setting.setDeviceAndVaultName(localStorage.getItem(lsKey) || "");
if (this.services.setting.getDeviceAndVaultName() == "") {
if (this.settings.usePluginSync) {
this._log("Device name missing. Disabling plug-in sync.", LOG_LEVEL_NOTICE);
this.settings.usePluginSync = false;
}
}
// this.core.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB);
}
private _currentSettings(): ObsidianLiveSyncSettings {
return this.settings;
}
private _updateSettings(updateFn: (settings: ObsidianLiveSyncSettings) => ObsidianLiveSyncSettings): Promise<void> {
try {
const updated = updateFn(this.settings);
this.settings = updated;
} catch (ex) {
this._log("Error in update function: " + ex, LOG_LEVEL_URGENT);
return Promise.reject(ex);
}
return Promise.resolve();
}
private _applyPartial(partial: Partial<ObsidianLiveSyncSettings>): Promise<void> {
try {
this.settings = { ...this.settings, ...partial };
} catch (ex) {
this._log("Error in applying partial settings: " + ex, LOG_LEVEL_URGENT);
return Promise.reject(ex);
}
return Promise.resolve();
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
super.onBindFunction(core, services);
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
services.setting.clearUsedPassphrase.setHandler(this._clearUsedPassphrase.bind(this));
services.setting.decryptSettings.setHandler(this._decryptSettings.bind(this));
services.setting.adjustSettings.setHandler(this._adjustSettings.bind(this));
services.setting.loadSettings.setHandler(this._loadSettings.bind(this));
services.setting.currentSettings.setHandler(this._currentSettings.bind(this));
services.setting.updateSettings.setHandler(this._updateSettings.bind(this));
services.setting.applyPartial.setHandler(this._applyPartial.bind(this));
services.setting.saveDeviceAndVaultName.setHandler(this._saveDeviceAndVaultName.bind(this));
services.setting.saveSettingData.setHandler(this._saveSettingData.bind(this));
}
}
@@ -6,8 +6,8 @@ import { DEFAULT_SETTINGS, type FilePathWithPrefix, type ObsidianLiveSyncSetting
import { parseYaml, stringifyYaml } from "../../deps";
import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { AbstractModule } from "../AbstractModule.ts";
import type { ServiceContext } from "@/lib/src/services/base/ServiceBase.ts";
import type { InjectableServiceHub } from "@/lib/src/services/InjectableServices.ts";
import type { ServiceContext } from "@lib/services/base/ServiceBase.ts";
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
import type { LiveSyncCore } from "@/main.ts";
const SETTING_HEADER = "````yaml:livesync-setting\n";
const SETTING_FOOTER = "\n````";
@@ -246,7 +246,7 @@ We can perform a command in this file.
}
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub<ServiceContext>): void {
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub<ServiceContext>): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}
@@ -29,7 +29,7 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
get appId() {
return `${"appId" in this.app ? this.app.appId : ""}`;
}
onBindFunction(core: typeof this.plugin, services: typeof core.services): void {
override onBindFunction(core: typeof this.plugin, services: typeof core.services): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}
+1 -1
View File
@@ -194,7 +194,7 @@ export class ModuleSetupObsidian extends AbstractModule {
// }
// }
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
}
}
@@ -58,7 +58,7 @@ export class LiveSyncSetting extends Setting {
}
}
setDesc(desc: string | DocumentFragment): this {
override setDesc(desc: string | DocumentFragment): this {
this.descBuf = desc;
DEV: {
this._createDocStub("desc", desc);
@@ -66,7 +66,7 @@ export class LiveSyncSetting extends Setting {
super.setDesc(desc);
return this;
}
setName(name: string | DocumentFragment): this {
override setName(name: string | DocumentFragment): this {
this.nameBuf = name;
DEV: {
this._createDocStub("name", name);
@@ -16,7 +16,7 @@ import {
import { delay, isObjectDifferent, sizeToHumanReadable } from "../../../lib/src/common/utils.ts";
import { versionNumberString2Number } from "../../../lib/src/string_and_binary/convert.ts";
import { Logger } from "../../../lib/src/common/logger.ts";
import { checkSyncInfo } from "@/lib/src/pouchdb/negotiation.ts";
import { checkSyncInfo } from "@lib/pouchdb/negotiation.ts";
import { testCrypt } from "octagonal-wheels/encryption/encryption";
import ObsidianLiveSyncPlugin from "../../../main.ts";
import { scheduleTask } from "../../../common/utils.ts";
@@ -374,7 +374,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.initialSettings = undefined;
}
hide() {
override hide() {
this.isShown = false;
}
isShown: boolean = false;
@@ -32,7 +32,7 @@ export function paneMaintenance(
(e) => {
e.addEventListener("click", () => {
fireAndForget(async () => {
await this.services.remote.markResolved();
await this.services.replication.markResolved();
this.display();
});
});
@@ -59,7 +59,7 @@ export function paneMaintenance(
(e) => {
e.addEventListener("click", () => {
fireAndForget(async () => {
await this.services.remote.markUnlocked();
await this.services.replication.markUnlocked();
this.display();
});
});
@@ -78,7 +78,7 @@ export function paneMaintenance(
.setDisabled(false)
.setWarning()
.onClick(async () => {
await this.services.remote.markLocked();
await this.services.replication.markLocked();
})
)
.addOnUpdate(this.onlyOnCouchDBOrMinIO);
@@ -105,7 +105,7 @@ export function paneSyncSettings(
if (!this.editingSettings.isConfigured) {
this.editingSettings.isConfigured = true;
await this.saveAllDirtySettings();
await this.services.setting.realiseSetting();
await this.services.control.applySettings();
await this.rebuildDB("localOnly");
// this.resetEditingSettings();
if (
@@ -124,13 +124,13 @@ export function paneSyncSettings(
await this.confirmRebuild();
} else {
await this.saveAllDirtySettings();
await this.services.setting.realiseSetting();
await this.services.control.applySettings();
this.services.appLifecycle.askRestart();
}
}
} else {
await this.saveAllDirtySettings();
await this.services.setting.realiseSetting();
await this.services.control.applySettings();
}
});
});
@@ -169,7 +169,7 @@ export function paneSyncSettings(
}
await this.saveSettings(["liveSync", "periodicReplication"]);
await this.services.setting.realiseSetting();
await this.services.control.applySettings();
});
new Setting(paneEl)
+66 -77
View File
@@ -3,17 +3,13 @@ import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, VER, type ObsidianLiveSyncSettings
import {
EVENT_LAYOUT_READY,
EVENT_PLUGIN_LOADED,
EVENT_PLUGIN_UNLOADED,
EVENT_REQUEST_RELOAD_SETTING_TAB,
EVENT_SETTING_SAVED,
eventHub,
} from "../../common/events.ts";
import { $msg, setLang } from "../../lib/src/common/i18n.ts";
import { versionNumberString2Number } from "../../lib/src/string_and_binary/convert.ts";
import { cancelAllPeriodicTask, cancelAllTasks } from "octagonal-wheels/concurrency/task";
import { stopAllRunningProcessors } from "octagonal-wheels/concurrency/processor";
import { AbstractModule } from "../AbstractModule.ts";
import { EVENT_PLATFORM_UNLOADED } from "@lib/events/coreEvents";
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts";
import type { LiveSyncCore } from "../../main.ts";
import { initialiseWorkerModule } from "@lib/worker/bgWorker.ts";
@@ -49,7 +45,7 @@ export class ModuleLiveSyncMain extends AbstractModule {
}
if (!(await this.core.services.appLifecycle.onFirstInitialise())) return false;
// await this.core.$$realizeSettingSyncMode();
await this.services.setting.realiseSetting();
await this.services.control.applySettings();
fireAndForget(async () => {
this._log($msg("moduleLiveSyncMain.logAdditionalSafetyScan"), LOG_LEVEL_VERBOSE);
if (!(await this.services.appLifecycle.onScanningStartupIssues())) {
@@ -65,7 +61,7 @@ export class ModuleLiveSyncMain extends AbstractModule {
eventHub.onEvent(EVENT_SETTING_SAVED, (settings: ObsidianLiveSyncSettings) => {
fireAndForget(async () => {
try {
await this.core.services.setting.realiseSetting();
await this.core.services.control.applySettings();
const lang = this.core.services.setting.currentSettings()?.displayLanguage ?? undefined;
if (lang !== undefined) {
setLang(this.core.services.setting.currentSettings()?.displayLanguage);
@@ -139,89 +135,82 @@ export class ModuleLiveSyncMain extends AbstractModule {
return true;
}
async _onLiveSyncUnload(): Promise<void> {
eventHub.emitEvent(EVENT_PLUGIN_UNLOADED);
await this.services.appLifecycle.onBeforeUnload();
cancelAllPeriodicTask();
cancelAllTasks();
stopAllRunningProcessors();
await this.services.appLifecycle.onUnload();
this._unloaded = true;
for (const addOn of this.core.addOns) {
addOn.onunload();
}
if (this.localDatabase != null) {
this.localDatabase.onunload();
if (this.core.replicator) {
this.core.replicator?.closeReplication();
}
await this.localDatabase.close();
}
eventHub.emitEvent(EVENT_PLATFORM_UNLOADED);
eventHub.offAll();
this._log($msg("moduleLiveSyncMain.logUnloadingPlugin"));
return;
}
// async _onLiveSyncUnload(): Promise<void> {
// eventHub.emitEvent(EVENT_PLUGIN_UNLOADED);
// await this.services.appLifecycle.onBeforeUnload();
// cancelAllPeriodicTask();
// cancelAllTasks();
// stopAllRunningProcessors();
// await this.services.appLifecycle.onUnload();
// this._unloaded = true;
// for (const addOn of this.core.addOns) {
// addOn.onunload();
// }
// if (this.localDatabase != null) {
// this.localDatabase.onunload();
// if (this.core.replicator) {
// this.core.replicator?.closeReplication();
// }
// await this.localDatabase.close();
// }
// eventHub.emitEvent(EVENT_PLATFORM_UNLOADED);
// eventHub.offAll();
// this._log($msg("moduleLiveSyncMain.logUnloadingPlugin"));
// return;
// }
private async _realizeSettingSyncMode(): Promise<void> {
await this.services.appLifecycle.onSuspending();
await this.services.setting.onBeforeRealiseSetting();
this.localDatabase.refreshSettings();
await this.services.fileProcessing.commitPendingFileEvents();
await this.services.setting.onRealiseSetting();
// disable all sync temporary.
if (this.services.appLifecycle.isSuspended()) return;
await this.services.appLifecycle.onResuming();
await this.services.appLifecycle.onResumed();
await this.services.setting.onSettingRealised();
return;
}
// private async _realizeSettingSyncMode(): Promise<void> {
// await this.services.appLifecycle.onSuspending();
// await this.services.setting.onBeforeRealiseSetting();
// this.localDatabase.refreshSettings();
// await this.services.fileProcessing.commitPendingFileEvents();
// await this.services.setting.onRealiseSetting();
// // disable all sync temporary.
// if (this.services.appLifecycle.isSuspended()) return;
// await this.services.appLifecycle.onResuming();
// await this.services.appLifecycle.onResumed();
// await this.services.setting.onSettingRealised();
// return;
// }
_isReloadingScheduled(): boolean {
return this.core._totalProcessingCount !== undefined;
}
// isReady = false;
isReady = false;
// _isReady(): boolean {
// return this.isReady;
// }
_isReady(): boolean {
return this.isReady;
}
// _markIsReady(): void {
// this.isReady = true;
// }
_markIsReady(): void {
this.isReady = true;
}
// _resetIsReady(): void {
// this.isReady = false;
// }
_resetIsReady(): void {
this.isReady = false;
}
// _suspended = false;
// _isSuspended(): boolean {
// return this._suspended || !this.settings?.isConfigured;
// }
_suspended = false;
_isSuspended(): boolean {
return this._suspended || !this.settings?.isConfigured;
}
// _setSuspended(value: boolean) {
// this._suspended = value;
// }
_setSuspended(value: boolean) {
this._suspended = value;
}
// _unloaded = false;
// _isUnloaded(): boolean {
// return this._unloaded;
// }
_unloaded = false;
_isUnloaded(): boolean {
return this._unloaded;
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
super.onBindFunction(core, services);
services.appLifecycle.isSuspended.setHandler(this._isSuspended.bind(this));
services.appLifecycle.setSuspended.setHandler(this._setSuspended.bind(this));
services.appLifecycle.isReady.setHandler(this._isReady.bind(this));
services.appLifecycle.markIsReady.setHandler(this._markIsReady.bind(this));
services.appLifecycle.resetIsReady.setHandler(this._resetIsReady.bind(this));
services.appLifecycle.hasUnloaded.setHandler(this._isUnloaded.bind(this));
services.appLifecycle.isReloadingScheduled.setHandler(this._isReloadingScheduled.bind(this));
// services.appLifecycle.isSuspended.setHandler(this._isSuspended.bind(this));
// services.appLifecycle.setSuspended.setHandler(this._setSuspended.bind(this));
// services.appLifecycle.isReady.setHandler(this._isReady.bind(this));
// services.appLifecycle.markIsReady.setHandler(this._markIsReady.bind(this));
// services.appLifecycle.resetIsReady.setHandler(this._resetIsReady.bind(this));
// services.appLifecycle.hasUnloaded.setHandler(this._isUnloaded.bind(this));
services.appLifecycle.onReady.addHandler(this._onLiveSyncReady.bind(this));
services.appLifecycle.onWireUpEvents.addHandler(this._wireUpEvents.bind(this));
services.appLifecycle.onLoad.addHandler(this._onLiveSyncLoad.bind(this));
services.appLifecycle.onAppUnload.addHandler(this._onLiveSyncUnload.bind(this));
services.setting.realiseSetting.setHandler(this._realizeSettingSyncMode.bind(this));
}
}
+19 -3
View File
@@ -1,13 +1,20 @@
import { InjectableAPIService } from "@/lib/src/services/implements/injectable/InjectableAPIService";
import type { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext";
import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService";
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext";
import { Platform, type Command, type ViewCreator } from "obsidian";
import { ObsHttpHandler } from "../essentialObsidian/APILib/ObsHttpHandler";
import { ObsidianConfirm } from "./ObsidianConfirm";
import type { Confirm } from "@lib/interfaces/Confirm";
// All Services will be migrated to be based on Plain Services, not Injectable Services.
// This is a migration step.
export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceContext> {
_customHandler: ObsHttpHandler | undefined;
_confirmInstance: Confirm;
constructor(context: ObsidianServiceContext) {
super(context);
this._confirmInstance = new ObsidianConfirm(context);
}
getCustomFetchHandler(): ObsHttpHandler {
if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined);
return this._customHandler;
@@ -35,7 +42,7 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
return this.context.app;
}
getPlatform(): string {
override getPlatform(): string {
if (Platform.isAndroidApp) {
return "android-app";
} else if (Platform.isIosApp) {
@@ -63,6 +70,11 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
override getAppID(): string {
return `${"appId" in this.app ? this.app.appId : ""}`;
}
override getSystemVaultName(): string {
return this.app.vault.getName();
}
override getAppVersion(): string {
const navigatorString = globalThis.navigator?.userAgent ?? "";
const match = navigatorString.match(/obsidian\/([0-9]+\.[0-9]+\.[0-9]+)/);
@@ -76,6 +88,10 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
return this.context.plugin.manifest.version;
}
get confirm(): Confirm {
return this._confirmInstance;
}
addCommand<TCommand extends Command>(command: TCommand): TCommand {
return this.context.plugin.addCommand(command) as TCommand;
}
+34 -9
View File
@@ -1,6 +1,6 @@
import { InjectableServiceHub } from "@/lib/src/services/implements/injectable/InjectableServiceHub";
import { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext";
import type { ServiceInstances } from "@/lib/src/services/ServiceHub";
import { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
import { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext";
import type { ServiceInstances } from "@lib/services/ServiceHub";
import type ObsidianLiveSyncPlugin from "@/main";
import {
ObsidianConflictService,
@@ -8,13 +8,14 @@ import {
ObsidianReplicationService,
ObsidianReplicatorService,
ObsidianRemoteService,
ObsidianSettingService,
ObsidianTweakValueService,
ObsidianTestService,
ObsidianDatabaseEventService,
ObsidianConfigService,
ObsidianKeyValueDBService,
ObsidianControlService,
} from "./ObsidianServices";
import { ObsidianSettingService } from "./ObsidianSettingService";
import { ObsidianDatabaseService } from "./ObsidianDatabaseService";
import { ObsidianAPIService } from "./ObsidianAPIService";
import { ObsidianAppLifecycleService } from "./ObsidianAppLifecycleService";
@@ -29,16 +30,21 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
const context = new ObsidianServiceContext(plugin.app, plugin, plugin);
const API = new ObsidianAPIService(context);
const appLifecycle = new ObsidianAppLifecycleService(context);
const conflict = new ObsidianConflictService(context);
const fileProcessing = new ObsidianFileProcessingService(context);
const replication = new ObsidianReplicationService(context);
const remote = new ObsidianRemoteService(context);
const setting = new ObsidianSettingService(context);
const tweakValue = new ObsidianTweakValueService(context);
const setting = new ObsidianSettingService(context, {
APIService: API,
});
const appLifecycle = new ObsidianAppLifecycleService(context, {
settingService: setting,
});
const vault = new ObsidianVaultService(context, {
settingService: setting,
APIService: API,
});
const test = new ObsidianTestService(context);
const databaseEvents = new ObsidianDatabaseEventService(context);
@@ -56,7 +62,6 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
vault: vault,
});
const config = new ObsidianConfigService(context, {
vaultService: vault,
settingService: setting,
APIService: API,
});
@@ -65,12 +70,31 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
appLifecycleService: appLifecycle,
databaseEventService: databaseEvents,
});
const replication = new ObsidianReplicationService(context, {
APIService: API,
appLifecycleService: appLifecycle,
databaseEventService: databaseEvents,
replicatorService: replicator,
settingService: setting,
fileProcessingService: fileProcessing,
databaseService: database,
});
const control = new ObsidianControlService(context, {
appLifecycleService: appLifecycle,
databaseService: database,
fileProcessingService: fileProcessing,
settingService: setting,
APIService: API,
replicatorService: replicator,
});
const ui = new ObsidianUIService(context, {
appLifecycle,
config,
replicator,
APIService: API,
control: control,
});
// Using 'satisfies' to ensure all services are provided
const serviceInstancesToInit = {
appLifecycle: appLifecycle,
@@ -90,6 +114,7 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
API: API,
config: config,
keyValueDB: keyValueDB,
control: control,
} satisfies Required<ServiceInstances<ObsidianServiceContext>>;
super(context, serviceInstancesToInit);
+4 -4
View File
@@ -4,12 +4,12 @@ import { InjectableFileProcessingService } from "@lib/services/implements/inject
import { InjectableRemoteService } from "@lib/services/implements/injectable/InjectableRemoteService";
import { InjectableReplicationService } from "@lib/services/implements/injectable/InjectableReplicationService";
import { InjectableReplicatorService } from "@lib/services/implements/injectable/InjectableReplicatorService";
import { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
import { InjectableTestService } from "@lib/services/implements/injectable/InjectableTestService";
import { InjectableTweakValueService } from "@lib/services/implements/injectable/InjectableTweakValueService";
import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat";
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts";
import { KeyValueDBService } from "@/lib/src/services/base/KeyValueDBService";
import { KeyValueDBService } from "@lib/services/base/KeyValueDBService";
import { ControlService } from "@lib/services/base/ControlService";
export class ObsidianDatabaseEventService extends InjectableDatabaseEventService<ObsidianServiceContext> {}
@@ -23,8 +23,6 @@ export class ObsidianReplicationService extends InjectableReplicationService<Obs
export class ObsidianRemoteService extends InjectableRemoteService<ObsidianServiceContext> {}
// InjectableConflictService
export class ObsidianConflictService extends InjectableConflictService<ObsidianServiceContext> {}
// InjectableSettingService
export class ObsidianSettingService extends InjectableSettingService<ObsidianServiceContext> {}
// InjectableTweakValueService
export class ObsidianTweakValueService extends InjectableTweakValueService<ObsidianServiceContext> {}
// InjectableTestService
@@ -32,3 +30,5 @@ export class ObsidianTestService extends InjectableTestService<ObsidianServiceCo
export class ObsidianConfigService extends ConfigServiceBrowserCompat<ObsidianServiceContext> {}
export class ObsidianKeyValueDBService extends KeyValueDBService<ObsidianServiceContext> {}
export class ObsidianControlService extends ControlService<ObsidianServiceContext> {}
@@ -0,0 +1,35 @@
import { type ObsidianLiveSyncSettings } from "@lib/common/types";
import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
import { eventHub } from "@lib/hub/hub";
import { SettingService, type SettingServiceDependencies } from "@lib/services/base/SettingService";
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext";
export class ObsidianSettingService<T extends ObsidianServiceContext> extends SettingService<T> {
constructor(context: T, dependencies: SettingServiceDependencies) {
super(context, dependencies);
this.onSettingSaved.addHandler((settings) => {
eventHub.emitEvent(EVENT_SETTING_SAVED, settings);
return Promise.resolve(true);
});
this.onSettingLoaded.addHandler((settings) => {
eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB);
return Promise.resolve(true);
});
}
protected setItem(key: string, value: string) {
return localStorage.setItem(key, value);
}
protected getItem(key: string): string {
return localStorage.getItem(key) ?? "";
}
protected deleteItem(key: string): void {
localStorage.removeItem(key);
}
protected override async saveData(data: ObsidianLiveSyncSettings): Promise<void> {
return await this.context.liveSyncPlugin.saveData(data);
}
protected override async loadData(): Promise<ObsidianLiveSyncSettings | undefined> {
return await this.context.liveSyncPlugin.loadData();
}
}
+8 -5
View File
@@ -2,14 +2,16 @@ import type { ConfigService } from "@lib/services/base/ConfigService";
import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService";
import type { ReplicatorService } from "@lib/services/base/ReplicatorService";
import { UIService } from "@lib/services//implements/base/UIService";
import { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext";
import { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext";
import { ObsidianSvelteDialogManager } from "./SvelteDialogObsidian";
import { ObsidianConfirm } from "./ObsidianConfirm";
import DialogToCopy from "@/lib/src/UI/dialogues/DialogueToCopy.svelte";
import DialogToCopy from "@lib/UI/dialogues/DialogueToCopy.svelte";
import type { IAPIService, IControlService } from "@lib/services/base/IService";
export type ObsidianUIServiceDependencies<T extends ObsidianServiceContext = ObsidianServiceContext> = {
appLifecycle: AppLifecycleService<T>;
config: ConfigService<T>;
replicator: ReplicatorService<T>;
APIService: IAPIService;
control: IControlService;
};
export class ObsidianUIService extends UIService<ObsidianServiceContext> {
@@ -17,17 +19,18 @@ export class ObsidianUIService extends UIService<ObsidianServiceContext> {
return DialogToCopy;
}
constructor(context: ObsidianServiceContext, dependents: ObsidianUIServiceDependencies<ObsidianServiceContext>) {
const obsidianConfirm = new ObsidianConfirm(context);
const obsidianConfirm = dependents.APIService.confirm;
const obsidianSvelteDialogManager = new ObsidianSvelteDialogManager<ObsidianServiceContext>(context, {
appLifecycle: dependents.appLifecycle,
config: dependents.config,
replicator: dependents.replicator,
confirm: obsidianConfirm,
control: dependents.control,
});
super(context, {
appLifecycle: dependents.appLifecycle,
dialogManager: obsidianSvelteDialogManager,
confirm: obsidianConfirm,
APIService: dependents.APIService,
});
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ declare module "obsidian" {
// InjectableVaultService
export class ObsidianVaultService extends InjectableVaultService<ObsidianServiceContext> {
vaultName(): string {
override vaultName(): string {
return this.context.app.vault.getName();
}
getActiveFilePath(): FilePath | undefined {
+3
View File
@@ -0,0 +1,3 @@
import { enableI18nFeature } from "./onLayoutReady/enablei18n";
export const onLayoutReadyFeatures = [enableI18nFeature];
@@ -0,0 +1,41 @@
import { getLanguage } from "@/deps";
import { createServiceFeature } from "../types.ts";
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@lib/common/rosetta";
import { $msg, setLang } from "@lib/common/i18n";
export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API } }) => {
let isChanged = false;
const settings = setting.currentSettings();
if (settings.displayLanguage == "") {
const obsidianLanguage = getLanguage();
if (
SUPPORTED_I18N_LANGS.indexOf(obsidianLanguage) !== -1 && // Check if the language is supported
obsidianLanguage != settings.displayLanguage // Check if the language is different from the current setting
) {
// Check if the current setting is not empty (Means migrated or installed).
// settings.displayLanguage = obsidianLanguage as I18N_LANGS;
await setting.applyPartial({ displayLanguage: obsidianLanguage as I18N_LANGS });
isChanged = true;
setLang(settings.displayLanguage);
} else if (settings.displayLanguage == "") {
// settings.displayLanguage = "def";
await setting.applyPartial({ displayLanguage: "def" });
setLang(settings.displayLanguage);
await setting.saveSettingData();
}
}
if (isChanged) {
const revert = $msg("dialog.yourLanguageAvailable.btnRevertToDefault");
if (
(await API.confirm.askSelectStringDialogue($msg(`dialog.yourLanguageAvailable`), ["OK", revert], {
defaultAction: "OK",
title: $msg(`dialog.yourLanguageAvailable.Title`),
})) == revert
) {
await setting.applyPartial({ displayLanguage: "def" });
setLang(settings.displayLanguage);
}
await setting.saveSettingData();
}
return true;
});
+78
View File
@@ -0,0 +1,78 @@
import type { IServiceHub } from "@lib/services/base/IService";
import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess";
import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder";
import type { IFileHandler } from "@lib/interfaces/FileHandler";
import type { StorageAccess } from "@lib/interfaces/StorageAccess";
import type { LogFunction } from "@/lib/src/services/lib/logUtils";
export interface ServiceModules {
storageAccess: StorageAccess;
/**
* Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc.
*/
databaseFileAccess: DatabaseFileAccess;
/**
* File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc.
*/
fileHandler: IFileHandler;
/**
* Rebuilder for handling database rebuilding operations.
*/
rebuilder: Rebuilder;
}
export type RequiredServices<T extends keyof IServiceHub> = Pick<IServiceHub, T>;
export type RequiredServiceModules<T extends keyof ServiceModules> = Pick<ServiceModules, T>;
export type NecessaryServices<T extends keyof IServiceHub, U extends keyof ServiceModules> = {
services: RequiredServices<T>;
serviceModules: RequiredServiceModules<U>;
};
export type ServiceFeatureFunction<T extends keyof IServiceHub, U extends keyof ServiceModules, TR> = (
host: NecessaryServices<T, U>
) => TR;
type ServiceFeatureContext<T> = T & {
_log: LogFunction;
};
export type ServiceFeatureFunctionWithContext<T extends keyof IServiceHub, U extends keyof ServiceModules, C, TR> = (
host: NecessaryServices<T, U>,
context: ServiceFeatureContext<C>
) => TR;
/**
* Helper function to create a service feature with proper typing.
* @param featureFunction The feature function to be wrapped.
* @returns The same feature function with proper typing.
* @example
* const myFeatureDef = createServiceFeature(({ services: { API }, serviceModules: { storageAccess } }) => {
* // ...
* });
* const myFeature = myFeatureDef.bind(null, this); // <- `this` may `ObsidianLiveSyncPlugin` or a custom context object
* appLifecycle.onLayoutReady(myFeature);
*/
export function createServiceFeature<T extends keyof IServiceHub, U extends keyof ServiceModules, TR>(
featureFunction: ServiceFeatureFunction<T, U, TR>
): ServiceFeatureFunction<T, U, TR> {
return featureFunction;
}
type ContextFactory<T extends keyof IServiceHub, U extends keyof ServiceModules, C> = (
host: NecessaryServices<T, U>
) => ServiceFeatureContext<C>;
export function serviceFeature<T extends keyof IServiceHub, U extends keyof ServiceModules>() {
return {
create<TR>(featureFunction: ServiceFeatureFunction<T, U, TR>) {
return featureFunction;
},
withContext<C extends object = object>(ContextFactory: ContextFactory<T, U, C>) {
return {
create:
<TR>(featureFunction: ServiceFeatureFunctionWithContext<T, U, C, TR>) =>
(host: NecessaryServices<T, U>, context: ServiceFeatureContext<C>) =>
featureFunction(host, ContextFactory(host)),
};
},
};
}
+160
View File
@@ -0,0 +1,160 @@
import { markChangesAreSame } from "@/common/utils";
import type { FilePath, UXDataWriteOptions, UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
import { TFolder, type TAbstractFile, TFile, type Stat, type App, type DataWriteOptions, normalizePath } from "@/deps";
import { FileAccessBase, toArrayBuffer, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts";
import { TFileToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
declare module "obsidian" {
interface Vault {
getAbstractFileByPathInsensitive(path: string): TAbstractFile | null;
}
interface DataAdapter {
reconcileInternalFile?(path: string): Promise<void>;
}
}
export class FileAccessObsidian extends FileAccessBase<TAbstractFile, TFile, TFolder, Stat> {
app: App;
override getPath(file: string | TAbstractFile): FilePath {
return (typeof file === "string" ? file : file.path) as FilePath;
}
override isFile(file: TAbstractFile | null): file is TFile {
return file instanceof TFile;
}
override isFolder(file: TAbstractFile | null): file is TFolder {
return file instanceof TFolder;
}
override _statFromNative(file: TFile): Promise<TFile["stat"]> {
return Promise.resolve(file.stat);
}
override nativeFileToUXFileInfoStub(file: TFile): UXFileInfoStub {
return TFileToUXFileInfoStub(file);
}
override nativeFolderToUXFolder(folder: TFolder): UXFolderInfo {
if (folder instanceof TFolder) {
return this.nativeFolderToUXFolder(folder);
} else {
throw new Error(`Not a folder: ${(folder as TAbstractFile)?.name}`);
}
}
constructor(app: App, dependencies: FileAccessBaseDependencies) {
super({
storageAccessManager: dependencies.storageAccessManager,
vaultService: dependencies.vaultService,
settingService: dependencies.settingService,
APIService: dependencies.APIService,
});
this.app = app;
}
protected override _normalisePath(path: string): string {
return normalizePath(path);
}
protected async _adapterMkdir(path: string) {
await this.app.vault.adapter.mkdir(path);
}
protected _getAbstractFileByPath(path: FilePath) {
return this.app.vault.getAbstractFileByPath(path);
}
protected _getAbstractFileByPathInsensitive(path: FilePath) {
return this.app.vault.getAbstractFileByPathInsensitive(path);
}
protected async _tryAdapterStat(path: FilePath) {
if (!(await this.app.vault.adapter.exists(path))) return null;
return await this.app.vault.adapter.stat(path);
}
protected async _adapterStat(path: FilePath) {
return await this.app.vault.adapter.stat(path);
}
protected async _adapterExists(path: FilePath) {
return await this.app.vault.adapter.exists(path);
}
protected async _adapterRemove(path: FilePath) {
await this.app.vault.adapter.remove(path);
}
protected async _adapterRead(path: FilePath) {
return await this.app.vault.adapter.read(path);
}
protected async _adapterReadBinary(path: FilePath) {
return await this.app.vault.adapter.readBinary(path);
}
_adapterWrite(file: string, data: string, options?: UXDataWriteOptions): Promise<void> {
return this.app.vault.adapter.write(file, data, options);
}
_adapterWriteBinary(file: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
return this.app.vault.adapter.writeBinary(file, toArrayBuffer(data), options);
}
protected _adapterList(basePath: string): Promise<{ files: string[]; folders: string[] }> {
return Promise.resolve(this.app.vault.adapter.list(basePath));
}
async _vaultCacheRead(file: TFile) {
return await this.app.vault.cachedRead(file);
}
protected async _vaultRead(file: TFile): Promise<string> {
return await this.app.vault.read(file);
}
protected async _vaultReadBinary(file: TFile): Promise<ArrayBuffer> {
return await this.app.vault.readBinary(file);
}
protected override markChangesAreSame(path: string, mtime: number, newMtime: number) {
return markChangesAreSame(path, mtime, newMtime);
}
protected override async _vaultModify(file: TFile, data: string, options?: UXDataWriteOptions): Promise<void> {
return await this.app.vault.modify(file, data, options);
}
protected override async _vaultModifyBinary(
file: TFile,
data: ArrayBuffer,
options?: UXDataWriteOptions
): Promise<void> {
return await this.app.vault.modifyBinary(file, toArrayBuffer(data), options);
}
protected override async _vaultCreate(path: string, data: string, options?: UXDataWriteOptions): Promise<TFile> {
return await this.app.vault.create(path, data, options);
}
protected override async _vaultCreateBinary(
path: string,
data: ArrayBuffer,
options?: UXDataWriteOptions
): Promise<TFile> {
return await this.app.vault.createBinary(path, toArrayBuffer(data), options);
}
protected override _trigger(name: string, ...data: any[]) {
return this.app.vault.trigger(name, ...data);
}
protected override async _reconcileInternalFile(path: string) {
return await Promise.resolve(this.app.vault.adapter.reconcileInternalFile?.(path));
}
protected override async _adapterAppend(normalizedPath: string, data: string, options?: DataWriteOptions) {
return await this.app.vault.adapter.append(normalizedPath, data, options);
}
protected override async _delete(file: TFile | TFolder, force = false) {
return await this.app.vault.delete(file, force);
}
protected override async _trash(file: TFile | TFolder, force = false) {
return await this.app.vault.trash(file, force);
}
protected override _getFiles() {
return this.app.vault.getFiles();
}
}
@@ -0,0 +1,6 @@
import type { TAbstractFile, TFile, TFolder, Stat } from "@/deps";
import { ServiceFileAccessBase } from "@lib/serviceModules/ServiceFileAccessBase";
// For typechecking purpose
export class ServiceFileAccessObsidian extends ServiceFileAccessBase<TAbstractFile, TFile, TFolder, Stat> {}
@@ -1,363 +0,0 @@
import { TFile, TFolder, type ListedFiles } from "@/deps.ts";
import { LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import type {
FilePath,
FilePathWithPrefix,
UXDataWriteOptions,
UXFileInfo,
UXFileInfoStub,
UXFolderInfo,
UXStat,
} from "@lib/common/types";
import { ServiceModuleBase } from "@lib/serviceModules/ServiceModuleBase";
import type { APIService } from "@lib/services/base/APIService";
import type { IStorageAccessManager, StorageAccess } from "@lib/interfaces/StorageAccess.ts";
import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService";
import type { FileProcessingService } from "@lib/services/base/FileProcessingService";
import { ObsidianFileAccess } from "@/modules/coreObsidian/storageLib/SerializedFileAccess";
import { StorageEventManager } from "@lib/interfaces/StorageEventManager.ts";
import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
import { createBlob, type CustomRegExp } from "@lib/common/utils";
import type { VaultService } from "@lib/services/base/VaultService";
import type { SettingService } from "@lib/services/base/SettingService";
export interface StorageAccessObsidianDependencies {
API: APIService;
appLifecycle: AppLifecycleService;
fileProcessing: FileProcessingService;
vault: VaultService;
setting: SettingService;
storageEventManager: StorageEventManager;
storageAccessManager: IStorageAccessManager;
vaultAccess: ObsidianFileAccess;
}
export class ServiceFileAccessObsidian
extends ServiceModuleBase<StorageAccessObsidianDependencies>
implements StorageAccess
{
private vaultAccess: ObsidianFileAccess;
private vaultManager: StorageEventManager;
private vault: VaultService;
private setting: SettingService;
constructor(services: StorageAccessObsidianDependencies) {
super(services);
// this.appLifecycle = services.appLifecycle;
this.vault = services.vault;
this.setting = services.setting;
this.vaultManager = services.storageEventManager;
this.vaultAccess = services.vaultAccess;
services.appLifecycle.onFirstInitialise.addHandler(this._everyOnFirstInitialize.bind(this));
services.fileProcessing.commitPendingFileEvents.addHandler(this._everyCommitPendingFileEvent.bind(this));
}
restoreState() {
return this.vaultManager.restoreState();
}
async _everyOnFirstInitialize(): Promise<boolean> {
await this.vaultManager.beginWatch();
return Promise.resolve(true);
}
async _everyCommitPendingFileEvent(): Promise<boolean> {
await this.vaultManager.waitForIdle();
return Promise.resolve(true);
}
async writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<boolean> {
const file = this.vaultAccess.getAbstractFileByPath(path);
if (file instanceof TFile) {
return this.vaultAccess.vaultModify(file, data, opt);
} else if (file === null) {
if (!path.endsWith(".md")) {
// Very rare case, we encountered this case with `writing-goals-history.csv` file.
// Indeed, that file not appears in the File Explorer, but it exists in the vault.
// Hence, we cannot retrieve the file from the vault by getAbstractFileByPath, and we cannot write it via vaultModify.
// It makes `File already exists` error.
// Therefore, we need to write it via adapterWrite.
// Maybe there are others like this, so I will write it via adapterWrite.
// This is a workaround for the issue, but I don't know if this is the right solution.
// (So limits to non-md files).
// Has Obsidian been patched?, anyway, writing directly might be a safer approach.
// However, does changes of that file trigger file-change event?
await this.vaultAccess.adapterWrite(path, data, opt);
// For safety, check existence
return await this.vaultAccess.adapterExists(path);
} else {
return (await this.vaultAccess.vaultCreate(path, data, opt)) instanceof TFile;
}
} else {
this._log(`Could not write file (Possibly already exists as a folder): ${path}`, LOG_LEVEL_VERBOSE);
return false;
}
}
readFileAuto(path: string): Promise<string | ArrayBuffer> {
const file = this.vaultAccess.getAbstractFileByPath(path);
if (file instanceof TFile) {
return this.vaultAccess.vaultRead(file);
} else {
throw new Error(`Could not read file (Possibly does not exist): ${path}`);
}
}
readFileText(path: string): Promise<string> {
const file = this.vaultAccess.getAbstractFileByPath(path);
if (file instanceof TFile) {
return this.vaultAccess.vaultRead(file);
} else {
throw new Error(`Could not read file (Possibly does not exist): ${path}`);
}
}
isExists(path: string): Promise<boolean> {
return Promise.resolve(this.vaultAccess.getAbstractFileByPath(path) instanceof TFile);
}
async writeHiddenFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<boolean> {
try {
await this.vaultAccess.adapterWrite(path, data, opt);
return true;
} catch (e) {
this._log(`Could not write hidden file: ${path}`, LOG_LEVEL_VERBOSE);
this._log(e, LOG_LEVEL_VERBOSE);
return false;
}
}
async appendHiddenFile(path: string, data: string, opt?: UXDataWriteOptions): Promise<boolean> {
try {
await this.vaultAccess.adapterAppend(path, data, opt);
return true;
} catch (e) {
this._log(`Could not append hidden file: ${path}`, LOG_LEVEL_VERBOSE);
this._log(e, LOG_LEVEL_VERBOSE);
return false;
}
}
stat(path: string): Promise<UXStat | null> {
const file = this.vaultAccess.getAbstractFileByPath(path);
if (file === null) return Promise.resolve(null);
if (file instanceof TFile) {
return Promise.resolve({
ctime: file.stat.ctime,
mtime: file.stat.mtime,
size: file.stat.size,
type: "file",
});
} else {
throw new Error(`Could not stat file (Possibly does not exist): ${path}`);
}
}
statHidden(path: string): Promise<UXStat | null> {
return this.vaultAccess.tryAdapterStat(path);
}
async removeHidden(path: string): Promise<boolean> {
try {
await this.vaultAccess.adapterRemove(path);
if (this.vaultAccess.tryAdapterStat(path) !== null) {
return false;
}
return true;
} catch (e) {
this._log(`Could not remove hidden file: ${path}`, LOG_LEVEL_VERBOSE);
this._log(e, LOG_LEVEL_VERBOSE);
return false;
}
}
async readHiddenFileAuto(path: string): Promise<string | ArrayBuffer> {
return await this.vaultAccess.adapterReadAuto(path);
}
async readHiddenFileText(path: string): Promise<string> {
return await this.vaultAccess.adapterRead(path);
}
async readHiddenFileBinary(path: string): Promise<ArrayBuffer> {
return await this.vaultAccess.adapterReadBinary(path);
}
async isExistsIncludeHidden(path: string): Promise<boolean> {
return (await this.vaultAccess.tryAdapterStat(path)) !== null;
}
async ensureDir(path: string): Promise<boolean> {
try {
await this.vaultAccess.ensureDirectory(path);
return true;
} catch (e) {
this._log(`Could not ensure directory: ${path}`, LOG_LEVEL_VERBOSE);
this._log(e, LOG_LEVEL_VERBOSE);
return false;
}
}
triggerFileEvent(event: string, path: string): void {
const file = this.vaultAccess.getAbstractFileByPath(path);
if (file === null) return;
this.vaultAccess.trigger(event, file);
}
async triggerHiddenFile(path: string): Promise<void> {
await this.vaultAccess.reconcileInternalFile(path);
}
// getFileStub(file: TFile): UXFileInfoStub {
// return TFileToUXFileInfoStub(file);
// }
getFileStub(path: string): UXFileInfoStub | null {
const file = this.vaultAccess.getAbstractFileByPath(path);
if (file instanceof TFile) {
return TFileToUXFileInfoStub(file);
} else {
return null;
}
}
async readStubContent(stub: UXFileInfoStub): Promise<UXFileInfo | false> {
const file = this.vaultAccess.getAbstractFileByPath(stub.path);
if (!(file instanceof TFile)) {
this._log(`Could not read file (Possibly does not exist or a folder): ${stub.path}`, LOG_LEVEL_VERBOSE);
return false;
}
const data = await this.vaultAccess.vaultReadAuto(file);
return {
...stub,
...TFileToUXFileInfoStub(file),
body: createBlob(data),
};
}
getStub(path: string): UXFileInfoStub | UXFolderInfo | null {
const file = this.vaultAccess.getAbstractFileByPath(path);
if (file instanceof TFile) {
return TFileToUXFileInfoStub(file);
} else if (file instanceof TFolder) {
return TFolderToUXFileInfoStub(file);
}
return null;
}
getFiles(): UXFileInfoStub[] {
return this.vaultAccess.getFiles().map((f) => TFileToUXFileInfoStub(f));
}
getFileNames(): FilePath[] {
return this.vaultAccess.getFiles().map((f) => f.path as FilePath);
}
async getFilesIncludeHidden(
basePath: string,
includeFilter?: CustomRegExp[],
excludeFilter?: CustomRegExp[],
skipFolder: string[] = [".git", ".trash", "node_modules"]
): Promise<FilePath[]> {
let w: ListedFiles;
try {
w = await this.vaultAccess.adapterList(basePath);
// w = await this.plugin.app.vault.adapter.list(basePath);
} catch (ex) {
this._log(`Could not traverse(getFilesIncludeHidden):${basePath}`, LOG_LEVEL_INFO);
this._log(ex, LOG_LEVEL_VERBOSE);
return [];
}
skipFolder = skipFolder.map((e) => e.toLowerCase());
let files = [] as string[];
for (const file of w.files) {
if (includeFilter && includeFilter.length > 0) {
if (!includeFilter.some((e) => e.test(file))) continue;
}
if (excludeFilter && excludeFilter.some((ee) => ee.test(file))) {
continue;
}
if (await this.vault.isIgnoredByIgnoreFile(file)) continue;
files.push(file);
}
for (const v of w.folders) {
const folderName = (v.split("/").pop() ?? "").toLowerCase();
if (skipFolder.some((e) => folderName === e)) {
continue;
}
if (excludeFilter && excludeFilter.some((e) => e.test(v))) {
continue;
}
if (await this.vault.isIgnoredByIgnoreFile(v)) {
continue;
}
// OK, deep dive!
files = files.concat(await this.getFilesIncludeHidden(v, includeFilter, excludeFilter, skipFolder));
}
return files as FilePath[];
}
async touched(file: UXFileInfoStub | FilePathWithPrefix): Promise<void> {
const path = typeof file === "string" ? file : file.path;
await this.vaultAccess.touch(path as FilePath);
}
recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean {
const xFile = typeof file === "string" ? (this.vaultAccess.getAbstractFileByPath(file) as TFile) : file;
if (xFile === null) return false;
if (xFile instanceof TFolder) return false;
return this.vaultAccess.recentlyTouched(xFile);
}
clearTouched(): void {
this.vaultAccess.clearTouched();
}
delete(file: FilePathWithPrefix | UXFileInfoStub | string, force: boolean): Promise<void> {
const xPath = typeof file === "string" ? file : file.path;
const xFile = this.vaultAccess.getAbstractFileByPath(xPath);
if (xFile === null) return Promise.resolve();
if (!(xFile instanceof TFile) && !(xFile instanceof TFolder)) return Promise.resolve();
return this.vaultAccess.delete(xFile, force);
}
trash(file: FilePathWithPrefix | UXFileInfoStub | string, system: boolean): Promise<void> {
const xPath = typeof file === "string" ? file : file.path;
const xFile = this.vaultAccess.getAbstractFileByPath(xPath);
if (xFile === null) return Promise.resolve();
if (!(xFile instanceof TFile) && !(xFile instanceof TFolder)) return Promise.resolve();
return this.vaultAccess.trash(xFile, system);
}
// $readFileBinary(path: string): Promise<ArrayBuffer> {
// const file = this.vaultAccess.getAbstractFileByPath(path);
// if (file instanceof TFile) {
// return this.vaultAccess.vaultReadBinary(file);
// } else {
// throw new Error(`Could not read file (Possibly does not exist): ${path}`);
// }
// }
// async $appendFileAuto(path: string, data: string | ArrayBuffer, opt?: DataWriteOptions): Promise<boolean> {
// const file = this.vaultAccess.getAbstractFileByPath(path);
// if (file instanceof TFile) {
// return this.vaultAccess.a(file, data, opt);
// } else if (file !== null) {
// return await this.vaultAccess.vaultCreate(path, data, opt) instanceof TFile;
// } else {
// this._log(`Could not append file (Possibly already exists as a folder): ${path}`, LOG_LEVEL_VERBOSE);
// return false;
// }
// }
async __deleteVaultItem(file: TFile | TFolder) {
if (file instanceof TFile) {
if (!(await this.vault.isTargetFile(file.path))) return;
}
const dir = file.parent;
const settings = this.setting.currentSettings();
if (settings.trashInsteadDelete) {
await this.vaultAccess.trash(file, false);
} else {
await this.vaultAccess.delete(file, true);
}
this._log(`xxx <- STORAGE (deleted) ${file.path}`);
if (dir) {
this._log(`files: ${dir.children.length}`);
if (dir.children.length == 0) {
if (!settings.doNotDeleteFolder) {
this._log(
`All files under the parent directory (${dir.path}) have been deleted, so delete this one.`
);
await this.__deleteVaultItem(dir);
}
}
}
}
async deleteVaultItem(fileSrc: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise<void> {
const path = typeof fileSrc === "string" ? fileSrc : fileSrc.path;
const file = this.vaultAccess.getAbstractFileByPath(path);
if (file === null) return;
if (file instanceof TFile || file instanceof TFolder) {
return await this.__deleteVaultItem(file);
}
}
}
+27
View File
@@ -0,0 +1,27 @@
import type { DatabaseFileAccess } from "@/lib/src/interfaces/DatabaseFileAccess";
import type { Rebuilder } from "@/lib/src/interfaces/DatabaseRebuilder";
import type { IFileHandler } from "@/lib/src/interfaces/FileHandler";
import type { StorageAccess } from "@/lib/src/interfaces/StorageAccess";
import type { IServiceHub } from "./lib/src/services/base/IService";
export interface ServiceModules {
storageAccess: StorageAccess;
/**
* Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc.
*/
databaseFileAccess: DatabaseFileAccess;
/**
* File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc.
*/
fileHandler: IFileHandler;
/**
* Rebuilder for handling database rebuilding operations.
*/
rebuilder: Rebuilder;
}
export interface LiveSyncHost {
services: IServiceHub;
serviceModules: ServiceModules;
}
+49
View File
@@ -0,0 +1,49 @@
import type { TerserOptions } from "vite";
export const terserOption: TerserOptions = {
mangle: {
// properties: {
// regex: /^_p_/,
// },
eval: true,
keep_classnames: true,
keep_fnames: true,
// module: true,
// safari10: true,
// toplevel: true,
},
// mangle: false,
compress: {
defaults: false,
arguments: true,
// drop_console: false,
ecma: 2020,
// keep_classnames: true,
// keep_fnames: false,
// module: true,
passes: 4,
// arrows: true,
// collapse_vars: true,
// comparisons: true,
// computed_props: true,
// conditionals: true,
dead_code: true,
evaluate: true,
// hoist_funs: true,
// hoist_props: true,
// hoist_vars: false,
// if_return: true,
inline: true,
// join_vars: true,
// reduce_funcs: true,
// reduce_vars: true,
// sequences: true,
// side_effects: false,
},
format: {
// beautify: true,
ecma: 2020,
safari10: true,
webkit: true,
}
}
+7 -9
View File
@@ -122,13 +122,11 @@ export async function waitForIdle(harness: LiveSyncHarness): Promise<void> {
for (let i = 0; i < 20; i++) {
await delay(25);
const processing =
harness.plugin.databaseQueueCount.value +
harness.plugin.processingFileEventCount.value +
harness.plugin.pendingFileEventCount.value +
harness.plugin.totalQueued.value +
harness.plugin.batched.value +
harness.plugin.processing.value +
harness.plugin.storageApplyingCount.value;
harness.plugin.services.replication.databaseQueueCount.value +
harness.plugin.services.fileProcessing.totalQueued.value +
harness.plugin.services.fileProcessing.batched.value +
harness.plugin.services.fileProcessing.processing.value +
harness.plugin.services.replication.storageApplyingCount.value;
if (processing === 0) {
if (i > 0) {
@@ -141,8 +139,8 @@ export async function waitForIdle(harness: LiveSyncHarness): Promise<void> {
export async function waitForClosed(harness: LiveSyncHarness): Promise<void> {
await delay(100);
for (let i = 0; i < 10; i++) {
if (harness.plugin.services.appLifecycle.hasUnloaded()) {
console.log("App Lifecycle has unloaded");
if (harness.plugin.services.control.hasUnloaded()) {
console.log("App has unloaded");
return;
}
await delay(100);
+1
View File
@@ -14,6 +14,7 @@
"alwaysStrict": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noImplicitOverride": true,
"noEmit": true,
"lib": ["es2018", "DOM", "ES5", "ES6", "ES7", "es2019.array", "ES2021.WeakRef", "ES2020.BigInt", "ESNext.Intl"],
"strictBindCallApply": true,
+95
View File
@@ -3,6 +3,101 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
## 0.25.43-patched-9 a.ka. 0.25.44-rc1
We are finally ready for release. I think I will go ahead and release it after using it for a few days.
### Fixed
- Hidden file synchronisation now works!
- Now Hidden file synchronisation respects `.ignore` files.
- Replicator initialisation during rebuilding now works correctly.
### Refactored
- Some methods naming have been changed for better clarity, i.e., `_isTargetFileByLocalDB` is now `_isTargetAcceptedByLocalDB`.
### Follow-up tasks memo (After 0.25.44)
Going forward, functionality that does not span multiple events is expected to be implemented as middleware-style functions rather than modules based on classes.
Consequently, the existing modules will likely be gradually dismantled.
For reference, `ModuleReplicator.ts` has extracted several functionalities as functions.
However, this does not negate object-oriented design. Where lifecycles and state are present, and the Liskov Substitution Principle can be upheld, we design using classes. After all, a visible state is preferable to a hidden state. In other words, the handler still accepts both functions and member methods, so formally there is no change.
As undertaking this for everything would be a bit longer task, I intend to release it at this stage.
Note: I left using `setHandler`s that as a mark of `need to be refactored`. Basically, they should be implemented in the service itself. That because it is just only a mis-designed separated implementation.
## 0.25.43-patched-8
I really must thank you all. You know that it seems we have just a little more to do.
Note: This version is not fully tested yet. Be careful to use this. Very dogfood-y one.
### Fixed
- Now the device name is saved correctly.
### Refactored
- Add `override` keyword to all overridden items.
- More dynamic binding has been removed.
- The number of inverted dependencies has decreased much more.
- Some check-logic; i.e., like pre-replication check is now separated into check functions and added to the service as handlers, layered.
- This may help with better testing and better maintainability.
## 0.25.43-patched-7
19th February, 2026
Right then, let us make a decision already.
Last time, since I found a bug, I ended up doing a few other things as well, but next time I intend to release it with just the bug fix. It is quite substantial, after all.
Customisation Sync has mostly been verified. Hidden file synchronisation has not been done yet.
Vite's build system is not in the production. However, I possibly migrate to it in the future.
And, the `daily-progress` will be tidied on releasing 0.25.44. Do not worry!
### Fixed
- Fixed an issue where the StorageEventManager was not correctly loading the settings.
- Replication statistics are now correctly reset after switching replicators.
### Refactored
- Now, many reactive values which keep the state or statistics of the plugin are moved to the services which have the responsibility for these states.
- `serviceFeatures` are now able to be added to the services; this is not a class module, but a function which accepts dependencies and returns an addHandler-able function. This is for better separation of concerns, better maintainability, and testability.
- `control` service; is a meta-service which is responsible for orchestrating services has been added.
- Don't you think stopping replication or something occurs during `settingService.realiseSetting` is quite weird? It may be done by the control service, which can orchestrate the setting service and the replicator service.
-
- Some functions on services have been moved. e.g., `getSystemVaultName` is now on the API service.
- Setting Service is now responsible for the setting, no longer using dynamic binding for the modules.
## 0.25.43-patched-6
18th February, 2026
Let me confess that I have lied about `now all ambiguous properties`... I have found some more implicit calling.
Note: I have not checked hidden file sync and customisation sync yet. Please report if you find any unexpected behaviour in these features.
### Fixed
- Now ReplicatorService responds to database reset and database initialisation events to dispose of the active replicator.
- Fixes some unlocking issues during rebuilding.
### Refactored
- Now `StorageEventManagerBase` is separated from `StorageEventManagerObsidian` following their concerns.
- No longer using `ObsidianFileAccess` indirectly during checking duplicated-file events.
- Last event memorisation is now moved into the StorageAccessManager, just like the file processing interlocking.
- These methods, i.e., `ObsidianFileAccess.touch`. `StorageEventManager.recentlyTouched`, and `StorageEventManager.touch` are still available, but simply call the StorageAccessManager's methods.
- Now `FileAccessBase` is separated from `FileAccessObsidian` following their concerns.
## 0.25.43-patched-5
17th February, 2026
+163 -3
View File
@@ -1,4 +1,164 @@
import { defineConfig, mergeConfig } from "vitest/config";
import viteConfig from "./vitest.config.common";
import { defineConfig } from "vitest/config";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { sveltePreprocess } from "svelte-preprocess";
import inlineWorkerPlugin from "esbuild-plugin-inline-worker";
import copy from "rollup-plugin-copy";
import path from "path";
import { fileURLToPath } from "node:url";
import fs from "node:fs";
import { platform } from "node:process";
export default mergeConfig(viteConfig, defineConfig({}));
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + "");
const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
// const moduleAliasPlugin = {
// name: "module-alias",
// setup(build: any) {
// build.onResolve({ filter: /.(dev)(.ts|)$/ }, (args: any) => {
// // console.log(args.path);
// if (prod) {
// const prodTs = args.path.replace(".dev", ".prod");
// const statFile = prodTs.endsWith(".ts") ? prodTs : prodTs + ".ts";
// const realPath = path.join(args.resolveDir, statFile);
// console.log(`Checking ${statFile}`);
// if (fs.existsSync(realPath)) {
// console.log(`Replaced ${args.path} with ${prodTs}`);
// return {
// path: realPath,
// namespace: "file",
// };
// }
// }
// return null;
// });
// build.onResolve({ filter: /.(platform)(.ts|)$/ }, (args: any) => {
// // console.log(args.path);
// if (prod) {
// const prodTs = args.path.replace(".platform", ".obsidian");
// const statFile = prodTs.endsWith(".ts") ? prodTs : prodTs + ".ts";
// const realPath = path.join(args.resolveDir, statFile);
// console.log(`Checking ${statFile}`);
// if (fs.existsSync(realPath)) {
// console.log(`Replaced ${args.path} with ${prodTs}`);
// return {
// path: realPath,
// namespace: "file",
// };
// }
// }
// return null;
// });
// },
// };
const externals = [
"obsidian",
"electron",
"crypto",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
];
const define = {
MANIFEST_VERSION: `"${manifestJson.version}"`,
PACKAGE_VERSION: `"${packageJson.version}"`,
UPDATE_INFO: `${updateInfo}`,
global: "globalThis",
hostPlatform: `"${platform}"`,
};
const PATHS_TEST_INSTALL = process.env?.PATHS_TEST_INSTALL || "";
const PATH_TEST_INSTALL = PATHS_TEST_INSTALL.split(path.delimiter)
.map((p) => p.trim())
.filter((p) => p.length);
if (PATH_TEST_INSTALL) {
console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`);
} else {
console.log(
"Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows)."
);
}
import { terserOption } from "./terser_vite.config";
export default defineConfig(({ mode }) => {
const prod = mode === "production" || mode === "original";
let minify = prod ? "terser" : false;
let outFile = `main_vite.${prod ? "prod" : "dev"}.js`;
if (mode == "original") {
console.log("Building original unminified version");
minify = false;
outFile = `main_vite.original.js`;
}
outFile = `main.js`;
return {
plugins: [
// moduleAliasPlugin,
inlineWorkerPlugin({
external: externals,
treeShaking: true,
}),
svelte({
preprocess: sveltePreprocess(),
compilerOptions: { css: "injected", preserveComments: false },
}),
copy({
targets: ["manifest.json", "main.js", "styles.css"]
.map((file) => PATH_TEST_INSTALL.map((dest) => ({ src: file, dest: dest })))
.flat(),
// Copy after the build is complete
hook: "writeBundle",
verbose: true,
}),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@lib": path.resolve(__dirname, "./src/lib/src"),
src: path.resolve(__dirname, "./src"),
},
},
build: {
target: 'es2018',
commonjsOptions: {},
lib: {
entry: path.resolve(__dirname, "src/main.ts"),
name: "main",
fileName: () => outFile,
formats: ["cjs"], //
},
rollupOptions: {
external: externals,
output: {
globals: {
obsidian: "obsidian",
electron: "electron",
},
entryFileNames: outFile,
inlineDynamicImports: true,
manualChunks: undefined,
},
},
minify: minify ? "terser" : false,
// minify:false,
terserOptions: terserOption,
outDir: ".",
emptyOutDir: false,
sourcemap: prod ? false : "hidden",
},
define: define,
worker: {
format: "iife",
},
}
})