mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-02-25 05:28:47 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55545da45f | ||
|
|
96165b4f9b | ||
|
|
abe613539b |
@@ -23,7 +23,7 @@ The Database name to synchronize.
|
||||
## Local Database Configurations
|
||||
"Local Database" is created inside your obsidian.
|
||||
|
||||
### Batch database update (beta)
|
||||
### Batch database update
|
||||
Delay database update until raise replication, open another file, window visibility changed, or file events except for file modification.
|
||||
This option can not be used with LiveSync at the same time.
|
||||
|
||||
@@ -78,6 +78,27 @@ When running these operations, every synchronization settings is disabled.
|
||||
|
||||
**And even your passphrase is wrong, It doesn't be checked before the plugin really decrypts. So If you set the wrong passphrase and run "Apply and Receive", you will get an amount of decryption error. But, this is the specification.**
|
||||
|
||||
### minimum chunk size and LongLine threshold
|
||||
The configuration of chunk splitting.
|
||||
|
||||
Self-hosted LiveSync splits the note into chunks for efficient synchronization. This chunk should be longer than "Minimum chunk size".
|
||||
|
||||
Specifically, the length of the chunk is determined by the following orders.
|
||||
|
||||
1. Find the nearest newline character, and if it is farther than LongLineThreshold, this piece becomes an independent chunk.
|
||||
|
||||
2. If not, find nearest to these items.
|
||||
1. Newline character
|
||||
2. Empty line (Windows style)
|
||||
3. Empty line (non-Windows style)
|
||||
3. Compare the farther in these 3 positions and next "\[newline\]#" position, pick a shorter piece to as chunk.
|
||||
|
||||
This rule was made empirically from my dataset. If this rule acts as badly on your data. Please give me the information.
|
||||
|
||||
You can dump saved note structure to `Dump informations of this doc`. Replace every character to x except newline and "#" when sending information to me.
|
||||
|
||||
Default values are 20 letters and 250 letters.
|
||||
|
||||
## General Settings
|
||||
|
||||
### Do not show low-priority log
|
||||
@@ -122,32 +143,58 @@ Self-hosted LiveSync will delete the folder when the folder becomes empty. If th
|
||||
### Use newer file if conflicted (beta)
|
||||
Always use the newer file to resolve and overwrite when conflict has occurred.
|
||||
|
||||
### minimum chunk size and LongLine threshold
|
||||
The configuration of chunk splitting.
|
||||
### Advanced settings
|
||||
Self-hosted LiveSync using PouchDB and synchronizes with the remote by [this protocol](https://docs.couchdb.org/en/stable/replication/protocol.html).
|
||||
So, it splits every entry into chunks to be acceptable by the database with limited payload size and document size.
|
||||
|
||||
Self-hosted LiveSync splits the note into chunks for efficient synchronization. This chunk should be longer than "Minimum chunk size".
|
||||
However, it was not enough.
|
||||
According to [2.4.2.5.2. Upload Batch of Changed Documents](https://docs.couchdb.org/en/stable/replication/protocol.html#upload-batch-of-changed-documents) in [Replicate Changes](https://docs.couchdb.org/en/stable/replication/protocol.html#replicate-changes), it might become a bigger request.
|
||||
|
||||
Specifically, the length of the chunk is determined by the following orders.
|
||||
Unfortunately, there is no way to deal with this automatically by size for every request.
|
||||
Therefore, I made it possible to configure this.
|
||||
|
||||
1. Find the nearest newline character, and if it is farther than LongLineThreshold, this piece becomes an independent chunk.
|
||||
Note: If you set these values lower number, the number of requests will increase.
|
||||
Therefore, if you are far from the server, the total throughput will be low, and the traffic will increase.
|
||||
|
||||
2. If not, find nearest to these items.
|
||||
1. Newline character
|
||||
2. Empty line (Windows style)
|
||||
3. Empty line (non-Windows style)
|
||||
3. Compare the farther in these 3 positions and next "\[newline\]#" position, pick a shorter piece to as chunk.
|
||||
### Batch size
|
||||
Number of change feed items to process at a time. Defaults to 250.
|
||||
|
||||
This rule was made empirically from my dataset. If this rule acts as badly on your data. Please give me the information.
|
||||
|
||||
You can dump saved note structure to `Dump informations of this doc`. Replace every character to x except newline and "#" when sending information to me.
|
||||
|
||||
Default values are 20 letters and 250 letters.
|
||||
### Batch limit
|
||||
Number of batches to process at a time. Defaults to 40. This along with batch size controls how many docs are kept in memory at a time.
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
### Show status inside editor
|
||||
Show information inside the editor pane.
|
||||
It would be useful for mobile.
|
||||
|
||||
### Check integrity on saving
|
||||
Check all chunks are correctly saved on saving.
|
||||
|
||||
### Presets
|
||||
You can set synchronization method at once as these pattern:
|
||||
- LiveSync
|
||||
- LiveSync : enabled
|
||||
- Batch database update : disabled
|
||||
- Periodic Sync : disabled
|
||||
- Sync on Save : disabled
|
||||
- Sync on File Open : disabled
|
||||
- Sync on Start : disabled
|
||||
- Periodic w/ batch
|
||||
- LiveSync : disabled
|
||||
- Batch database update : enabled
|
||||
- Periodic Sync : enabled
|
||||
- Sync on Save : disabled
|
||||
- Sync on File Open : enabled
|
||||
- Sync on Start : enabled
|
||||
- Disable all sync
|
||||
- LiveSync : disabled
|
||||
- Batch database update : disabled
|
||||
- Periodic Sync : disabled
|
||||
- Sync on Save : disabled
|
||||
- Sync on File Open : disabled
|
||||
- Sync on Start : disabled
|
||||
|
||||
## Hatch
|
||||
From here, everything is under the hood. Please handle it with care.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ CouchDBのURIを入力します。Cloudantの場合は「External Endpoint(prefe
|
||||
## Local Database Configurations
|
||||
端末内に作成されるデータベースの設定です。
|
||||
|
||||
### Batch database update (beta)
|
||||
### Batch database update
|
||||
データベースの更新を以下の事象が発生するまで遅延させます。
|
||||
- レプリケーションが発生する
|
||||
- 他のファイルを開く
|
||||
@@ -78,6 +78,24 @@ End to End 暗号化を行うに当たって、異なるパスフレーズで暗
|
||||
どちらのオペレーションも、実行するとすべての同期設定が無効化されます。
|
||||
**また、パスフレーズのチェックは、実際に復号するまで行いません。そのため、パスフレーズを間違えて設定し、Apply and receiveで同期を行うと、大量のエラーが発生します。これは仕様です。**
|
||||
|
||||
### minimum chunk size と LongLine threshold
|
||||
チャンクの分割についての設定です。
|
||||
Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk size文字確保した上で、できるだけ効率的に同期できるよう、ノートを分割してチャンクを作成します。
|
||||
これは、同期を行う際に、一定の文字数で分割した場合、先頭の方を編集すると、その後の分割位置がすべてずれ、結果としてほぼまるごとのファイルのファイル送受信を行うことになっていた問題を避けるために実装されました。
|
||||
具体的には、先頭から順に直近の下記の箇所を検索し、一番長く切れたものを一つのチャンクとします。
|
||||
|
||||
1. 次の改行を探し、それがLongLine Thresholdより先であれば、一つのチャンクとして確定します。
|
||||
|
||||
2. そうではない場合は、下記を順に探します。
|
||||
1. 改行
|
||||
2. windowsでの空行がある所
|
||||
3. 非Windowsでの空行がある所
|
||||
3. この三つのうち一番遠い場所と、 「改行後、#から始まる所」を比べ、短い方をチャンクとします。
|
||||
|
||||
このルールは経験則的に作りました。実データが偏っているため。もし思わぬ挙動をしている場合は、是非コマンドから`Dump informations of this doc`を選択し、情報をください。
|
||||
改行文字と#を除き、すべて●に置換しても、アルゴリズムは有効に働きます。
|
||||
デフォルトは20文字と、250文字です。
|
||||
|
||||
## General Settings
|
||||
一般的な設定です。
|
||||
|
||||
@@ -124,24 +142,6 @@ Self-hosted LiveSyncは通常、フォルダ内のファイルがすべて削除
|
||||
### Use newer file if conflicted (beta)
|
||||
競合が発生したとき、常に新しいファイルを使用して競合を自動的に解決します。
|
||||
|
||||
### minimum chunk size と LongLine threshold
|
||||
チャンクの分割についての設定です。
|
||||
Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk size文字確保した上で、できるだけ効率的に同期できるよう、ノートを分割してチャンクを作成します。
|
||||
これは、同期を行う際に、一定の文字数で分割した場合、先頭の方を編集すると、その後の分割位置がすべてずれ、結果としてほぼまるごとのファイルのファイル送受信を行うことになっていた問題を避けるために実装されました。
|
||||
具体的には、先頭から順に直近の下記の箇所を検索し、一番長く切れたものを一つのチャンクとします。
|
||||
|
||||
1. 次の改行を探し、それがLongLine Thresholdより先であれば、一つのチャンクとして確定します。
|
||||
|
||||
2. そうではない場合は、下記を順に探します。
|
||||
1. 改行
|
||||
2. windowsでの空行がある所
|
||||
3. 非Windowsでの空行がある所
|
||||
3. この三つのうち一番遠い場所と、 「改行後、#から始まる所」を比べ、短い方をチャンクとします。
|
||||
|
||||
このルールは経験則的に作りました。実データが偏っているため。もし思わぬ挙動をしている場合は、是非コマンドから`Dump informations of this doc`を選択し、情報をください。
|
||||
改行文字と#を除き、すべて●に置換しても、アルゴリズムは有効に働きます。
|
||||
デフォルトは20文字と、250文字です。
|
||||
|
||||
## Miscellaneous
|
||||
その他の設定です
|
||||
### Show status inside editor
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.1",
|
||||
"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",
|
||||
|
||||
120
package-lock.json
generated
120
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
@@ -23,7 +23,7 @@
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"obsidian": "^0.12.0",
|
||||
"obsidian": "^0.13.11",
|
||||
"rollup": "^2.32.1",
|
||||
"tslib": "^2.2.0",
|
||||
"typescript": "^4.2.4"
|
||||
@@ -132,6 +132,43 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/rangeset": {
|
||||
"version": "0.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/rangeset/-/rangeset-0.19.5.tgz",
|
||||
"integrity": "sha512-L3b+RIwIRKOJ3pJLOtpkxCUjGnxZKFyPb0CjYWKnVLuzEIaEExWWK7sp6rsejxOy8RjYzfCHlFhYB4UdQN7brw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^0.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "0.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.19.6.tgz",
|
||||
"integrity": "sha512-sqIQZE9VqwQj7D4c2oz9mfLhlT1ElAzGB5lO1lE33BPyrdNy1cJyCIOecT4cn4VeJOFrnjOeu+IftZ3zqdFETw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@codemirror/text": "^0.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/text": {
|
||||
"version": "0.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/text/-/text-0.19.5.tgz",
|
||||
"integrity": "sha512-Syu5Xc7tZzeUAM/y4fETkT0zgGr48rDG+w4U38bPwSIUr+L9S/7w2wDE1WGNzjaZPz12F6gb1gxWiSTg9ocLow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "0.19.37",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.19.37.tgz",
|
||||
"integrity": "sha512-SLuLx9p0O1ZHXLehvl5MwSvUrQRcsNGemzTgJ0zRajmc3BBsNigI1PXxdo7tvBhO5DcAzRRBXoke9DZFUR6Qqg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@codemirror/rangeset": "^0.19.5",
|
||||
"@codemirror/state": "^0.19.3",
|
||||
"@codemirror/text": "^0.19.0",
|
||||
"style-mod": "^4.0.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
|
||||
@@ -2185,11 +2222,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "0.12.17",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.12.17.tgz",
|
||||
"integrity": "sha512-YvCAlRym9D8zNPXt6Ez8QubSTVGoChx6lb58zqI13Dcrz3l1lgUO+pcOGDiD5Qa67nzDZLXo3aV2rqkCCpTvGQ==",
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.11.tgz",
|
||||
"integrity": "sha512-KxOvAh4CG5vzcukmHvyuK9hUIr6ZFlM9FQfGZEwrrEV8VG2/W2Tk5cWrg0VM7EkGE3QBmjX6owjIDIO8QDXVUQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^0.19.6",
|
||||
"@codemirror/view": "^0.19.31",
|
||||
"@types/codemirror": "0.0.108",
|
||||
"moment": "2.29.1"
|
||||
}
|
||||
@@ -2645,6 +2684,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
|
||||
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -2819,6 +2864,12 @@
|
||||
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
|
||||
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -2962,6 +3013,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@codemirror/rangeset": {
|
||||
"version": "0.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/rangeset/-/rangeset-0.19.5.tgz",
|
||||
"integrity": "sha512-L3b+RIwIRKOJ3pJLOtpkxCUjGnxZKFyPb0CjYWKnVLuzEIaEExWWK7sp6rsejxOy8RjYzfCHlFhYB4UdQN7brw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@codemirror/state": "^0.19.0"
|
||||
}
|
||||
},
|
||||
"@codemirror/state": {
|
||||
"version": "0.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.19.6.tgz",
|
||||
"integrity": "sha512-sqIQZE9VqwQj7D4c2oz9mfLhlT1ElAzGB5lO1lE33BPyrdNy1cJyCIOecT4cn4VeJOFrnjOeu+IftZ3zqdFETw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@codemirror/text": "^0.19.0"
|
||||
}
|
||||
},
|
||||
"@codemirror/text": {
|
||||
"version": "0.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/text/-/text-0.19.5.tgz",
|
||||
"integrity": "sha512-Syu5Xc7tZzeUAM/y4fETkT0zgGr48rDG+w4U38bPwSIUr+L9S/7w2wDE1WGNzjaZPz12F6gb1gxWiSTg9ocLow==",
|
||||
"dev": true
|
||||
},
|
||||
"@codemirror/view": {
|
||||
"version": "0.19.37",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.19.37.tgz",
|
||||
"integrity": "sha512-SLuLx9p0O1ZHXLehvl5MwSvUrQRcsNGemzTgJ0zRajmc3BBsNigI1PXxdo7tvBhO5DcAzRRBXoke9DZFUR6Qqg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@codemirror/rangeset": "^0.19.5",
|
||||
"@codemirror/state": "^0.19.3",
|
||||
"@codemirror/text": "^0.19.0",
|
||||
"style-mod": "^4.0.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"@eslint/eslintrc": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
|
||||
@@ -4511,11 +4599,13 @@
|
||||
}
|
||||
},
|
||||
"obsidian": {
|
||||
"version": "0.12.17",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.12.17.tgz",
|
||||
"integrity": "sha512-YvCAlRym9D8zNPXt6Ez8QubSTVGoChx6lb58zqI13Dcrz3l1lgUO+pcOGDiD5Qa67nzDZLXo3aV2rqkCCpTvGQ==",
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.11.tgz",
|
||||
"integrity": "sha512-KxOvAh4CG5vzcukmHvyuK9hUIr6ZFlM9FQfGZEwrrEV8VG2/W2Tk5cWrg0VM7EkGE3QBmjX6owjIDIO8QDXVUQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@codemirror/state": "^0.19.6",
|
||||
"@codemirror/view": "^0.19.31",
|
||||
"@types/codemirror": "0.0.108",
|
||||
"moment": "2.29.1"
|
||||
}
|
||||
@@ -4822,6 +4912,12 @@
|
||||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
||||
"dev": true
|
||||
},
|
||||
"style-mod": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
|
||||
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -4962,6 +5058,12 @@
|
||||
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
||||
"dev": true
|
||||
},
|
||||
"w3c-keyname": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
|
||||
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==",
|
||||
"dev": true
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.1",
|
||||
"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",
|
||||
"scripts": {
|
||||
@@ -22,7 +22,7 @@
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"obsidian": "^0.12.0",
|
||||
"obsidian": "^0.13.11",
|
||||
"rollup": "^2.32.1",
|
||||
"tslib": "^2.2.0",
|
||||
"typescript": "^4.2.4"
|
||||
|
||||
@@ -7,6 +7,7 @@ export class ConflictResolveModal extends Modal {
|
||||
// result: Array<[number, string]>;
|
||||
result: diff_result;
|
||||
callback: (remove_rev: string) => Promise<void>;
|
||||
|
||||
constructor(app: App, diff: diff_result, callback: (remove_rev: string) => Promise<void>) {
|
||||
super(app);
|
||||
this.result = diff;
|
||||
@@ -45,18 +46,21 @@ export class ConflictResolveModal extends Modal {
|
||||
contentEl.createEl("button", { text: "Keep A" }, (e) => {
|
||||
e.addEventListener("click", async () => {
|
||||
await this.callback(this.result.right.rev);
|
||||
this.callback = null;
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
contentEl.createEl("button", { text: "Keep B" }, (e) => {
|
||||
e.addEventListener("click", async () => {
|
||||
await this.callback(this.result.left.rev);
|
||||
this.callback = null;
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
contentEl.createEl("button", { text: "Concat both" }, (e) => {
|
||||
e.addEventListener("click", async () => {
|
||||
await this.callback(null);
|
||||
await this.callback("");
|
||||
this.callback = null;
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
@@ -70,5 +74,8 @@ export class ConflictResolveModal extends Modal {
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
if (this.callback != null) {
|
||||
this.callback(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,41 +403,44 @@ export class LocalPouchDB {
|
||||
async deleteDBEntry(path: string, opt?: PouchDB.Core.GetOptions): Promise<boolean> {
|
||||
await this.waitForGCComplete();
|
||||
const id = path2id(path);
|
||||
|
||||
try {
|
||||
let obj: EntryDocResponse = null;
|
||||
if (opt) {
|
||||
obj = await this.localDatabase.get(id, opt);
|
||||
} else {
|
||||
obj = await this.localDatabase.get(id);
|
||||
}
|
||||
return await runWithLock("file:" + id, false, async () => {
|
||||
if (opt) {
|
||||
obj = await this.localDatabase.get(id, opt);
|
||||
} else {
|
||||
obj = await this.localDatabase.get(id);
|
||||
}
|
||||
|
||||
if (obj.type && obj.type == "leaf") {
|
||||
//do nothing for leaf;
|
||||
return false;
|
||||
}
|
||||
//Check it out and fix docs to regular case
|
||||
if (!obj.type || (obj.type && obj.type == "notes")) {
|
||||
obj._deleted = true;
|
||||
const r = await this.localDatabase.put(obj);
|
||||
this.updateRecentModifiedDocs(r.id, r.rev, true);
|
||||
if (typeof this.corruptedEntries[obj._id] != "undefined") {
|
||||
delete this.corruptedEntries[obj._id];
|
||||
if (obj.type && obj.type == "leaf") {
|
||||
//do nothing for leaf;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
// simple note
|
||||
}
|
||||
if (obj.type == "newnote" || obj.type == "plain") {
|
||||
obj._deleted = true;
|
||||
const r = await this.localDatabase.put(obj);
|
||||
Logger(`entry removed:${obj._id}-${r.rev}`);
|
||||
this.updateRecentModifiedDocs(r.id, r.rev, true);
|
||||
if (typeof this.corruptedEntries[obj._id] != "undefined") {
|
||||
delete this.corruptedEntries[obj._id];
|
||||
//Check it out and fix docs to regular case
|
||||
if (!obj.type || (obj.type && obj.type == "notes")) {
|
||||
obj._deleted = true;
|
||||
const r = await this.localDatabase.put(obj);
|
||||
this.updateRecentModifiedDocs(r.id, r.rev, true);
|
||||
if (typeof this.corruptedEntries[obj._id] != "undefined") {
|
||||
delete this.corruptedEntries[obj._id];
|
||||
}
|
||||
return true;
|
||||
// simple note
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (obj.type == "newnote" || obj.type == "plain") {
|
||||
obj._deleted = true;
|
||||
const r = await this.localDatabase.put(obj);
|
||||
Logger(`entry removed:${obj._id}-${r.rev}`);
|
||||
this.updateRecentModifiedDocs(r.id, r.rev, true);
|
||||
if (typeof this.corruptedEntries[obj._id] != "undefined") {
|
||||
delete this.corruptedEntries[obj._id];
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} catch (ex) {
|
||||
if (ex.status && ex.status == 404) {
|
||||
return false;
|
||||
@@ -478,10 +481,13 @@ export class LocalPouchDB {
|
||||
let notfound = 0;
|
||||
for (const v of delDocs) {
|
||||
try {
|
||||
const item = await this.localDatabase.get(v);
|
||||
item._deleted = true;
|
||||
await this.localDatabase.put(item);
|
||||
this.updateRecentModifiedDocs(item._id, item._rev, true);
|
||||
await runWithLock("file:" + v, false, async () => {
|
||||
const item = await this.localDatabase.get(v);
|
||||
item._deleted = true;
|
||||
await this.localDatabase.put(item);
|
||||
this.updateRecentModifiedDocs(item._id, item._rev, true);
|
||||
});
|
||||
|
||||
deleteCount++;
|
||||
} catch (ex) {
|
||||
if (ex.status && ex.status == 404) {
|
||||
@@ -540,7 +546,6 @@ export class LocalPouchDB {
|
||||
cPieceSize = 0;
|
||||
// lookup for next splittion .
|
||||
// we're standing on "\n"
|
||||
// debugger
|
||||
do {
|
||||
const n1 = leftData.indexOf("\n", cPieceSize + 1);
|
||||
const n2 = leftData.indexOf("\n\n", cPieceSize + 1);
|
||||
@@ -691,33 +696,35 @@ export class LocalPouchDB {
|
||||
type: plainSplit ? "plain" : "newnote",
|
||||
};
|
||||
// Here for upsert logic,
|
||||
try {
|
||||
const old = await this.localDatabase.get(newDoc._id);
|
||||
if (!old.type || old.type == "notes" || old.type == "newnote" || old.type == "plain") {
|
||||
// simple use rev for new doc
|
||||
newDoc._rev = old._rev;
|
||||
await runWithLock("file:" + newDoc._id, false, async () => {
|
||||
try {
|
||||
const old = await this.localDatabase.get(newDoc._id);
|
||||
if (!old.type || old.type == "notes" || old.type == "newnote" || old.type == "plain") {
|
||||
// simple use rev for new doc
|
||||
newDoc._rev = old._rev;
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex.status && ex.status == 404) {
|
||||
// NO OP/
|
||||
} else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex.status && ex.status == 404) {
|
||||
// NO OP/
|
||||
const r = await this.localDatabase.put(newDoc, { force: true });
|
||||
this.updateRecentModifiedDocs(r.id, r.rev, newDoc._deleted);
|
||||
if (typeof this.corruptedEntries[note._id] != "undefined") {
|
||||
delete this.corruptedEntries[note._id];
|
||||
}
|
||||
if (this.settings.checkIntegrityOnSave) {
|
||||
if (!this.sanCheck(await this.localDatabase.get(r.id))) {
|
||||
Logger("note save failed!", LOG_LEVEL.NOTICE);
|
||||
} else {
|
||||
Logger(`note has been surely saved:${newDoc._id}:${r.rev}`);
|
||||
}
|
||||
} else {
|
||||
throw ex;
|
||||
Logger(`note saved:${newDoc._id}:${r.rev}`);
|
||||
}
|
||||
}
|
||||
const r = await this.localDatabase.put(newDoc, { force: true });
|
||||
this.updateRecentModifiedDocs(r.id, r.rev, newDoc._deleted);
|
||||
if (typeof this.corruptedEntries[note._id] != "undefined") {
|
||||
delete this.corruptedEntries[note._id];
|
||||
}
|
||||
if (this.settings.checkIntegrityOnSave) {
|
||||
if (!this.sanCheck(await this.localDatabase.get(r.id))) {
|
||||
Logger("note save failed!", LOG_LEVEL.NOTICE);
|
||||
} else {
|
||||
Logger(`note has been surely saved:${newDoc._id}:${r.rev}`);
|
||||
}
|
||||
} else {
|
||||
Logger(`note saved:${newDoc._id}:${r.rev}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Logger(`note coud not saved:${note._id}`);
|
||||
}
|
||||
@@ -756,8 +763,8 @@ export class LocalPouchDB {
|
||||
}
|
||||
|
||||
const syncOptionBase: PouchDB.Replication.SyncOptions = {
|
||||
batch_size: 250,
|
||||
batches_limit: 40,
|
||||
batches_limit: setting.batches_limit,
|
||||
batch_size: setting.batch_size,
|
||||
};
|
||||
|
||||
const db = dbret.db;
|
||||
@@ -837,6 +844,15 @@ export class LocalPouchDB {
|
||||
locked: false,
|
||||
accepted_nodes: [this.nodeid],
|
||||
};
|
||||
// const remoteInfo = dbret.info;
|
||||
// const localInfo = await this.localDatabase.info();
|
||||
// const remoteDocsCount = remoteInfo.doc_count;
|
||||
// const localDocsCount = localInfo.doc_count;
|
||||
// const remoteUpdSeq = typeof remoteInfo.update_seq == "string" ? Number(remoteInfo.update_seq.split("-")[0]) : remoteInfo.update_seq;
|
||||
// const localUpdSeq = typeof localInfo.update_seq == "string" ? Number(localInfo.update_seq.split("-")[0]) : localInfo.update_seq;
|
||||
|
||||
// Logger(`Database diffences: remote:${remoteDocsCount} docs / last update ${remoteUpdSeq}`);
|
||||
// Logger(`Database diffences: local :${localDocsCount} docs / last update ${localUpdSeq}`);
|
||||
|
||||
const remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defMilestonePoint);
|
||||
this.remoteLocked = remoteMilestone.locked;
|
||||
@@ -851,12 +867,12 @@ export class LocalPouchDB {
|
||||
}
|
||||
|
||||
const syncOptionBase: PouchDB.Replication.SyncOptions = {
|
||||
batch_size: 250,
|
||||
batches_limit: 40,
|
||||
batches_limit: setting.batches_limit,
|
||||
batch_size: setting.batch_size,
|
||||
};
|
||||
const syncOption: PouchDB.Replication.SyncOptions = keepAlive ? { live: true, retry: true, heartbeat: 30000, ...syncOptionBase } : { ...syncOptionBase };
|
||||
|
||||
return { db: dbret.db, syncOptionBase, syncOption };
|
||||
return { db: dbret.db, info: dbret.info, syncOptionBase, syncOption };
|
||||
}
|
||||
|
||||
async openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>): Promise<boolean> {
|
||||
@@ -876,87 +892,89 @@ export class LocalPouchDB {
|
||||
//replicate once
|
||||
this.syncStatus = "STARTED";
|
||||
|
||||
return new Promise<boolean>(async (res, rej) => {
|
||||
let resolved = false;
|
||||
const _openReplicationSync = () => {
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
this.syncHandler = this.localDatabase.sync<EntryDoc>(db, syncOption);
|
||||
this.syncHandler
|
||||
.on("active", () => {
|
||||
this.syncStatus = "CONNECTED";
|
||||
this.updateInfo();
|
||||
Logger("Replication activated");
|
||||
})
|
||||
.on("change", async (e) => {
|
||||
try {
|
||||
if (e.direction == "pull") {
|
||||
// console.log(`pulled data:${e.change.docs.map((e) => e._id).join(",")}`);
|
||||
await callback(e.change.docs);
|
||||
Logger(`replicated ${e.change.docs_read} doc(s)`);
|
||||
this.docArrived += e.change.docs.length;
|
||||
} else {
|
||||
// console.log(`put data:${e.change.docs.map((e) => e._id).join(",")}`);
|
||||
this.docSent += e.change.docs.length;
|
||||
}
|
||||
if (notice != null) {
|
||||
notice.setMessage(`↑${e.change.docs_written} ↓${e.change.docs_read}`);
|
||||
}
|
||||
this.updateInfo();
|
||||
} catch (ex) {
|
||||
Logger("Replication callback error");
|
||||
Logger(ex);
|
||||
}
|
||||
})
|
||||
.on("complete", (e) => {
|
||||
this.syncStatus = "COMPLETED";
|
||||
this.updateInfo();
|
||||
Logger("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
||||
if (notice != null) notice.hide();
|
||||
if (!keepAlive) {
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
// if keep alive runnning, resolve here,
|
||||
res(true);
|
||||
}
|
||||
})
|
||||
.on("denied", (e) => {
|
||||
this.syncStatus = "ERRORED";
|
||||
this.updateInfo();
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
if (notice != null) notice.hide();
|
||||
Logger("Replication denied", LOG_LEVEL.NOTICE);
|
||||
// Logger(e);
|
||||
rej(e);
|
||||
})
|
||||
.on("error", (e) => {
|
||||
this.syncStatus = "ERRORED";
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
this.updateInfo();
|
||||
if (notice != null) notice.hide();
|
||||
Logger("Replication error", LOG_LEVEL.NOTICE);
|
||||
// Logger(e);
|
||||
rej(e);
|
||||
})
|
||||
.on("paused", (e) => {
|
||||
this.syncStatus = "PAUSED";
|
||||
this.updateInfo();
|
||||
if (notice != null) notice.hide();
|
||||
Logger("replication paused", LOG_LEVEL.VERBOSE);
|
||||
if (keepAlive && !resolved) {
|
||||
// if keep alive runnning, resolve here,
|
||||
resolved = true;
|
||||
res(true);
|
||||
}
|
||||
// Logger(e);
|
||||
});
|
||||
};
|
||||
if (!keepAlive) {
|
||||
return await _openReplicationSync();
|
||||
}
|
||||
let resolved = false;
|
||||
const docArrivedOnStart = this.docArrived;
|
||||
const docSentOnStart = this.docSent;
|
||||
const _openReplicationSync = () => {
|
||||
Logger("Sync Main Started");
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
Logger("Pull before replicate.");
|
||||
Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE);
|
||||
Logger(await db.info(), LOG_LEVEL.VERBOSE);
|
||||
const replicate = this.localDatabase.replicate.from(db, syncOptionBase);
|
||||
this.syncHandler = this.localDatabase.sync<EntryDoc>(db, syncOption);
|
||||
this.syncHandler
|
||||
.on("active", () => {
|
||||
this.syncStatus = "CONNECTED";
|
||||
this.updateInfo();
|
||||
Logger("Replication activated");
|
||||
})
|
||||
.on("change", async (e) => {
|
||||
try {
|
||||
if (e.direction == "pull") {
|
||||
// console.log(`pulled data:${e.change.docs.map((e) => e._id).join(",")}`);
|
||||
await callback(e.change.docs);
|
||||
Logger(`replicated ${e.change.docs_read} doc(s)`);
|
||||
this.docArrived += e.change.docs.length;
|
||||
} else {
|
||||
// console.log(`put data:${e.change.docs.map((e) => e._id).join(",")}`);
|
||||
this.docSent += e.change.docs.length;
|
||||
}
|
||||
if (notice != null) {
|
||||
notice.setMessage(`↑${this.docSent - docSentOnStart} ↓${this.docArrived - docArrivedOnStart}`);
|
||||
}
|
||||
this.updateInfo();
|
||||
} catch (ex) {
|
||||
Logger("Replication callback error");
|
||||
Logger(ex);
|
||||
}
|
||||
})
|
||||
.on("complete", (e) => {
|
||||
this.syncStatus = "COMPLETED";
|
||||
this.updateInfo();
|
||||
Logger("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
||||
if (notice != null) notice.hide();
|
||||
if (!keepAlive) {
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
// if keep alive runnning, resolve here,
|
||||
}
|
||||
})
|
||||
.on("denied", (e) => {
|
||||
this.syncStatus = "ERRORED";
|
||||
this.updateInfo();
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
if (notice != null) notice.hide();
|
||||
Logger("Replication denied", LOG_LEVEL.NOTICE);
|
||||
Logger(e);
|
||||
})
|
||||
.on("error", (e) => {
|
||||
this.syncStatus = "ERRORED";
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
this.updateInfo();
|
||||
if (notice != null) notice.hide();
|
||||
Logger("Replication error", LOG_LEVEL.NOTICE);
|
||||
Logger(e);
|
||||
})
|
||||
.on("paused", (e) => {
|
||||
this.syncStatus = "PAUSED";
|
||||
this.updateInfo();
|
||||
if (notice != null) notice.hide();
|
||||
Logger("replication paused", LOG_LEVEL.VERBOSE);
|
||||
if (keepAlive && !resolved) {
|
||||
// if keep alive runnning, resolve here,
|
||||
resolved = true;
|
||||
}
|
||||
// Logger(e);
|
||||
});
|
||||
return this.syncHandler;
|
||||
};
|
||||
if (!keepAlive) {
|
||||
await _openReplicationSync();
|
||||
return true;
|
||||
}
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
Logger("Pull before replicate.");
|
||||
Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE);
|
||||
Logger(await db.info(), LOG_LEVEL.VERBOSE);
|
||||
let replicate: PouchDB.Replication.Replication<EntryDoc>;
|
||||
try {
|
||||
replicate = this.localDatabase.replicate.from(db, syncOptionBase);
|
||||
replicate
|
||||
.on("active", () => {
|
||||
this.syncStatus = "CONNECTED";
|
||||
@@ -979,36 +997,23 @@ export class LocalPouchDB {
|
||||
Logger("Replication callback error");
|
||||
Logger(ex);
|
||||
}
|
||||
})
|
||||
.on("complete", async (info) => {
|
||||
this.syncStatus = "COMPLETED";
|
||||
this.updateInfo();
|
||||
this.cancelHandler(replicate);
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
Logger("Replication pull completed.");
|
||||
await _openReplicationSync();
|
||||
})
|
||||
.on("denied", (e) => {
|
||||
this.syncStatus = "ERRORED";
|
||||
this.updateInfo();
|
||||
if (notice != null) notice.hide();
|
||||
Logger("Pulling Replication denied", LOG_LEVEL.NOTICE);
|
||||
this.cancelHandler(replicate);
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
rej(e);
|
||||
})
|
||||
.on("error", (e) => {
|
||||
this.syncStatus = "ERRORED";
|
||||
this.updateInfo();
|
||||
Logger("Pulling Replication error", LOG_LEVEL.INFO);
|
||||
this.cancelHandler(replicate);
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
if (notice != null) notice.hide();
|
||||
// debugger;
|
||||
Logger(e);
|
||||
rej(e);
|
||||
});
|
||||
});
|
||||
this.syncStatus = "COMPLETED";
|
||||
this.updateInfo();
|
||||
this.cancelHandler(replicate);
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
Logger("Replication pull completed.");
|
||||
_openReplicationSync();
|
||||
return true;
|
||||
} catch (ex) {
|
||||
this.syncStatus = "ERRORED";
|
||||
this.updateInfo();
|
||||
Logger("Pulling Replication error", LOG_LEVEL.NOTICE);
|
||||
this.cancelHandler(replicate);
|
||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||
if (notice != null) notice.hide();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
closeReplication() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
import { App, Notice, PluginSettingTab, Setting, sanitizeHTMLToDom } from "obsidian";
|
||||
import { EntryDoc, LOG_LEVEL } from "./types";
|
||||
import { escapeStringToHTML, versionNumberString2Number, path2id, id2path, runWithLock } from "./utils";
|
||||
import { Logger } from "./logger";
|
||||
@@ -72,7 +72,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
const containerRemoteDatabaseEl = containerEl.createDiv();
|
||||
containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" });
|
||||
const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: "The remote configuration is locked while any synchronization is enabled." });
|
||||
const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: `These settings are kept locked while automatic synchronization options are enabled. Disable these options in the "Sync Settings" tab to unlock.` });
|
||||
syncWarn.addClass("op-warn");
|
||||
syncWarn.addClass("sls-hidden");
|
||||
|
||||
@@ -82,6 +82,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
if (this.plugin.settings.syncOnFileOpen) return true;
|
||||
if (this.plugin.settings.syncOnSave) return true;
|
||||
if (this.plugin.settings.syncOnStart) return true;
|
||||
if (this.plugin.localDatabase.syncStatus == "CONNECTED") return true;
|
||||
if (this.plugin.localDatabase.syncStatus == "PAUSED") return true;
|
||||
return false;
|
||||
};
|
||||
const applyDisplayEnabled = () => {
|
||||
@@ -304,6 +306,44 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
})
|
||||
);
|
||||
|
||||
containerLocalDatabaseEl.createEl("div", {
|
||||
text: sanitizeHTMLToDom(`Advanced settings<br>
|
||||
Configuration of how LiveSync makes chunks from the file.`),
|
||||
});
|
||||
new Setting(containerLocalDatabaseEl)
|
||||
.setName("Minimum chunk size")
|
||||
.setDesc("(letters), minimum chunk size.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.minimumChunkSize + "")
|
||||
.onChange(async (value) => {
|
||||
let v = Number(value);
|
||||
if (isNaN(v) || v < 10 || v > 1000) {
|
||||
v = 10;
|
||||
}
|
||||
this.plugin.settings.minimumChunkSize = v;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.setAttribute("type", "number");
|
||||
});
|
||||
|
||||
new Setting(containerLocalDatabaseEl)
|
||||
.setName("LongLine Threshold")
|
||||
.setDesc("(letters), If the line is longer than this, make the line to chunk")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.longLineThreshold + "")
|
||||
.onChange(async (value) => {
|
||||
let v = Number(value);
|
||||
if (isNaN(v) || v < 10 || v > 1000) {
|
||||
v = 10;
|
||||
}
|
||||
this.plugin.settings.longLineThreshold = v;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.setAttribute("type", "number");
|
||||
});
|
||||
|
||||
addScreenElement("10", containerLocalDatabaseEl);
|
||||
const containerGeneralSettingsEl = containerEl.createDiv();
|
||||
containerGeneralSettingsEl.createEl("h3", { text: "General Settings" });
|
||||
@@ -458,35 +498,40 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
containerSyncSettingEl.createEl("div", {
|
||||
text: sanitizeHTMLToDom(`Advanced settings<br>
|
||||
If you reached the payload size limit when using IBM Cloudant, please set batch size and batch limit to a lower value.`),
|
||||
});
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Minimum chunk size")
|
||||
.setDesc("(letters), minimum chunk size.")
|
||||
.setName("Batch size")
|
||||
.setDesc("Number of change feed items to process at a time. Defaults to 250.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.minimumChunkSize + "")
|
||||
.setValue(this.plugin.settings.batch_size + "")
|
||||
.onChange(async (value) => {
|
||||
let v = Number(value);
|
||||
if (isNaN(v) || v < 10 || v > 1000) {
|
||||
if (isNaN(v) || v < 10) {
|
||||
v = 10;
|
||||
}
|
||||
this.plugin.settings.minimumChunkSize = v;
|
||||
this.plugin.settings.batch_size = v;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.setAttribute("type", "number");
|
||||
});
|
||||
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("LongLine Threshold")
|
||||
.setDesc("(letters), If the line is longer than this, make the line to chunk")
|
||||
.setName("Batch limit")
|
||||
.setDesc("Number of batches to process at a time. Defaults to 40. This along with batch size controls how many docs are kept in memory at a time.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.longLineThreshold + "")
|
||||
.setValue(this.plugin.settings.batches_limit + "")
|
||||
.onChange(async (value) => {
|
||||
let v = Number(value);
|
||||
if (isNaN(v) || v < 10 || v > 1000) {
|
||||
if (isNaN(v) || v < 10) {
|
||||
v = 10;
|
||||
}
|
||||
this.plugin.settings.longLineThreshold = v;
|
||||
this.plugin.settings.batches_limit = v;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.setAttribute("type", "number");
|
||||
@@ -544,10 +589,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
Logger("Synchronization setting configured as LiveSync.", LOG_LEVEL.NOTICE);
|
||||
} else if (currentPrest == "PERIODIC") {
|
||||
this.plugin.settings.batchSave = true;
|
||||
this.plugin.settings.periodicReplication = false;
|
||||
this.plugin.settings.periodicReplication = true;
|
||||
this.plugin.settings.syncOnSave = false;
|
||||
this.plugin.settings.syncOnStart = false;
|
||||
this.plugin.settings.syncOnFileOpen = false;
|
||||
this.plugin.settings.syncOnStart = true;
|
||||
this.plugin.settings.syncOnFileOpen = true;
|
||||
Logger("Synchronization setting configured as Periodic sync with batch database update.", LOG_LEVEL.NOTICE);
|
||||
} else {
|
||||
Logger("All synchronization disabled.", LOG_LEVEL.NOTICE);
|
||||
@@ -1084,7 +1129,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
xx.addClass("mod-warning");
|
||||
}
|
||||
} else {
|
||||
containerCorruptedDataEl.createEl("div", { text: "There's no collupted data." });
|
||||
containerCorruptedDataEl.createEl("div", { text: "There is no corrupted data." });
|
||||
}
|
||||
applyDisplayEnabled();
|
||||
addScreenElement("70", containerCorruptedDataEl);
|
||||
|
||||
234
src/main.ts
234
src/main.ts
@@ -1,7 +1,7 @@
|
||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest } from "obsidian";
|
||||
import { diff_match_patch } from "diff-match-patch";
|
||||
|
||||
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, PluginDataEntry, LOG_LEVEL, VER, PERIODIC_PLUGIN_SWEEP, DEFAULT_SETTINGS, PluginList, DevicePluginList } from "./types";
|
||||
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, PluginDataEntry, LOG_LEVEL, VER, PERIODIC_PLUGIN_SWEEP, DEFAULT_SETTINGS, PluginList, DevicePluginList, diff_result } from "./types";
|
||||
import { base64ToString, arrayBufferToBase64, base64ToArrayBuffer, isValidPath, versionNumberString2Number, id2path, path2id, runWithLock } from "./utils";
|
||||
import { Logger, setLogger } from "./logger";
|
||||
import { LocalPouchDB } from "./LocalPouchDB";
|
||||
@@ -28,7 +28,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const lsname = "obsidian-live-sync-ver" + this.app.vault.getName();
|
||||
const last_version = localStorage.getItem(lsname);
|
||||
await this.loadSettings();
|
||||
if (!last_version || Number(last_version) < VER) {
|
||||
if (last_version && Number(last_version) < VER) {
|
||||
this.settings.liveSync = false;
|
||||
this.settings.syncOnSave = false;
|
||||
this.settings.syncOnStart = false;
|
||||
@@ -116,6 +116,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.localDatabase.getDBEntry(view.file.path, {}, true, false);
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-checkdoc-conflicted",
|
||||
name: "Resolve if conflicted.",
|
||||
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
||||
await this.showIfConflicted(view.file);
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-gc",
|
||||
name: "garbage collect now",
|
||||
@@ -284,18 +291,18 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.watchVaultChangeAsync(file, ...args);
|
||||
}
|
||||
async applyBatchChange() {
|
||||
if (!this.settings.batchSave || this.batchFileChange.length == 0) {
|
||||
return [];
|
||||
}
|
||||
return await runWithLock("batchSave", false, async () => {
|
||||
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
|
||||
this.batchFileChange = [];
|
||||
const files = this.app.vault.getFiles();
|
||||
const promises = batchItems.map(async (e) => {
|
||||
try {
|
||||
if (await this.app.vault.adapter.exists(normalizePath(e))) {
|
||||
const f = files.find((f) => f.path == e);
|
||||
if (f) {
|
||||
await this.updateIntoDB(f);
|
||||
Logger(`Batch save:${e}`);
|
||||
}
|
||||
const f = this.app.vault.getAbstractFileByPath(normalizePath(e));
|
||||
if (f && f instanceof TFile) {
|
||||
await this.updateIntoDB(f);
|
||||
Logger(`Batch save:${e}`);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(`Batch save error:${e}`, LOG_LEVEL.NOTICE);
|
||||
@@ -358,25 +365,37 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
async watchVaultRenameAsync(file: TAbstractFile, oldFile: any) {
|
||||
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||
await this.applyBatchChange();
|
||||
try {
|
||||
await this.applyBatchChange();
|
||||
} catch (ex) {
|
||||
Logger(ex);
|
||||
}
|
||||
if (file instanceof TFolder) {
|
||||
const newFiles = this.GetAllFilesRecursively(file);
|
||||
// for guard edge cases. this won't happen and each file's event will be raise.
|
||||
for (const i of newFiles) {
|
||||
const newFilePath = normalizePath(this.getFilePath(i));
|
||||
const newFile = this.app.vault.getAbstractFileByPath(newFilePath);
|
||||
if (newFile instanceof TFile) {
|
||||
Logger(`save ${newFile.path} into db`);
|
||||
await this.updateIntoDB(newFile);
|
||||
try {
|
||||
const newFilePath = normalizePath(this.getFilePath(i));
|
||||
const newFile = this.app.vault.getAbstractFileByPath(newFilePath);
|
||||
if (newFile instanceof TFile) {
|
||||
Logger(`save ${newFile.path} into db`);
|
||||
await this.updateIntoDB(newFile);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(ex);
|
||||
}
|
||||
}
|
||||
Logger(`delete below ${oldFile} from db`);
|
||||
await this.deleteFromDBbyPath(oldFile);
|
||||
} else if (file instanceof TFile) {
|
||||
Logger(`file save ${file.path} into db`);
|
||||
await this.updateIntoDB(file);
|
||||
Logger(`deleted ${oldFile} into db`);
|
||||
await this.deleteFromDBbyPath(oldFile);
|
||||
try {
|
||||
Logger(`file save ${file.path} into db`);
|
||||
await this.updateIntoDB(file);
|
||||
Logger(`deleted ${oldFile} into db`);
|
||||
await this.deleteFromDBbyPath(oldFile);
|
||||
} catch (ex) {
|
||||
Logger(ex);
|
||||
}
|
||||
}
|
||||
this.gcHook();
|
||||
}
|
||||
@@ -398,9 +417,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100);
|
||||
console.log(valutName + ":" + newmessage);
|
||||
// if (this.statusBar2 != null) {
|
||||
// this.statusBar2.setText(newmessage.substring(0, 60));
|
||||
// }
|
||||
|
||||
if (level >= LOG_LEVEL.NOTICE) {
|
||||
if (messagecontent in this.notifies) {
|
||||
@@ -468,7 +484,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
try {
|
||||
const newfile = await this.app.vault.createBinary(normalizePath(path), bin, { ctime: doc.ctime, mtime: doc.mtime });
|
||||
Logger("live : write to local (newfile:b) " + path);
|
||||
await this.app.vault.trigger("create", newfile);
|
||||
this.app.vault.trigger("create", newfile);
|
||||
} catch (ex) {
|
||||
Logger("could not write to local (newfile:bin) " + path, LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
@@ -483,7 +499,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
try {
|
||||
const newfile = await this.app.vault.create(normalizePath(path), doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
||||
Logger("live : write to local (newfile:p) " + path);
|
||||
await this.app.vault.trigger("create", newfile);
|
||||
this.app.vault.trigger("create", newfile);
|
||||
} catch (ex) {
|
||||
Logger("could not write to local (newfile:plain) " + path, LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
@@ -544,7 +560,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
try {
|
||||
await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime });
|
||||
Logger(msg);
|
||||
await this.app.vault.trigger("modify", file);
|
||||
this.app.vault.trigger("modify", file);
|
||||
} catch (ex) {
|
||||
Logger("could not write to local (modify:bin) " + path, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
@@ -558,7 +574,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
try {
|
||||
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
||||
Logger(msg);
|
||||
await this.app.vault.trigger("modify", file);
|
||||
this.app.vault.trigger("modify", file);
|
||||
} catch (ex) {
|
||||
Logger("could not write to local (modify:plain) " + path, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
@@ -574,20 +590,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
async handleDBChanged(change: EntryBody) {
|
||||
const allfiles = this.app.vault.getFiles();
|
||||
const targetFiles = allfiles.filter((e) => e.path == id2path(change._id));
|
||||
if (targetFiles.length == 0) {
|
||||
const targetFile = this.app.vault.getAbstractFileByPath(id2path(change._id));
|
||||
if (targetFile == null) {
|
||||
if (change._deleted) {
|
||||
return;
|
||||
}
|
||||
const doc = change;
|
||||
await this.doc2storage_create(doc);
|
||||
}
|
||||
if (targetFiles.length == 1) {
|
||||
} else if (targetFile instanceof TFile) {
|
||||
const doc = change;
|
||||
const file = targetFiles[0];
|
||||
const file = targetFile;
|
||||
await this.doc2storate_modify(doc, file);
|
||||
await this.showIfConflicted(file);
|
||||
this.queueConflictedCheck(file);
|
||||
} else {
|
||||
Logger(`${id2path(change._id)} is already exist as the folder`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,7 +736,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
let waiting = "";
|
||||
if (this.settings.batchSave) {
|
||||
waiting = " " + this.batchFileChange.map((e) => "🛫").join("");
|
||||
waiting = waiting.replace(/🛫{10}/g, "🚀");
|
||||
waiting = waiting.replace(/(🛫){10}/g, "🚀");
|
||||
}
|
||||
const message = `Sync:${w} ↑${sent} ↓${arrived}${waiting}`;
|
||||
this.setStatusBarText(message);
|
||||
@@ -909,7 +925,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
// --> conflict resolving
|
||||
async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> {
|
||||
try {
|
||||
const doc = await this.localDatabase.getDBEntry(path, { rev: rev });
|
||||
const doc = await this.localDatabase.getDBEntry(path, { rev: rev }, false, false);
|
||||
if (doc === false) return false;
|
||||
let data = doc.data;
|
||||
if (doc.datatype == "newnote") {
|
||||
@@ -936,7 +952,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
* @returns true -> resolved, false -> nothing to do, or check result.
|
||||
*/
|
||||
async getConflictedStatus(path: string): Promise<diff_check_result> {
|
||||
const test = await this.localDatabase.getDBEntry(path, { conflicts: true });
|
||||
const test = await this.localDatabase.getDBEntry(path, { conflicts: true }, false, false);
|
||||
if (test === false) return false;
|
||||
if (test == null) return false;
|
||||
if (!test._conflicts) return false;
|
||||
@@ -990,69 +1006,110 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
diff: diff,
|
||||
};
|
||||
}
|
||||
showMergeDialog(file: TFile, conflictCheckResult: diff_result): Promise<boolean> {
|
||||
return new Promise((res, rej) => {
|
||||
Logger("open conflict dialog", LOG_LEVEL.VERBOSE);
|
||||
new ConflictResolveModal(this.app, conflictCheckResult, async (selected) => {
|
||||
const testDoc = await this.localDatabase.getDBEntry(file.path, { conflicts: true });
|
||||
if (testDoc === false) {
|
||||
Logger("Missing file..", LOG_LEVEL.VERBOSE);
|
||||
return res(true);
|
||||
}
|
||||
if (!testDoc._conflicts) {
|
||||
Logger("Nothing have to do with this conflict", LOG_LEVEL.VERBOSE);
|
||||
return res(true);
|
||||
}
|
||||
const toDelete = selected;
|
||||
const toKeep = conflictCheckResult.left.rev != toDelete ? conflictCheckResult.left.rev : conflictCheckResult.right.rev;
|
||||
if (toDelete == "") {
|
||||
//concat both,
|
||||
// write data,and delete both old rev.
|
||||
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
|
||||
await this.app.vault.modify(file, p);
|
||||
await this.updateIntoDB(file);
|
||||
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.left.rev });
|
||||
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.right.rev });
|
||||
await this.pullFile(file.path);
|
||||
Logger("concat both file");
|
||||
setTimeout(() => {
|
||||
//resolved, check again.
|
||||
this.showIfConflicted(file);
|
||||
}, 500);
|
||||
} else if (toDelete == null) {
|
||||
Logger("Leave it still conflicted");
|
||||
} else {
|
||||
Logger(`resolved conflict:${file.path}`);
|
||||
await this.localDatabase.deleteDBEntry(file.path, { rev: toDelete });
|
||||
await this.pullFile(file.path, null, true, toKeep);
|
||||
setTimeout(() => {
|
||||
//resolved, check again.
|
||||
this.showIfConflicted(file);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return res(true);
|
||||
}).open();
|
||||
});
|
||||
}
|
||||
conflictedCheckFiles: string[] = [];
|
||||
|
||||
// queueing the conflicted file check
|
||||
conflictedCheckTimer: number;
|
||||
queueConflictedCheck(file: TFile) {
|
||||
this.conflictedCheckFiles = this.conflictedCheckFiles.filter((e) => e != file.path);
|
||||
this.conflictedCheckFiles.push(file.path);
|
||||
if (this.conflictedCheckTimer != null) {
|
||||
window.clearTimeout(this.conflictedCheckTimer);
|
||||
}
|
||||
this.conflictedCheckTimer = window.setTimeout(async () => {
|
||||
this.conflictedCheckTimer = null;
|
||||
const checkFiles = JSON.parse(JSON.stringify(this.conflictedCheckFiles)) as string[];
|
||||
for (const filename of checkFiles) {
|
||||
try {
|
||||
const file = this.app.vault.getAbstractFileByPath(filename);
|
||||
if (file != null && file instanceof TFile) {
|
||||
await this.showIfConflicted(file);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(ex);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
async showIfConflicted(file: TFile) {
|
||||
await runWithLock("conflicted", false, async () => {
|
||||
const conflictCheckResult = await this.getConflictedStatus(file.path);
|
||||
if (conflictCheckResult === false) return; //nothign to do.
|
||||
if (conflictCheckResult === false) {
|
||||
//nothign to do.
|
||||
return;
|
||||
}
|
||||
if (conflictCheckResult === true) {
|
||||
//auto resolved, but need check again;
|
||||
Logger("conflict:Automatically merged, but we have to check it again");
|
||||
setTimeout(() => {
|
||||
this.showIfConflicted(file);
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
//there conflicts, and have to resolve ;
|
||||
const leaf = this.app.workspace.activeLeaf;
|
||||
if (leaf) {
|
||||
new ConflictResolveModal(this.app, conflictCheckResult, async (selected) => {
|
||||
const testDoc = await this.localDatabase.getDBEntry(file.path, { conflicts: true });
|
||||
if (testDoc === false) return;
|
||||
if (!testDoc._conflicts) {
|
||||
Logger("something went wrong on merging.", LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
const toDelete = selected;
|
||||
if (toDelete == null) {
|
||||
//concat both,
|
||||
// write data,and delete both old rev.
|
||||
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
|
||||
await this.app.vault.modify(file, p);
|
||||
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.left.rev });
|
||||
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.right.rev });
|
||||
return;
|
||||
}
|
||||
if (toDelete == "") {
|
||||
return;
|
||||
}
|
||||
Logger(`resolved conflict:${file.path}`);
|
||||
await this.localDatabase.deleteDBEntry(file.path, { rev: toDelete });
|
||||
await this.pullFile(file.path, null, true);
|
||||
setTimeout(() => {
|
||||
//resolved, check again.
|
||||
this.showIfConflicted(file);
|
||||
}, 500);
|
||||
}).open();
|
||||
}
|
||||
await this.showMergeDialog(file, conflictCheckResult);
|
||||
});
|
||||
}
|
||||
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
|
||||
if (!fileList) {
|
||||
fileList = this.app.vault.getFiles();
|
||||
}
|
||||
const targetFiles = fileList.filter((e) => e.path == id2path(filename));
|
||||
if (targetFiles.length == 0) {
|
||||
const targetFile = this.app.vault.getAbstractFileByPath(id2path(filename));
|
||||
if (targetFile == null) {
|
||||
//have to create;
|
||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||
if (doc === false) return;
|
||||
await this.doc2storage_create(doc, force);
|
||||
} else if (targetFiles.length == 1) {
|
||||
} else if (targetFile instanceof TFile) {
|
||||
//normal case
|
||||
const file = targetFiles[0];
|
||||
const file = targetFile;
|
||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||
if (doc === false) return;
|
||||
await this.doc2storate_modify(doc, file, force);
|
||||
} else {
|
||||
Logger(`target files:${filename} is two or more files in your vault`);
|
||||
Logger(`target files:${filename} is exists as the folder`);
|
||||
//something went wrong..
|
||||
}
|
||||
//when to opened file;
|
||||
@@ -1105,17 +1162,21 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
children: [],
|
||||
datatype: datatype,
|
||||
};
|
||||
//From here
|
||||
const old = await this.localDatabase.getDBEntry(fullpath, null, false, false);
|
||||
if (old !== false) {
|
||||
const oldData = { data: old.data, deleted: old._deleted };
|
||||
const newData = { data: d.data, deleted: d._deleted };
|
||||
if (JSON.stringify(oldData) == JSON.stringify(newData)) {
|
||||
Logger("not changed:" + fullpath + (d._deleted ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
|
||||
return;
|
||||
//upsert should locked
|
||||
const isNotChanged = await runWithLock("file:" + fullpath, false, async () => {
|
||||
const old = await this.localDatabase.getDBEntry(fullpath, null, false, false);
|
||||
if (old !== false) {
|
||||
const oldData = { data: old.data, deleted: old._deleted };
|
||||
const newData = { data: d.data, deleted: d._deleted };
|
||||
if (JSON.stringify(oldData) == JSON.stringify(newData)) {
|
||||
Logger("not changed:" + fullpath + (d._deleted ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
|
||||
return true;
|
||||
}
|
||||
// d._rev = old._rev;
|
||||
}
|
||||
// d._rev = old._rev;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (isNotChanged) return;
|
||||
await this.localDatabase.putDBEntry(d);
|
||||
|
||||
Logger("put database:" + fullpath + "(" + datatype + ") ");
|
||||
@@ -1167,7 +1228,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
return { plugins, allPlugins, thisDevicePlugins };
|
||||
}
|
||||
async sweepPlugin(showMessage = false) {
|
||||
console.log(`pluginSync:${this.settings.usePluginSync}`);
|
||||
if (!this.settings.usePluginSync) return;
|
||||
await runWithLock("sweepplugin", false, async () => {
|
||||
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
||||
|
||||
@@ -56,6 +56,8 @@ export interface ObsidianLiveSyncSettings {
|
||||
autoSweepPluginsPeriodic: boolean;
|
||||
notifyPluginOrSettingUpdated: boolean;
|
||||
checkIntegrityOnSave: boolean;
|
||||
batch_size: number;
|
||||
batches_limit: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
|
||||
@@ -94,6 +96,8 @@ export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
|
||||
autoSweepPluginsPeriodic: false,
|
||||
notifyPluginOrSettingUpdated: false,
|
||||
checkIntegrityOnSave: false,
|
||||
batch_size: 250,
|
||||
batches_limit: 40,
|
||||
};
|
||||
|
||||
export const PERIODIC_PLUGIN_SWEEP = 60;
|
||||
|
||||
17
src/utils.ts
17
src/utils.ts
@@ -72,7 +72,7 @@ export function resolveWithIgnoreKnownError<T>(p: Promise<T>, def: T): Promise<T
|
||||
|
||||
export function isValidPath(filename: string): boolean {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const regex = /[\u0000-\u001f]|[\\"':?<>|*]/g;
|
||||
const regex = /[\u0000-\u001f]|[\\":?<>|*#]/g;
|
||||
let x = filename.replace(regex, "_");
|
||||
const win = /(\\|\/)(COM\d|LPT\d|CON|PRN|AUX|NUL|CLOCK$)($|\.)/gi;
|
||||
const sx = (x = x.replace(win, "/_"));
|
||||
@@ -114,16 +114,16 @@ function objectToKey(key: any): string {
|
||||
const keys = Object.keys(key).sort((a, b) => a.localeCompare(b));
|
||||
return keys.map((e) => e + objectToKey(key[e])).join(":");
|
||||
}
|
||||
// Just run some async/await as like transacion SERIALIZABLE
|
||||
|
||||
// Just run async/await as like transacion ISOLATION SERIALIZABLE
|
||||
export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: () => Promise<T>): Promise<T> {
|
||||
Logger(`Lock:${key}:enter`, LOG_LEVEL.VERBOSE);
|
||||
// Logger(`Lock:${key}:enter`, LOG_LEVEL.VERBOSE);
|
||||
const lockKey = typeof key === "string" ? key : objectToKey(key);
|
||||
const handleNextProcs = () => {
|
||||
if (typeof pendingProcs[lockKey] === "undefined") {
|
||||
//simply unlock
|
||||
runningProcs.remove(lockKey);
|
||||
Logger(`Lock:${lockKey}:released`, LOG_LEVEL.VERBOSE);
|
||||
// Logger(`Lock:${lockKey}:released`, LOG_LEVEL.VERBOSE);
|
||||
} else {
|
||||
Logger(`Lock:${lockKey}:left ${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE);
|
||||
let nextProc = null;
|
||||
@@ -143,6 +143,10 @@ export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: (
|
||||
handleNextProcs();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (pendingProcs && lockKey in pendingProcs && pendingProcs[lockKey].length == 0) {
|
||||
delete pendingProcs[lockKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -164,7 +168,7 @@ export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: (
|
||||
new Promise<void>((res, rej) => {
|
||||
proc()
|
||||
.then((v) => {
|
||||
Logger(`Lock:${key}:processed`, LOG_LEVEL.VERBOSE);
|
||||
// Logger(`Lock:${key}:processed`, LOG_LEVEL.VERBOSE);
|
||||
handleNextProcs();
|
||||
responderRes(v);
|
||||
res();
|
||||
@@ -178,10 +182,11 @@ export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: (
|
||||
});
|
||||
|
||||
pendingProcs[lockKey].push(subproc);
|
||||
// Logger(`Lock:${lockKey}:queud:left${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE);
|
||||
return responder;
|
||||
} else {
|
||||
runningProcs.push(lockKey);
|
||||
Logger(`Lock:${lockKey}:aqquired`, LOG_LEVEL.VERBOSE);
|
||||
// Logger(`Lock:${lockKey}:aqquired`, LOG_LEVEL.VERBOSE);
|
||||
return new Promise((res, rej) => {
|
||||
proc()
|
||||
.then((v) => {
|
||||
|
||||
Reference in New Issue
Block a user