mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-01 13:58:18 +00:00
Merge main and refresh translation resources
This commit is contained in:
commit
037e39c9ff
247 files changed
+23914
-2510
No files matched your search
@@ -19,6 +19,7 @@ export const EVENT_REQUEST_RUN_DOCTOR = "request-run-doctor";
|
||||
export const EVENT_REQUEST_RUN_FIX_INCOMPLETE = "request-run-fix-incomplete";
|
||||
|
||||
export const EVENT_ANALYSE_DB_USAGE = "analyse-db-usage";
|
||||
export const EVENT_INSPECT_DB_INTEGRITY = "inspect-db-integrity";
|
||||
export const EVENT_REQUEST_PERFORM_GC_V3 = "request-perform-gc-v3";
|
||||
// export const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size";
|
||||
// export const EVENT_FILE_CHANGED = "file-changed";
|
||||
@@ -37,6 +38,7 @@ declare global {
|
||||
[EVENT_REQUEST_RUN_DOCTOR]: string;
|
||||
[EVENT_REQUEST_RUN_FIX_INCOMPLETE]: undefined;
|
||||
[EVENT_ANALYSE_DB_USAGE]: undefined;
|
||||
[EVENT_INSPECT_DB_INTEGRITY]: undefined;
|
||||
[EVENT_REQUEST_PERFORM_GC_V3]: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,111 @@
|
||||
* remove it from this map in the same change.
|
||||
*/
|
||||
export const liveSyncProvisionalEnglishMessages = {
|
||||
"Hidden File Sync is disabled on this device. Enable it in Sync Settings before using Automatic or initialising hidden files.":
|
||||
"Hidden File Sync is disabled on this device. Enable it in Sync Settings before using Automatic or initialising hidden files.",
|
||||
"Hidden File Sync initialisation was not started.": "Hidden File Sync initialisation was not started.",
|
||||
"(RegExp) Limit normal files to matching paths. Leave empty to allow all normal file paths. Hidden File Sync uses its own target patterns.":
|
||||
"(RegExp) Limit normal files to matching paths. Leave empty to allow all normal file paths. Hidden File Sync uses its own target patterns.",
|
||||
"(RegExp) Skip changes to matching normal and hidden files, locally and remotely.":
|
||||
"(RegExp) Skip changes to matching normal and hidden files, locally and remotely.",
|
||||
"Unknown platform": "Unknown platform",
|
||||
"This removes only this device's record from the remote.":
|
||||
"This removes only this device's record from the remote.",
|
||||
"This remote is unlocked. A device that returns can register again.":
|
||||
"This remote is unlocked. A device that returns can register again.",
|
||||
"This remote is locked. Use the existing recovery process for devices that need to join again.":
|
||||
"This remote is locked. Use the existing recovery process for devices that need to join again.",
|
||||
"This remote is locked. If the device returns, use the existing recovery process to join it again.":
|
||||
"This remote is locked. If the device returns, use the existing recovery process to join it again.",
|
||||
"This remote does not support conditional updates, so the device record could not be removed.":
|
||||
"This remote does not support conditional updates, so the device record could not be removed.",
|
||||
"This remote does not currently have a record of joined devices.":
|
||||
"This remote does not currently have a record of joined devices.",
|
||||
"This list shows device information recorded by the remote. It does not show whether a device is online now.":
|
||||
"This list shows device information recorded by the remote. It does not show whether a device is online now.",
|
||||
"This device has been locked out. Use the existing recovery process to join again.":
|
||||
"This device has been locked out. Use the existing recovery process to join again.",
|
||||
"This device": "This device",
|
||||
"This remote has been cleaned. Use the existing recovery process to join devices again as needed.":
|
||||
"This remote has been cleaned. Use the existing recovery process to join devices again as needed.",
|
||||
"The device record was forgotten.": "The device record was forgotten.",
|
||||
"The device record was already absent.": "The device record was already absent.",
|
||||
"The device list changed while you were trying to forget this device. Refresh the list before trying again.":
|
||||
"The device list changed while you were trying to forget this device. Refresh the list before trying again.",
|
||||
"The device information could not be loaded. Close this dialogue and open Joined devices again if the remote configuration has changed.":
|
||||
"The device information could not be loaded. Close this dialogue and open Joined devices again if the remote configuration has changed.",
|
||||
"Review device records for the active CouchDB or Object Storage connection.":
|
||||
"Review device records for the active CouchDB or Object Storage connection.",
|
||||
Refresh: "Refresh",
|
||||
"Plug-in version": "Plug-in version",
|
||||
Platform: "Platform",
|
||||
"Older Self-hosted LiveSync versions detected: ${versions}.":
|
||||
"Older Self-hosted LiveSync versions detected: ${versions}.",
|
||||
"Newer Self-hosted LiveSync versions detected: ${versions}.":
|
||||
"Newer Self-hosted LiveSync versions detected: ${versions}.",
|
||||
"Reviewing and aligning Self-hosted LiveSync versions across devices is recommended.":
|
||||
"Reviewing and aligning Self-hosted LiveSync versions across devices is recommended.",
|
||||
Open: "Open",
|
||||
"Not recorded": "Not recorded",
|
||||
"Not accepted": "Not accepted",
|
||||
"No joined devices are recorded on this remote.": "No joined devices are recorded on this remote.",
|
||||
"Locked out": "Locked out",
|
||||
"Loading joined devices…": "Loading joined devices…",
|
||||
"Last recorded": "Last recorded",
|
||||
"Joined devices could not be updated. Refresh the list and try again. If the remote configuration has changed, close this dialogue and open Joined devices again.":
|
||||
"Joined devices could not be updated. Refresh the list and try again. If the remote configuration has changed, close this dialogue and open Joined devices again.",
|
||||
"Joined devices are available after database startup with an active CouchDB or Object Storage connection.":
|
||||
"Joined devices are available after database startup with an active CouchDB or Object Storage connection.",
|
||||
"Joined devices": "Joined devices",
|
||||
"Review devices": "Review devices",
|
||||
"Synchronisation between different plug-in versions is generally safe unless the connection is explicitly blocked. However, it may be less efficient or use more storage, and P2P synchronisation may be unavailable.":
|
||||
"Synchronisation between different plug-in versions is generally safe unless the connection is explicitly blocked. However, it may be less efficient or use more storage, and P2P synchronisation may be unavailable.",
|
||||
"Forgetting…": "Forgetting…",
|
||||
"Forgetting the record does not delete Vault files or revoke credentials.":
|
||||
"Forgetting the record does not delete Vault files or revoke credentials.",
|
||||
"Forget this device record?": "Forget this device record?",
|
||||
"Forget device": "Forget device",
|
||||
Close: "Close",
|
||||
Cancel: "Cancel",
|
||||
"App version": "App version",
|
||||
Accepted: "Accepted",
|
||||
"Preparing follow-up report…": "Preparing follow-up report…",
|
||||
"Cancelling local search. Checked ${COUNT} entries.": "Cancelling local search. Checked ${COUNT} entries.",
|
||||
"Search current Vault paths and current local Metadata only. Older history and remote-only files are not searched.":
|
||||
"Search current Vault paths and current local Metadata only. Older history and remote-only files are not searched.",
|
||||
"The lookup does not read file contents or contact a remote database. Follow-up reports include current conflicts and merge bases.":
|
||||
"The lookup does not read file contents or contact a remote database. Follow-up reports include current conflicts and merge bases.",
|
||||
"Local file diagnostic report": "Local file diagnostic report",
|
||||
"Checked ${COUNT} local entries.": "Checked ${COUNT} local entries.",
|
||||
"Enter a valid report path pseudonym.": "Enter a valid report path pseudonym.",
|
||||
"Search cancelled. Matches found so far are shown.": "Search cancelled. Matches found so far are shown.",
|
||||
"No matches were found in current Vault paths or current local Metadata. Older history and remote-only files are not searched.":
|
||||
"No matches were found in current Vault paths or current local Metadata. Older history and remote-only files are not searched.",
|
||||
"Found ${COUNT} local matches.": "Found ${COUNT} local matches.",
|
||||
"The local search could not finish.": "The local search could not finish.",
|
||||
"The file could not be opened.": "The file could not be opened.",
|
||||
"The follow-up report could not be prepared.": "The follow-up report could not be prepared.",
|
||||
"Report path pseudonym": "Report path pseudonym",
|
||||
Source: "Source",
|
||||
Metadata: "Metadata",
|
||||
Vault: "Vault",
|
||||
"Metadata document ID": "Metadata document ID",
|
||||
"No current Metadata document": "No current Metadata document",
|
||||
"Current Vault file": "Current Vault file",
|
||||
Present: "Present",
|
||||
Missing: "Missing",
|
||||
"Open file": "Open file",
|
||||
"Prepare follow-up report": "Prepare follow-up report",
|
||||
"This report uses pseudonyms. Review its coverage before sharing.":
|
||||
"This report uses pseudonyms. Review its coverage before sharing.",
|
||||
"Back to matches": "Back to matches",
|
||||
"Reading revisions": "Reading revisions",
|
||||
"Calculating database usage": "Calculating database usage",
|
||||
"Discovering revisions": "Discovering revisions",
|
||||
"Find a file from a report pseudonym": "Find a file from a report pseudonym",
|
||||
"Find matching paths in this Vault and local Metadata, then prepare a follow-up report using the same pseudonyms.":
|
||||
"Find matching paths in this Vault and local Metadata, then prepare a follow-up report using the same pseudonyms.",
|
||||
"Find file": "Find file",
|
||||
"⚠️ Initialise or overwrite the remote": "⚠️ Initialise or overwrite the remote",
|
||||
"🔗 Join this device": "🔗 Join this device",
|
||||
"⚙️ Apply settings only (advanced)": "⚙️ Apply settings only (advanced)",
|
||||
@@ -251,6 +356,48 @@ export const liveSyncProvisionalEnglishMessages = {
|
||||
"An ID source is required to enable this option.": "An ID source is required to enable this option.",
|
||||
"The ID source or recovery code is invalid. Check it and try again.":
|
||||
"The ID source or recovery code is invalid. Check it and try again.",
|
||||
"Read normal-file Metadata and current conflicts locally and in CouchDB, then compare content for suspect groups. Choose original names or pseudonyms before starting.":
|
||||
"Read normal-file Metadata and current conflicts locally and in CouchDB, then compare content for suspect groups. Choose original names or pseudonyms before starting.",
|
||||
Inspect: "Inspect",
|
||||
"Inspect database integrity": "Inspect database integrity",
|
||||
"Inspection completed. Review the findings and coverage before sharing.":
|
||||
"Inspection completed. Review the findings and coverage before sharing.",
|
||||
"Inspection partially completed. Some information could not be checked; review the reported reasons.":
|
||||
"Inspection partially completed. Some information could not be checked; review the reported reasons.",
|
||||
"Inspection failed. The report records the available evidence and reasons.":
|
||||
"Inspection failed. The report records the available evidence and reasons.",
|
||||
"Inspection cancelled. The report contains only the evidence collected so far.":
|
||||
"Inspection cancelled. The report contains only the evidence collected so far.",
|
||||
"Report copied.": "Report copied.",
|
||||
"Copy failed. Select the report text and copy it manually.":
|
||||
"Copy failed. Select the report text and copy it manually.",
|
||||
"Download requested. You can also copy the report if downloads are unavailable on this device.":
|
||||
"Download requested. You can also copy the report if downloads are unavailable on this device.",
|
||||
"Pseudonyms include a public salt, so guessed paths can be checked. Original names include private filenames. Reports omit file contents and credentials.":
|
||||
"Pseudonyms include a public salt, so guessed paths can be checked. Original names include private filenames. Reports omit file contents and credentials.",
|
||||
"Read available local revisions and report their Chunk references. Settings are left unchanged.":
|
||||
"Read available local revisions and report their Chunk references. Settings are left unchanged.",
|
||||
"Inspect normal-file Metadata, current conflicts, and suspect content against the Vault. Remote inspection supports CouchDB. Older history is excluded; findings do not authorise deletion.":
|
||||
"Inspect normal-file Metadata, current conflicts, and suspect content against the Vault. Remote inspection supports CouchDB. Older history is excluded; findings do not authorise deletion.",
|
||||
"Include remote database": "Include remote database",
|
||||
"Names in this report": "Names in this report",
|
||||
"Pseudonyms (recommended)": "Pseudonyms (recommended)",
|
||||
"Original names": "Original names",
|
||||
"Cancelling inspection…": "Cancelling inspection…",
|
||||
"Items inspected": "Items inspected",
|
||||
"Cancel inspection": "Cancel inspection",
|
||||
"This report uses temporary pseudonyms. Counts and relationships remain visible.":
|
||||
"This report uses temporary pseudonyms. Counts and relationships remain visible.",
|
||||
"This report contains original paths and identifiers. Review them before sharing.":
|
||||
"This report contains original paths and identifiers. Review them before sharing.",
|
||||
"Partial Usage results are available as JSON so their coverage and reasons stay with the rows.":
|
||||
"Partial Usage results are available as JSON so their coverage and reasons stay with the rows.",
|
||||
"Report format": "Report format",
|
||||
"Diagnostic report": "Diagnostic report",
|
||||
"Copy report": "Copy report",
|
||||
"Save report": "Save report",
|
||||
"The inspection could not finish. Close this dialogue and try again after database startup.":
|
||||
"The inspection could not finish. Close this dialogue and try again after database startup.",
|
||||
} as const;
|
||||
|
||||
export type LiveSyncProvisionalMessageKey = keyof typeof liveSyncProvisionalEnglishMessages;
|
||||
@@ -1297,6 +1297,10 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "CouchDB 连接调优",
|
||||
"zh-tw": "CouchDB 連線調校",
|
||||
},
|
||||
"CouchDB only. Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild both databases from a complete authoritative Vault and Fetch on the other devices. Resolve colliding file names first. P2P and Object Storage do not support version 1.":
|
||||
{
|
||||
def: "CouchDB only. Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild both databases from a complete authoritative Vault and Fetch on the other devices. Resolve colliding file names first. P2P and Object Storage do not support version 1.",
|
||||
},
|
||||
"CouchDB validates the database name when you connect. The name must not be empty.": {
|
||||
def: "CouchDB validates the database name when you connect. The name must not be empty.",
|
||||
es: "CouchDB valida el nombre de la base de datos al conectar. El nombre no puede estar vacío.",
|
||||
@@ -3871,6 +3875,9 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "稍后",
|
||||
"zh-tw": "稍後",
|
||||
},
|
||||
"Legacy path identity": {
|
||||
def: "Legacy path identity",
|
||||
},
|
||||
"Limit: {datetime} ({timestamp})": {
|
||||
def: "Limit: {datetime} ({timestamp})",
|
||||
es: "Límite: {datetime} ({timestamp})",
|
||||
@@ -7937,6 +7944,9 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "路径",
|
||||
"zh-tw": "路徑",
|
||||
},
|
||||
"Path identity mode": {
|
||||
def: "Path identity mode",
|
||||
},
|
||||
"Path Obfuscation": {
|
||||
def: "Path Obfuscation",
|
||||
es: "Ofuscación de rutas",
|
||||
@@ -13774,6 +13784,9 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "输入服务器信息",
|
||||
"zh-tw": "選擇同步遠端",
|
||||
},
|
||||
"Unicode NFC path identity": {
|
||||
def: "Unicode NFC path identity",
|
||||
},
|
||||
"Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": {
|
||||
def: "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.",
|
||||
es: "Nombre único entre dispositivos sincronizados. Para editarlo, desactive sincronización de personalización",
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
"Copy Report to clipboard": "Copy Report to clipboard",
|
||||
"CouchDB Configuration": "CouchDB Configuration",
|
||||
"CouchDB Connection Tweak": "CouchDB Connection Tweak",
|
||||
"CouchDB only. Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild both databases from a complete authoritative Vault and Fetch on the other devices. Resolve colliding file names first. P2P and Object Storage do not support version 1.": "CouchDB only. Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild both databases from a complete authoritative Vault and Fetch on the other devices. Resolve colliding file names first. P2P and Object Storage do not support version 1.",
|
||||
"CouchDB validates the database name when you connect. The name must not be empty.": "CouchDB validates the database name when you connect. The name must not be empty.",
|
||||
"Create or connect to database and continue": "Create or connect to database and continue",
|
||||
"Create P2P remote": "Create P2P remote",
|
||||
@@ -446,6 +447,7 @@
|
||||
"lang-zh-tw": "繁體中文",
|
||||
"Last tweak modified timestamp": "Last tweak modified timestamp",
|
||||
"Later": "Later",
|
||||
"Legacy path identity": "Legacy path identity",
|
||||
"Limit: {datetime} ({timestamp})": "Limit: {datetime} ({timestamp})",
|
||||
"live": "live",
|
||||
"LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.",
|
||||
@@ -841,6 +843,7 @@
|
||||
"Paste a connection string": "Paste a connection string",
|
||||
"Paste the Setup URI generated from one of your active devices.": "Paste the Setup URI generated from one of your active devices.",
|
||||
"Path": "Path",
|
||||
"Path identity mode": "Path identity mode",
|
||||
"Path Obfuscation": "Path Obfuscation",
|
||||
"Patterns to match files for overwriting instead of merging": "Patterns to match files for overwriting instead of merging",
|
||||
"Patterns to match files for syncing": "Patterns to match files for syncing",
|
||||
@@ -1523,6 +1526,7 @@
|
||||
"Ui.SetupWizard.SetupRemote.ProceedCouchDb": "Continue to CouchDB setup",
|
||||
"Ui.SetupWizard.SetupRemote.ProceedP2P": "Continue to P2P setup",
|
||||
"Ui.SetupWizard.SetupRemote.Title": "Choose a synchronisation remote",
|
||||
"Unicode NFC path identity": "Unicode NFC path identity",
|
||||
"Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.",
|
||||
"Unless you are certain, selecting this options is bit dangerous. It assumes that the server configuration is compatible with this device. If this is not the case, data loss may occur. Please ensure you know what you are doing.": "Unless you are certain, selecting this options is bit dangerous. It assumes that the server configuration is compatible with this device. If this is not the case, data loss may occur. Please ensure you know what you are doing.",
|
||||
"Updating list...": "Updating list...",
|
||||
|
||||
@@ -169,6 +169,8 @@ Copy: Copy
|
||||
Copy Report to clipboard: Copy Report to clipboard
|
||||
CouchDB Configuration: CouchDB Configuration
|
||||
CouchDB Connection Tweak: CouchDB Connection Tweak
|
||||
"CouchDB only. Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild both databases from a complete authoritative Vault and Fetch on the other devices. Resolve colliding file names first. P2P and Object Storage do not support version 1.":
|
||||
"CouchDB only. Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild both databases from a complete authoritative Vault and Fetch on the other devices. Resolve colliding file names first. P2P and Object Storage do not support version 1."
|
||||
CouchDB validates the database name when you connect. The name must not be empty.:
|
||||
CouchDB validates the database name when you connect. The name must not be
|
||||
empty.
|
||||
@@ -556,6 +558,7 @@ lang-ru: Русский
|
||||
lang-zh: 简体中文
|
||||
lang-zh-tw: 繁體中文
|
||||
Later: Later
|
||||
Legacy path identity: Legacy path identity
|
||||
"Limit: {datetime} ({timestamp})": "Limit: {datetime} ({timestamp})"
|
||||
live: live
|
||||
LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.:
|
||||
@@ -1251,6 +1254,7 @@ password: password
|
||||
Password: Password
|
||||
Paste a connection string: Paste a connection string
|
||||
Path: Path
|
||||
Path identity mode: Path identity mode
|
||||
Path Obfuscation: Path Obfuscation
|
||||
Patterns to match files for overwriting instead of merging: Patterns to match files for overwriting instead of merging
|
||||
Patterns to match files for syncing: Patterns to match files for syncing
|
||||
@@ -2042,6 +2046,7 @@ TweakMismatchResolve:
|
||||
TweakResolving: Configuration Mismatch Detected
|
||||
UseRemoteConfig: Use Remote Configuration
|
||||
AutoAcceptCompatible: Auto-Accept Available
|
||||
Unicode NFC path identity: Unicode NFC path identity
|
||||
Unique name between all synchronized devices. To edit this setting, please disable customization sync once.:
|
||||
Unique name between all synchronized devices. To edit this setting, please
|
||||
disable customization sync once.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RemoteDBSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { usesEncryptedInternalMetadata } from "@vrtmrz/livesync-commonlib/replication";
|
||||
import { getPathNormalizationVersion } from "@vrtmrz/livesync-commonlib/settings";
|
||||
|
||||
type EndpointProjection = readonly [kind: "url" | "invalid-url", value: string];
|
||||
|
||||
@@ -44,17 +45,18 @@ function projectHeaders(value: string): readonly (readonly [name: string, value:
|
||||
|
||||
function projectRemoteSecurity(settings: RemoteDBSettings) {
|
||||
return [
|
||||
getPathNormalizationVersion(settings),
|
||||
settings.idDerivationVersion,
|
||||
settings.idDerivationKey,
|
||||
settings.encrypt
|
||||
? ([
|
||||
"encrypted",
|
||||
settings.passphrase,
|
||||
settings.useDynamicIterationCount,
|
||||
settings.E2EEAlgorithm,
|
||||
settings.permitEmptyPassphrase,
|
||||
] as const)
|
||||
: (["plain"] as const),
|
||||
? ([
|
||||
"encrypted",
|
||||
settings.passphrase,
|
||||
settings.useDynamicIterationCount,
|
||||
settings.E2EEAlgorithm,
|
||||
settings.permitEmptyPassphrase,
|
||||
] as const)
|
||||
: (["plain"] as const),
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,30 @@ import {
|
||||
} from "./replicatorConfigurationIdentity";
|
||||
|
||||
describe("active Replicator configuration identity", () => {
|
||||
it("replaces a connection when the path identity mode changes", () => {
|
||||
const previous = configuredSettings({ pathNormalizationVersion: 0 });
|
||||
const next = { ...previous, pathNormalizationVersion: 1 as const };
|
||||
expect(getCouchDBReplicatorConfigurationIdentity(next)).not.toBe(
|
||||
getCouchDBReplicatorConfigurationIdentity(previous)
|
||||
);
|
||||
expect(getObjectStorageReplicatorConfigurationIdentity(next)).not.toBe(
|
||||
getObjectStorageReplicatorConfigurationIdentity(previous)
|
||||
);
|
||||
});
|
||||
|
||||
it("treats a missing path identity mode as version 0", () => {
|
||||
const missingVersion = configuredSettings();
|
||||
Reflect.deleteProperty(missingVersion, "pathNormalizationVersion");
|
||||
const explicitVersion0 = { ...missingVersion, pathNormalizationVersion: 0 as const };
|
||||
|
||||
expect(getCouchDBReplicatorConfigurationIdentity(explicitVersion0)).toBe(
|
||||
getCouchDBReplicatorConfigurationIdentity(missingVersion)
|
||||
);
|
||||
expect(getObjectStorageReplicatorConfigurationIdentity(explicitVersion0)).toBe(
|
||||
getObjectStorageReplicatorConfigurationIdentity(missingVersion)
|
||||
);
|
||||
});
|
||||
|
||||
function configuredSettings(overrides: Partial<ObsidianLiveSyncSettings> = {}): ObsidianLiveSyncSettings {
|
||||
return Object.assign(createNewVaultSettings(), {
|
||||
activeConfigurationId: "profile-a",
|
||||
|
||||
@@ -28,7 +28,10 @@ import {
|
||||
getObjectStorageReplicatorConfigurationIdentity,
|
||||
} from "./replicatorConfigurationIdentity";
|
||||
import {
|
||||
createCouchDBJoinedDevicesResourceFactory,
|
||||
createObjectStorageJoinedDevicesResourceFactory,
|
||||
createCouchDBConnectionProbeFactory,
|
||||
createCouchDBInitialFetchFastForwardResourceFactory,
|
||||
createCouchDBPreferredTweakProbeFactory,
|
||||
createCouchDBSecuritySeedResourceFactory,
|
||||
createCouchDBSynchronisationInformationResourceFactory,
|
||||
@@ -115,6 +118,9 @@ export function createCentralReplicatorProviderDefinitions(
|
||||
configurationIdentity: getCouchDBReplicatorConfigurationIdentity,
|
||||
create: () => Promise.resolve(new LiveSyncCouchDBReplicator(host)),
|
||||
remoteResources: {
|
||||
[REMOTE_RESOURCE_KINDS.JOINED_DEVICES]: supportedCapability(
|
||||
createCouchDBJoinedDevicesResourceFactory(host)
|
||||
),
|
||||
[REMOTE_RESOURCE_KINDS.CONNECTION]: supportedCapability(createCouchDBConnectionProbeFactory(host)),
|
||||
[REMOTE_RESOURCE_KINDS.PREFERRED_TWEAK]: supportedCapability(
|
||||
createCouchDBPreferredTweakProbeFactory(host)
|
||||
@@ -125,6 +131,9 @@ export function createCentralReplicatorProviderDefinitions(
|
||||
[REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION]: supportedCapability(
|
||||
createCouchDBSynchronisationInformationResourceFactory(host)
|
||||
),
|
||||
[REMOTE_RESOURCE_KINDS.INITIAL_FETCH_FAST_FORWARD]: supportedCapability(
|
||||
createCouchDBInitialFetchFastForwardResourceFactory(host)
|
||||
),
|
||||
},
|
||||
centralRemoteAdministration: COUCHDB_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
userInitiatedOneShot: supportedCapability(couchDBUserInitiatedOneShot),
|
||||
@@ -141,6 +150,9 @@ export function createCentralReplicatorProviderDefinitions(
|
||||
configurationIdentity: getObjectStorageReplicatorConfigurationIdentity,
|
||||
create: () => Promise.resolve(new LiveSyncJournalReplicator(host)),
|
||||
remoteResources: {
|
||||
[REMOTE_RESOURCE_KINDS.JOINED_DEVICES]: supportedCapability(
|
||||
createObjectStorageJoinedDevicesResourceFactory(host)
|
||||
),
|
||||
[REMOTE_RESOURCE_KINDS.CONNECTION]: supportedCapability(
|
||||
createObjectStorageConnectionProbeFactory(host)
|
||||
),
|
||||
@@ -151,6 +163,7 @@ export function createCentralReplicatorProviderDefinitions(
|
||||
createObjectStorageSecuritySeedResourceFactory(host)
|
||||
),
|
||||
[REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION]: CAPABILITY_NOT_APPLICABLE,
|
||||
[REMOTE_RESOURCE_KINDS.INITIAL_FETCH_FAST_FORWARD]: CAPABILITY_NOT_APPLICABLE,
|
||||
},
|
||||
centralRemoteAdministration: OBJECT_STORAGE_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
userInitiatedOneShot: supportedCapability(objectStorageUserInitiatedOneShot),
|
||||
|
||||
@@ -44,7 +44,16 @@ describe("central Replicator provider definitions", () => {
|
||||
it("keeps the retained remote-resource catalogue bounded", () => {
|
||||
expect
|
||||
.soft(Object.values(REMOTE_RESOURCE_KINDS).sort())
|
||||
.toEqual(["connection", "preferred-tweak", "security-seed", "synchronisation-information"].sort());
|
||||
.toEqual(
|
||||
[
|
||||
"connection",
|
||||
"initial-fetch-fast-forward",
|
||||
"joined-devices",
|
||||
"preferred-tweak",
|
||||
"security-seed",
|
||||
"synchronisation-information",
|
||||
].sort()
|
||||
);
|
||||
});
|
||||
|
||||
it("lets Journal prepare its own fresh Security Seed while CouchDB uses central preparation", () => {
|
||||
@@ -106,18 +115,27 @@ describe("central Replicator provider definitions", () => {
|
||||
const couchAdministration = definitions.get(REMOTE_COUCHDB)?.centralRemoteAdministration;
|
||||
const objectAdministration = definitions.get(REMOTE_MINIO)?.centralRemoteAdministration;
|
||||
|
||||
expect(Object.keys(couchResources ?? {}).sort()).toEqual(Object.values(REMOTE_RESOURCE_KINDS).sort());
|
||||
expect(Object.keys(objectResources ?? {}).sort()).toEqual(Object.values(REMOTE_RESOURCE_KINDS).sort());
|
||||
const resources = Object.values(REMOTE_RESOURCE_KINDS).sort();
|
||||
expect(Object.keys(couchResources ?? {}).sort()).toEqual(resources);
|
||||
expect(Object.keys(objectResources ?? {}).sort()).toEqual(resources);
|
||||
expect(couchResources?.[REMOTE_RESOURCE_KINDS.JOINED_DEVICES]?.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect(objectResources?.[REMOTE_RESOURCE_KINDS.JOINED_DEVICES]?.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect(couchResources?.[REMOTE_RESOURCE_KINDS.CONNECTION].kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect(couchResources?.[REMOTE_RESOURCE_KINDS.PREFERRED_TWEAK].kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect(couchResources?.[REMOTE_RESOURCE_KINDS.SECURITY_SEED].kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect(couchResources?.[REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION].kind).toBe(
|
||||
CAPABILITY_SUPPORT_KINDS.SUPPORTED
|
||||
);
|
||||
expect(couchResources?.[REMOTE_RESOURCE_KINDS.INITIAL_FETCH_FAST_FORWARD]?.kind).toBe(
|
||||
CAPABILITY_SUPPORT_KINDS.SUPPORTED
|
||||
);
|
||||
expect(objectResources?.[REMOTE_RESOURCE_KINDS.SECURITY_SEED].kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect(objectResources?.[REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION].kind).toBe(
|
||||
CAPABILITY_SUPPORT_KINDS.NOT_APPLICABLE
|
||||
);
|
||||
expect(objectResources?.[REMOTE_RESOURCE_KINDS.INITIAL_FETCH_FAST_FORWARD]?.kind).toBe(
|
||||
CAPABILITY_SUPPORT_KINDS.NOT_APPLICABLE
|
||||
);
|
||||
expect(couchAdministration?.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect(objectAdministration?.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect("activeRemoteReads" in definitions.get(REMOTE_COUCHDB)!).toBe(false);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
logger: vi.fn(),
|
||||
initialFetchFastForwardResource: undefined as unknown,
|
||||
couchDB: [] as Array<{
|
||||
host: unknown;
|
||||
isMobile: ReturnType<typeof vi.fn>;
|
||||
@@ -12,6 +13,9 @@ const mocks = vi.hoisted(() => ({
|
||||
getRemoteStatus: ReturnType<typeof vi.fn>;
|
||||
getRemotePreferredTweakValues: ReturnType<typeof vi.fn>;
|
||||
getReplicationPBKDF2Salt: ReturnType<typeof vi.fn>;
|
||||
getJoinedDevices: ReturnType<typeof vi.fn>;
|
||||
forgetJoinedDevice: ReturnType<typeof vi.fn>;
|
||||
createInitialFetchFastForwardResource: ReturnType<typeof vi.fn>;
|
||||
closeReplication: ReturnType<typeof vi.fn>;
|
||||
}>,
|
||||
objectStorage: [] as Array<{
|
||||
@@ -20,6 +24,8 @@ const mocks = vi.hoisted(() => ({
|
||||
getRemoteStatus: ReturnType<typeof vi.fn>;
|
||||
getRemotePreferredTweakValues: ReturnType<typeof vi.fn>;
|
||||
getReplicationPBKDF2Salt: ReturnType<typeof vi.fn>;
|
||||
getJoinedDevices: ReturnType<typeof vi.fn>;
|
||||
forgetJoinedDevice: ReturnType<typeof vi.fn>;
|
||||
closeReplication: ReturnType<typeof vi.fn>;
|
||||
}>,
|
||||
checkSyncInfo: vi.fn(async () => true),
|
||||
@@ -42,6 +48,9 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicato
|
||||
getRemoteStatus = vi.fn();
|
||||
getRemotePreferredTweakValues = vi.fn();
|
||||
getReplicationPBKDF2Salt = vi.fn();
|
||||
getJoinedDevices = vi.fn();
|
||||
forgetJoinedDevice = vi.fn();
|
||||
createInitialFetchFastForwardResource = vi.fn(() => mocks.initialFetchFastForwardResource);
|
||||
closeReplication = vi.fn();
|
||||
|
||||
constructor(host: unknown) {
|
||||
@@ -58,6 +67,8 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalRe
|
||||
getRemoteStatus = vi.fn();
|
||||
getRemotePreferredTweakValues = vi.fn();
|
||||
getReplicationPBKDF2Salt = vi.fn();
|
||||
getJoinedDevices = vi.fn();
|
||||
forgetJoinedDevice = vi.fn();
|
||||
closeReplication = vi.fn();
|
||||
|
||||
constructor(host: unknown) {
|
||||
@@ -69,10 +80,13 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalRe
|
||||
|
||||
import {
|
||||
createCouchDBConnectionProbeFactory,
|
||||
createCouchDBJoinedDevicesResourceFactory,
|
||||
createCouchDBInitialFetchFastForwardResourceFactory,
|
||||
createCouchDBPreferredTweakProbeFactory,
|
||||
createCouchDBSecuritySeedResourceFactory,
|
||||
createCouchDBSynchronisationInformationResourceFactory,
|
||||
createObjectStorageConnectionProbeFactory,
|
||||
createObjectStorageJoinedDevicesResourceFactory,
|
||||
createObjectStoragePreferredTweakProbeFactory,
|
||||
createObjectStorageSecuritySeedResourceFactory,
|
||||
} from "./replicatorResources";
|
||||
@@ -92,6 +106,7 @@ describe("replicator probe factories", () => {
|
||||
beforeEach(() => {
|
||||
mocks.couchDB.length = 0;
|
||||
mocks.objectStorage.length = 0;
|
||||
mocks.initialFetchFastForwardResource = undefined;
|
||||
mocks.checkSyncInfo.mockReset().mockResolvedValue(true);
|
||||
mocks.logger.mockClear();
|
||||
});
|
||||
@@ -226,6 +241,65 @@ describe("replicator probe factories", () => {
|
||||
expect(preferredReplicator.getRemotePreferredTweakValues).toHaveBeenCalledWith(snapshot);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
kind: "CouchDB",
|
||||
remoteType: REMOTE_COUCHDB,
|
||||
createFactory: createCouchDBJoinedDevicesResourceFactory,
|
||||
instances: () => mocks.couchDB,
|
||||
},
|
||||
{
|
||||
kind: "Object Storage",
|
||||
remoteType: REMOTE_MINIO,
|
||||
createFactory: createObjectStorageJoinedDevicesResourceFactory,
|
||||
instances: () => mocks.objectStorage,
|
||||
},
|
||||
])(
|
||||
"keeps $kind joined-device operations on an owned settings snapshot",
|
||||
async ({ remoteType, createFactory, instances }) => {
|
||||
const host = { name: "host" };
|
||||
const source = createSettings({ remoteType });
|
||||
const snapshot = { ...source };
|
||||
const factory = createFactory(host as never);
|
||||
const resource = await factory(source);
|
||||
const nextResource = await factory(source);
|
||||
const [replicator, nextReplicator] = instances();
|
||||
expect(instances()).toHaveLength(2);
|
||||
expect(replicator.host).toBe(host);
|
||||
expect(replicator.getJoinedDevices).not.toHaveBeenCalled();
|
||||
expect(replicator.forgetJoinedDevice).not.toHaveBeenCalled();
|
||||
const devices = {
|
||||
milestonePresent: true,
|
||||
locked: false,
|
||||
cleaned: false,
|
||||
devices: [{ id: "other", accepted: true }],
|
||||
};
|
||||
replicator.getJoinedDevices.mockResolvedValue(devices);
|
||||
replicator.forgetJoinedDevice.mockResolvedValue("conflict");
|
||||
|
||||
source.couchDB_DBNAME = "changed-vault";
|
||||
source.bucket = "changed-bucket";
|
||||
await expect(resource.read()).resolves.toBe(devices);
|
||||
await expect(resource.forget("other")).resolves.toBe("conflict");
|
||||
expect(replicator.getJoinedDevices).toHaveBeenCalledWith(snapshot);
|
||||
expect(replicator.getJoinedDevices.mock.calls[0][0]).not.toBe(source);
|
||||
expect(replicator.forgetJoinedDevice).toHaveBeenCalledWith(snapshot, "other");
|
||||
|
||||
const failure = new Error("remote unavailable");
|
||||
replicator.getJoinedDevices.mockRejectedValue(failure);
|
||||
await expect(resource.read()).rejects.toBe(failure);
|
||||
replicator.forgetJoinedDevice.mockRejectedValue(failure);
|
||||
await expect(resource.forget("other")).rejects.toBe(failure);
|
||||
const disposal = resource.dispose();
|
||||
expect(resource.dispose()).toBe(disposal);
|
||||
await disposal;
|
||||
expect(replicator.closeReplication).toHaveBeenCalledOnce();
|
||||
expect(nextReplicator.closeReplication).not.toHaveBeenCalled();
|
||||
await nextResource.dispose();
|
||||
expect(nextReplicator.closeReplication).toHaveBeenCalledOnce();
|
||||
}
|
||||
);
|
||||
|
||||
it("shares one successful asynchronous disposal promise for every probe kind", async () => {
|
||||
const couchProbe = await createCouchDBPreferredTweakProbeFactory({} as never)(createSettings());
|
||||
const objectProbe = await createObjectStoragePreferredTweakProbeFactory({} as never)(
|
||||
@@ -370,4 +444,29 @@ describe("replicator probe factories", () => {
|
||||
await resource.dispose();
|
||||
expect(replicator.closeReplication).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("creates an Initial Fetch Fast Forward resource from a settings snapshot and leaves ownership to the resource", async () => {
|
||||
const host = { name: "host" };
|
||||
const settings = createSettings();
|
||||
const snapshot = { ...settings };
|
||||
const prepare = vi.fn(async () => undefined);
|
||||
const dispose = vi.fn(async () => undefined);
|
||||
const resource = { prepare, dispose };
|
||||
const factory = createCouchDBInitialFetchFastForwardResourceFactory(host as never);
|
||||
mocks.initialFetchFastForwardResource = resource;
|
||||
|
||||
const result = await factory(settings);
|
||||
const replicator = mocks.couchDB[0];
|
||||
|
||||
settings.couchDB_URI = "https://changed.example.test";
|
||||
expect(replicator.createInitialFetchFastForwardResource).toHaveBeenCalledWith(snapshot);
|
||||
expect(replicator.createInitialFetchFastForwardResource.mock.calls[0][0]).not.toBe(settings);
|
||||
expect(replicator.host).toBe(host);
|
||||
expect(result).toBe(resource);
|
||||
|
||||
await expect(result.prepare()).resolves.toBeUndefined();
|
||||
await result.dispose();
|
||||
expect(prepare).toHaveBeenCalledOnce();
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -14,3 +14,9 @@ export {
|
||||
type SecuritySeedResourceHost,
|
||||
} from "./securitySeed";
|
||||
export { createCouchDBSynchronisationInformationResourceFactory } from "./synchronisationInformation";
|
||||
|
||||
export {
|
||||
createCouchDBJoinedDevicesResourceFactory,
|
||||
createObjectStorageJoinedDevicesResourceFactory,
|
||||
} from "./joinedDevices";
|
||||
export { createCouchDBInitialFetchFastForwardResourceFactory } from "./initialFetchFastForward";
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { InitialFetchFastForwardResourceFactory } from "@vrtmrz/livesync-commonlib/replication";
|
||||
import {
|
||||
LiveSyncCouchDBReplicator,
|
||||
type LiveSyncCouchDBReplicatorEnv,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import { snapshotRemoteSettings } from "./shared";
|
||||
|
||||
/** Create a private CouchDB Replicator for a fresh Fast Fetch checkpoint handoff. */
|
||||
export function createCouchDBInitialFetchFastForwardResourceFactory(
|
||||
host: LiveSyncCouchDBReplicatorEnv
|
||||
): InitialFetchFastForwardResourceFactory {
|
||||
return (setting) => {
|
||||
const snapshot = snapshotRemoteSettings(setting);
|
||||
const replicator = new LiveSyncCouchDBReplicator(host);
|
||||
return Promise.resolve(replicator.createInitialFetchFastForwardResource(snapshot));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { RemoteDBSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type {
|
||||
JoinedDevicesResource,
|
||||
JoinedDevicesResourceFactory,
|
||||
JoinedDevicesSnapshot,
|
||||
ForgetJoinedDeviceResult,
|
||||
} from "@vrtmrz/livesync-commonlib/replication";
|
||||
import {
|
||||
LiveSyncCouchDBReplicator,
|
||||
type LiveSyncCouchDBReplicatorEnv,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import { LiveSyncJournalReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator";
|
||||
import { createReplicatorDisposer, snapshotRemoteSettings, type ResourceReplicator } from "./shared";
|
||||
|
||||
interface JoinedDevicesReplicator extends ResourceReplicator {
|
||||
getJoinedDevices(setting: RemoteDBSettings): Promise<JoinedDevicesSnapshot>;
|
||||
forgetJoinedDevice(setting: RemoteDBSettings, deviceId: string): Promise<ForgetJoinedDeviceResult>;
|
||||
}
|
||||
|
||||
function createResource(replicator: JoinedDevicesReplicator, snapshot: RemoteDBSettings): JoinedDevicesResource {
|
||||
return {
|
||||
read: () => replicator.getJoinedDevices(snapshot),
|
||||
forget: (deviceId) => replicator.forgetJoinedDevice(snapshot, deviceId),
|
||||
dispose: createReplicatorDisposer(replicator),
|
||||
};
|
||||
}
|
||||
|
||||
export function createCouchDBJoinedDevicesResourceFactory(
|
||||
host: LiveSyncCouchDBReplicatorEnv
|
||||
): JoinedDevicesResourceFactory {
|
||||
return (setting) =>
|
||||
Promise.resolve(createResource(new LiveSyncCouchDBReplicator(host), snapshotRemoteSettings(setting)));
|
||||
}
|
||||
|
||||
export function createObjectStorageJoinedDevicesResourceFactory(
|
||||
host: LiveSyncCouchDBReplicatorEnv
|
||||
): JoinedDevicesResourceFactory {
|
||||
return (setting) =>
|
||||
Promise.resolve(createResource(new LiveSyncJournalReplicator(host), snapshotRemoteSettings(setting)));
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MemoryValueStore } from "@vrtmrz/value-store";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
@@ -18,17 +19,7 @@ import {
|
||||
} from "./turnSettingsPrivacy";
|
||||
|
||||
class MemorySettingService extends SettingService {
|
||||
readonly items = new Map<string, string>();
|
||||
saved?: ObsidianLiveSyncSettings;
|
||||
protected setItem(key: string, value: string) {
|
||||
this.items.set(key, value);
|
||||
}
|
||||
protected getItem(key: string) {
|
||||
return this.items.get(key) ?? "";
|
||||
}
|
||||
protected deleteItem(key: string) {
|
||||
this.items.delete(key);
|
||||
}
|
||||
protected saveData(settings: ObsidianLiveSyncSettings) {
|
||||
this.saved = structuredClone(settings);
|
||||
return Promise.resolve();
|
||||
@@ -70,6 +61,7 @@ describe("managed TURN settings privacy", () => {
|
||||
const originalURI = ConnectionStringParser.serialize({ type: "p2p", settings: current });
|
||||
current.remoteConfigurations.managed.uri = originalURI;
|
||||
const service = new MemorySettingService(new ServiceContext(), {
|
||||
deviceValues: new MemoryValueStore(),
|
||||
APIService: {
|
||||
getSystemVaultName: () => "test-vault",
|
||||
getAppID: () => "test-app",
|
||||
|
||||
Reference in new issue
Block a user