Compare commits

..

12 Commits

Author SHA1 Message Date
vorotamoroz e31e761c5a Merge pull request #1028 from vrtmrz/0_25_83
Releasing 0.25.83
2026-07-16 22:41:26 +09:00
vorotamoroz 977d50566d docs: finalise 0.25.83 release notes 2026-07-16 13:01:32 +00:00
github-actions[bot] 85fc62d61c Releasing 0.25.83 2026-07-16 12:28:26 +00:00
Ouyang Xingyuan 8e0d5581f7 test: cover the non-bulk newest resolution entry point
Reason:
- The non-bulk notice regression test should exercise the real resolve-by-newest call path.

Changes:
- Resolve a mocked conflict through `_anyResolveConflictByNewest` and assert that its success notice remains visible.
2026-07-16 21:18:16 +09:00
Ouyang Xingyuan 32e992db93 fix: scope merge notice suppression to bulk resolution
Reason:
- Bulk newest resolution should not display one success notice per file, while non-bulk resolution must retain its existing notice.

Changes:
- Pass an explicit notice flag only through the bulk resolution path.
- Cover bulk suppression, non-bulk notices, and ten-file progress updates.
2026-07-16 21:18:16 +09:00
Ouyang Xingyuan 3103dc4f52 fix: suppress bulk conflict resolution notice spam
Bulk newest-revision conflict resolution created one notice per resolved file
and updated progress for nine out of every ten files.

Keep per-file bulk resolution success messages in the log without displaying
notices, update notice-level progress once every ten files, and cover both
behaviours with unit tests.
2026-07-16 21:18:16 +09:00
vorotamoroz 943b2a3bd4 Point at merged commonlib rename guard 2026-07-16 20:48:12 +09:00
vorotamoroz 69f4879589 test: cover case-only file renames in Obsidian 2026-07-16 20:48:12 +09:00
vorotamoroz 918d3b5547 fix: guard case-only rename recovery 2026-07-16 20:48:12 +09:00
metrovoc 869a893c19 fix: synchronise case-only file renames safely 2026-07-16 20:48:12 +09:00
vorotamoroz ba7ea27d0c feat: distinguish remote operation and request activity 2026-07-16 20:13:10 +09:00
vorotamoroz 9f9758bc12 fix: align chunk reads with remote delivery
Consume commonlib's delivery lifecycle, classify finite replication entry points, and document why the five-minute inactivity fuse is only a leak safety valve. Record the remote-activity counter correction and preserve continuous replication's unbounded live channel.
2026-07-16 20:13:10 +09:00
10 changed files with 147 additions and 19 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.25.82",
"version": "0.25.83",
"minAppVersion": "1.7.2",
"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",
+5 -5
View File
@@ -1,12 +1,12 @@
{
"name": "obsidian-livesync",
"version": "0.25.82",
"version": "0.25.83",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "0.25.82",
"version": "0.25.83",
"license": "MIT",
"workspaces": [
"src/apps/cli",
@@ -16226,7 +16226,7 @@
},
"src/apps/cli": {
"name": "self-hosted-livesync-cli",
"version": "0.25.82-cli",
"version": "0.25.83-cli",
"dependencies": {
"chokidar": "^4.0.0",
"minimatch": "^10.2.5",
@@ -16252,7 +16252,7 @@
},
"src/apps/webapp": {
"name": "livesync-webapp",
"version": "0.25.82-webapp",
"version": "0.25.83-webapp",
"dependencies": {
"octagonal-wheels": "^0.1.51"
},
@@ -16267,7 +16267,7 @@
}
},
"src/apps/webpeer": {
"version": "0.25.82-webpeer",
"version": "0.25.83-webpeer",
"dependencies": {
"octagonal-wheels": "^0.1.51"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.25.82",
"version": "0.25.83",
"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",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "self-hosted-livesync-cli",
"private": true,
"version": "0.25.82-cli",
"version": "0.25.83-cli",
"main": "dist/index.cjs",
"type": "module",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "livesync-webapp",
"private": true,
"version": "0.25.82-webapp",
"version": "0.25.83-webapp",
"type": "module",
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "webpeer",
"private": true,
"version": "0.25.82-webpeer",
"version": "0.25.83-webpeer",
"type": "module",
"scripts": {
"dev": "vite",
@@ -30,7 +30,8 @@ export class ModuleConflictResolver extends AbstractModule {
private async _resolveConflictByDeletingRev(
path: FilePathWithPrefix,
deleteRevision: string,
subTitle = ""
subTitle = "",
showNotice = true
): Promise<typeof MISSING_OR_ERROR | typeof AUTO_MERGED> {
const title = `Resolving ${subTitle ? `[${subTitle}]` : ""}:`;
if (!(await this.core.fileHandler.deleteRevisionFromDB(path, deleteRevision))) {
@@ -58,7 +59,7 @@ export class ModuleConflictResolver extends AbstractModule {
this._log(`Could not write the resolved content to the storage: ${path}`, LOG_LEVEL_NOTICE);
return MISSING_OR_ERROR;
}
const level = subTitle.indexOf("same") !== -1 ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE;
const level = subTitle.indexOf("same") !== -1 || !showNotice ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE;
this._log(`${path} has been merged automatically`, level);
return AUTO_MERGED;
}
@@ -165,7 +166,7 @@ export class ModuleConflictResolver extends AbstractModule {
});
}
private async _anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> {
private async _anyResolveConflictByNewest(filename: FilePathWithPrefix, showNotice = true): Promise<boolean> {
const currentRev = await this.core.databaseFileAccess.fetchEntryMeta(filename, undefined, true);
if (currentRev == false) {
this._log(`Could not get current revision of ${filename}`);
@@ -203,7 +204,7 @@ export class ModuleConflictResolver extends AbstractModule {
this._log(
`conflict: Deleting the older revision ${mTimeAndRev[i][1]} (${new Date(mTimeAndRev[i][0]).toLocaleString()}) of ${filename}`
);
await this.services.conflict.resolveByDeletingRevision(filename, mTimeAndRev[i][1], "NEWEST");
await this._resolveConflictByDeletingRev(filename, mTimeAndRev[i][1], "NEWEST", showNotice);
}
return true;
}
@@ -214,13 +215,14 @@ export class ModuleConflictResolver extends AbstractModule {
let i = 0;
for (const file of files) {
if (i++ % 10)
i++;
if (i % 10 === 0)
this._log(
`Check and Processing ${i} / ${files.length}`,
LOG_LEVEL_NOTICE,
"resolveAllConflictedFilesByNewerOnes"
);
await this.services.conflict.resolveByNewest(file);
await this._anyResolveConflictByNewest(file, false);
}
this._log(`Done!`, LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes");
}
@@ -0,0 +1,118 @@
import { describe, expect, it, vi } from "vitest";
import {
DEFAULT_SETTINGS,
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
type FilePathWithPrefix,
type MetaEntry,
} from "@lib/common/types";
import { ModuleConflictResolver } from "./ModuleConflictResolver";
function createModule(files: FilePathWithPrefix[] = []) {
const core = {
_services: {
API: {
addLog: vi.fn(),
addCommand: vi.fn(),
registerWindow: vi.fn(),
addRibbonIcon: vi.fn(),
registerProtocolHandler: vi.fn(),
},
setting: {
saveSettingData: vi.fn(async () => undefined),
},
conflict: {
resolveByNewest: vi.fn(async () => true),
},
},
settings: DEFAULT_SETTINGS,
fileHandler: {
deleteRevisionFromDB: vi.fn(async () => true),
dbToStorage: vi.fn(async () => true),
},
databaseFileAccess: {
getConflictedRevs: vi.fn(async () => []),
},
storageAccess: {
getFileNames: vi.fn(async () => files),
},
} as any;
Object.defineProperty(core, "services", { get: () => core._services });
const module = new ModuleConflictResolver(core);
module._log = vi.fn();
return { module };
}
describe("ModuleConflictResolver bulk newest resolution", () => {
it("retains the success notice for a non-bulk newest resolution", async () => {
const { module } = createModule();
const path = "example.md" as FilePathWithPrefix;
module.core.databaseFileAccess.fetchEntryMeta = vi.fn(
async (_path: unknown, rev?: string): Promise<MetaEntry> =>
({
_id: "doc-id",
_rev: rev ?? "2-current",
path,
ctime: 1,
mtime: rev ? 1 : 2,
size: 0,
children: [],
type: "plain",
eden: {},
}) as unknown as MetaEntry
);
module.core.databaseFileAccess.getConflictedRevs = vi
.fn()
.mockResolvedValueOnce(["1-old"])
.mockResolvedValue([]);
await (module as any)._anyResolveConflictByNewest(path);
expect(module._log).toHaveBeenLastCalledWith(`${path} has been merged automatically`, LOG_LEVEL_NOTICE);
});
it("logs a successful bulk newest resolution without displaying a notice", async () => {
const { module } = createModule();
const path = "example.md" as FilePathWithPrefix;
module.core.databaseFileAccess.fetchEntryMeta = vi.fn(
async (_path: unknown, rev?: string): Promise<MetaEntry> =>
({
_id: "doc-id",
_rev: rev ?? "2-current",
path,
ctime: 1,
mtime: rev ? 1 : 2,
size: 0,
children: [],
type: "plain",
eden: {},
}) as unknown as MetaEntry
);
module.core.databaseFileAccess.getConflictedRevs = vi
.fn()
.mockResolvedValueOnce(["1-old"])
.mockResolvedValue([]);
await (module as any)._anyResolveConflictByNewest(path, false);
expect(module._log).toHaveBeenLastCalledWith(`${path} has been merged automatically`, LOG_LEVEL_INFO);
});
it("updates notice-level progress once every ten checked files", async () => {
const files = Array.from({ length: 11 }, (_, index) => `note-${index}.md` as FilePathWithPrefix);
const { module } = createModule(files);
const resolveByNewest = vi.spyOn(module as any, "_anyResolveConflictByNewest").mockResolvedValue(true);
await (module as any)._resolveAllConflictedFilesByNewerOnes();
expect(resolveByNewest).toHaveBeenCalledTimes(11);
expect(resolveByNewest).toHaveBeenCalledWith(files[0], false);
expect(module._log).toHaveBeenCalledWith(
"Check and Processing 10 / 11",
LOG_LEVEL_NOTICE,
"resolveAllConflictedFilesByNewerOnes"
);
expect(module._log).toHaveBeenCalledTimes(3);
});
});
+9 -2
View File
@@ -5,11 +5,18 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
## Unreleased
## 0.25.83
16th July, 2026
Our plug-in continues to improve every day thanks to all of your contributions. We have finally resolved an issue first reported in 2023. Thank you for everything you contribute.
### Fixed
- Fixed the 📲 remote-activity indicator remaining visible after CouchDB requests had completed.
- Fixed missing chunks being reported unavailable while an in-progress on-demand fetch or finite replication could still deliver them. Reads now follow the actual delivery lifecycle and recheck the local database when it finishes.
- Fixed an issue where changing only the letter case of a file name within the same directory could delete it on other devices when 'Handle files as Case-Sensitive' was disabled. Directory case changes remain unsupported (#198).
- Fixed an issue where changing only the letter case of a file name within the same directory could delete it on other devices when 'Handle files as Case-Sensitive' was disabled. Directory case changes remain unsupported (#198, PR #1014; [commonlib PR #68](https://github.com/vrtmrz/livesync-commonlib/pull/68)). Thank you to @metrovoc for the fix!
- Fixed **Resolve All conflicted files by the newer one** displaying a separate success notice for every resolved file and updating its progress notice for nine out of every ten checked files. Successful per-file results are now logged, progress updates every ten files, and errors and non-bulk success notices remain visible (#1016, PR #1017). Thank you to @apple-ouyang for the fix!
### Improved
@@ -23,7 +30,7 @@ Recently, I created a repository called Fancy Kit and have been trying to build
### Fixed
- Refreshed the remote Security Seed before each replication, preventing a client that remained open during a remote database rebuild from uploading data encrypted with the previous seed (#1018).
- Refreshed the remote Security Seed before each replication, preventing a client that remained open during a remote database rebuild from uploading data encrypted with the previous seed (#1018, PR #1019). Thank you to @apple-ouyang for the fix!
- The P2P **Start Sync & Close** action now waits for synchronisation to settle before closing the dialogue, avoiding premature release of screen-awake protection while work remains in flight.
### Improved
+2 -1
View File
@@ -4,5 +4,6 @@
"1.0.1": "0.9.12",
"1.0.0": "0.9.7",
"0.25.81": "1.7.2",
"0.25.82": "1.7.2"
"0.25.82": "1.7.2",
"0.25.83": "1.7.2"
}