From a3231b09064f76d244c1620991a589331c54ff8b Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 18 Sep 2026 03:34:31 +0000 Subject: [PATCH 1/4] Reuse Security Seed reads within each resource --- src/common/replicatorResources.unit.spec.ts | 40 +++++++++++++++++++ .../replicatorResources/securitySeed.ts | 13 +++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/common/replicatorResources.unit.spec.ts b/src/common/replicatorResources.unit.spec.ts index fa0bba62..2215a48a 100644 --- a/src/common/replicatorResources.unit.spec.ts +++ b/src/common/replicatorResources.unit.spec.ts @@ -285,6 +285,46 @@ describe("replicator probe factories", () => { expect(objectReplicator.closeReplication).toHaveBeenCalledOnce(); }); + it("reads the Security Seed once per resource, including concurrent reads, and refreshes for a new resource", async () => { + const settings = createSettings(); + const factory = createCouchDBSecuritySeedResourceFactory({} as never); + const firstResource = await factory(settings); + const firstReplicator = mocks.couchDB[0]; + const firstSeed = new Uint8Array([1]); + firstReplicator.getReplicationPBKDF2Salt.mockResolvedValue(firstSeed); + + const [first, concurrent] = await Promise.all([firstResource.read(), firstResource.read()]); + expect(first).toBe(firstSeed); + expect(concurrent).toBe(firstSeed); + await expect(firstResource.read()).resolves.toBe(firstSeed); + expect(firstReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledOnce(); + expect(firstReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledWith({ ...settings }, true); + await firstResource.dispose(); + + const nextResource = await factory(settings); + const nextReplicator = mocks.couchDB[1]; + const nextSeed = new Uint8Array([2]); + nextReplicator.getReplicationPBKDF2Salt.mockResolvedValue(nextSeed); + await expect(nextResource.read()).resolves.toBe(nextSeed); + expect(nextReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledOnce(); + expect(nextReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledWith({ ...settings }, true); + await nextResource.dispose(); + }); + + it("retries a failed Security Seed read within the same resource", async () => { + const resource = await createCouchDBSecuritySeedResourceFactory({} as never)(createSettings()); + const replicator = mocks.couchDB[0]; + const failure = new Error("connection interrupted"); + const seed = new Uint8Array([1]); + replicator.getReplicationPBKDF2Salt.mockRejectedValueOnce(failure).mockResolvedValueOnce(seed); + + await expect(resource.read()).rejects.toBe(failure); + await expect(resource.read()).resolves.toBe(seed); + await expect(resource.read()).resolves.toBe(seed); + expect(replicator.getReplicationPBKDF2Salt).toHaveBeenCalledTimes(2); + await resource.dispose(); + }); + it("checks synchronisation information through an owned connection and disposes the private Replicator", async () => { const settings = createSettings(); const snapshot = { ...settings }; diff --git a/src/common/replicatorResources/securitySeed.ts b/src/common/replicatorResources/securitySeed.ts index 6abf91b9..e8adafbd 100644 --- a/src/common/replicatorResources/securitySeed.ts +++ b/src/common/replicatorResources/securitySeed.ts @@ -21,8 +21,19 @@ function createSecuritySeedResourceFactory( return (setting) => { const snapshot = snapshotRemoteSettings(setting); const replicator = createReplicator(); + let readPromise: Promise> | undefined; + const read = () => { + if (readPromise) return readPromise; + const pending = Promise.resolve().then(() => replicator.getReplicationPBKDF2Salt(snapshot, true)); + readPromise = pending; + // A failed read must not poison a later retry within the same resource. + void pending.catch(() => { + if (readPromise === pending) readPromise = undefined; + }); + return pending; + }; return Promise.resolve({ - read: () => replicator.getReplicationPBKDF2Salt(snapshot, true), + read, dispose: createReplicatorDisposer(replicator), }); }; From 8ea656a7dac75b3d4ffac0bc5f5485602db473cd Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 18 Sep 2026 07:01:07 +0000 Subject: [PATCH 2/4] Use Commonlib 0.1.27 for Journal transfer readiness --- package-lock.json | 8 ++++---- package.json | 2 +- src/common/replicatorProviders.ts | 3 ++- src/common/replicatorProviders.unit.spec.ts | 6 ++++++ 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index a3febe7b..ffdefc91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "0.1.26", + "@vrtmrz/livesync-commonlib": "0.1.27", "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", @@ -4567,9 +4567,9 @@ } }, "node_modules/@vrtmrz/livesync-commonlib": { - "version": "0.1.26", - "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.26.tgz", - "integrity": "sha512-AVJky976PP1M+g18im6tJ1eKSq82uN73vkS98WWZWvMJ1UDz6O8YRVWa9dkFakAVXecdxxxLkiD45g5HTRnn6Q==", + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.27.tgz", + "integrity": "sha512-n/AIqXPGN9Z7zmq0QSKnlE5uY5hTZgXW6VM6NJERR0/ggx3TTv1b3WsSbiLpCeU7Mu6KbPqC2BSjr4jCGvSdCA==", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", diff --git a/package.json b/package.json index 034037c5..89df1549 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "0.1.26", + "@vrtmrz/livesync-commonlib": "0.1.27", "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", diff --git a/src/common/replicatorProviders.ts b/src/common/replicatorProviders.ts index 527215e6..845d8c2e 100644 --- a/src/common/replicatorProviders.ts +++ b/src/common/replicatorProviders.ts @@ -3,6 +3,7 @@ import { CAPABILITY_NOT_APPLICABLE, CENTRAL_REMOTE_REPLICATION_READINESS, NO_INTERACTION, + PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS, REPLICATION_PROGRESS_PRESENTATIONS, REMOTE_RESOURCE_KINDS, defineReplicatorProviderDefinitions, @@ -134,7 +135,7 @@ export function createCentralReplicatorProviderDefinitions( [REMOTE_MINIO]: { kind: REMOTE_MINIO, diagnosticName: "Object Storage", - readiness: CENTRAL_REMOTE_REPLICATION_READINESS, + readiness: PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS, isConfigured: (settings) => settings.remoteType === REMOTE_MINIO && !!settings.endpoint?.trim() && !!settings.bucket?.trim(), configurationIdentity: getObjectStorageReplicatorConfigurationIdentity, diff --git a/src/common/replicatorProviders.unit.spec.ts b/src/common/replicatorProviders.unit.spec.ts index fecfc4f3..54a2c441 100644 --- a/src/common/replicatorProviders.unit.spec.ts +++ b/src/common/replicatorProviders.unit.spec.ts @@ -47,6 +47,12 @@ describe("central Replicator provider definitions", () => { .toEqual(["connection", "preferred-tweak", "security-seed", "synchronisation-information"].sort()); }); + it("lets Journal prepare its own fresh Security Seed while CouchDB uses central preparation", () => { + const definitions = createCentralReplicatorProviderDefinitions({} as never); + expect(definitions.get(REMOTE_COUCHDB)?.readiness.centralRemotePreparation).toBe("required"); + expect(definitions.get(REMOTE_MINIO)?.readiness.centralRemotePreparation).toBe("provider-owned"); + }); + it("composes CouchDB and Object Storage policies outside LiveSyncBaseCore", async () => { const host = {} as Parameters[0]; const definitions = createCentralReplicatorProviderDefinitions(host); From 916ec4f2764958fc38028263108f233b9d43642c Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 18 Sep 2026 07:03:34 +0000 Subject: [PATCH 3/4] Document restart conflict protection in 1.0.29 notes --- updates.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/updates.md b/updates.md index 261888fd..fc0b1040 100644 --- a/updates.md +++ b/updates.md @@ -27,6 +27,14 @@ Unusually for this project, I have added a feature that relies on a particular i - Managed TURN settings are saved with your encrypted P2P profile and included when you share it through a Setup URI or QR code. - Your API token is omitted from generated reports. +### Synchronisation + +#### Fixed + +- Fixed a case where unchanged local files could overwrite newer synchronised content after a restart. (#994) +- Local content whose origin cannot be established is now preserved as a conflict, allowing you to review and choose which version to keep. +- The same protection applies to ordinary file synchronisation in the command-line tool. + ### Command-line tool #### Fixed From fa97f9696070f1e78215546493e750632214411b Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 18 Sep 2026 08:14:38 +0000 Subject: [PATCH 4/4] Group pending synchronisation fixes in Unreleased notes --- updates.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/updates.md b/updates.md index fc0b1040..b6ca8127 100644 --- a/updates.md +++ b/updates.md @@ -12,6 +12,16 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi ## Unreleased +### Synchronisation + +#### Fixed + +- Fixed a case where unchanged local files could overwrite newer synchronised content after a restart. (#994) +- Local content whose origin cannot be established is now preserved as a conflict, allowing you to review and choose which version to keep. +- The same protection applies to ordinary file synchronisation in the command-line tool. +- Fast Fetch now avoids repeated requests for the same Security Seed during a transfer, reducing network traffic during initial setup. +- Object Storage synchronisation now reads fresh synchronisation parameters before it can write to the remote, then reuses them for that transfer. A failed parameter read stops the transfer before writing. + ## 1.0.29 16th September, 2026 @@ -27,14 +37,6 @@ Unusually for this project, I have added a feature that relies on a particular i - Managed TURN settings are saved with your encrypted P2P profile and included when you share it through a Setup URI or QR code. - Your API token is omitted from generated reports. -### Synchronisation - -#### Fixed - -- Fixed a case where unchanged local files could overwrite newer synchronised content after a restart. (#994) -- Local content whose origin cannot be established is now preserved as a conflict, allowing you to review and choose which version to keep. -- The same protection applies to ordinary file synchronisation in the command-line tool. - ### Command-line tool #### Fixed