@vrtmrz/livesync-commonlib 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -8
- package/dist/API/DirectFileManipulatorV2.d.ts +8 -1
- package/dist/API/DirectFileManipulatorV2.js +52 -79
- package/dist/API/DirectFileWatchOwner.d.ts +24 -0
- package/dist/API/DirectFileWatchOwner.js +256 -0
- package/dist/API/processSetting.d.ts +1 -0
- package/dist/API/processSetting.js +3 -3
- package/dist/common/models/setting.const.defaults.js +1 -0
- package/dist/common/models/setting.const.qr.js +1 -0
- package/dist/common/models/setting.policy.d.ts +4 -0
- package/dist/common/models/setting.policy.js +1 -0
- package/dist/common/models/setting.type.d.ts +3 -1
- package/dist/common/models/shared.definition.configNames.js +1 -2
- package/dist/common/models/tweak.definition.d.ts +2 -1
- package/dist/common/pathNamespace.d.ts +4 -0
- package/dist/common/pathNamespace.js +10 -0
- package/dist/common/pathNormalization.d.ts +2 -0
- package/dist/common/pathNormalization.js +7 -2
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/common/settingConstants.js +4 -0
- package/dist/managers/ChunkFetcher.d.ts +3 -0
- package/dist/managers/ChunkFetcher.js +20 -13
- package/dist/managers/LiveSyncManagers.d.ts +2 -0
- package/dist/managers/LiveSyncManagers.js +1 -0
- package/dist/managers/chunkFetchSource.d.ts +14 -0
- package/dist/managers/chunkFetchSource.js +12 -0
- package/dist/platform/AmbiguousNormalisedPathError.d.ts +16 -0
- package/dist/platform/AmbiguousNormalisedPathError.js +21 -0
- package/dist/platform/StoragePathResolver.d.ts +15 -0
- package/dist/platform/StoragePathResolver.js +113 -0
- package/dist/platform/browser/UnicodeResolvingFileSystemAccessStorageAdapter.d.ts +74 -0
- package/dist/platform/browser/UnicodeResolvingFileSystemAccessStorageAdapter.js +169 -0
- package/dist/platform/browser/index.d.ts +1 -0
- package/dist/platform/browser/index.js +2 -0
- package/dist/platform/browser/storage.d.ts +23 -0
- package/dist/platform/browser/storage.js +138 -0
- package/dist/platform/node/UnicodeResolvingNodeStorageAdapter.d.ts +79 -0
- package/dist/platform/node/UnicodeResolvingNodeStorageAdapter.js +218 -0
- package/dist/platform/node/index.d.ts +1 -0
- package/dist/platform/node/index.js +2 -0
- package/dist/platform/node/storage.d.ts +30 -1
- package/dist/platform/node/storage.js +88 -0
- package/dist/platform/pathComparison.d.ts +53 -0
- package/dist/platform/pathComparison.js +39 -0
- package/dist/platform/pathTypes.d.ts +154 -0
- package/dist/platform/pathTypes.js +0 -0
- package/dist/platform/storagePathResolution.d.ts +13 -0
- package/dist/platform/storagePathResolution.js +9 -0
- package/dist/pouchdb/LiveSyncDBFunctions.js +5 -0
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +7 -1
- package/dist/pouchdb/LiveSyncLocalDB.js +13 -10
- package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
- package/dist/pouchdb/pathIdentityCompatibility.js +19 -1
- package/dist/replication/RemoteResource.d.ts +3 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +5 -3
- package/dist/replication/index.d.ts +2 -0
- package/dist/replication/index.js +2 -0
- package/dist/replication/journal/JournalSyncCore.d.ts +1 -0
- package/dist/replication/journal/JournalSyncCore.js +35 -19
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +11 -3
- package/dist/replication/journal/LiveSyncJournalReplicator.js +123 -45
- package/dist/replication/trystero/P2PRoomSessionOwner.js +14 -13
- package/dist/replication/trystero/TrysteroReplicator.d.ts +9 -5
- package/dist/replication/trystero/TrysteroReplicator.js +98 -70
- package/dist/replication/trystero/TrysteroReplicator.pathIdentity.d.ts +11 -0
- package/dist/replication/trystero/TrysteroReplicator.pathIdentity.js +72 -0
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +2 -0
- package/dist/replication/trystero/useP2PReplicatorFeature.js +6 -2
- package/dist/serviceModules/Rebuilder.d.ts +1 -0
- package/dist/serviceModules/Rebuilder.js +35 -6
- package/dist/services/base/FileProcessingService.d.ts +2 -2
- package/dist/services/base/RemoteConnection.d.ts +2 -0
- package/dist/services/base/RemoteService.d.ts +1 -1
- package/dist/services/base/RemoteService.js +13 -5
- package/dist/services/implements/injectable/InjectableVaultService.d.ts +1 -1
- package/dist/services/lib/couchdbHttpCache.d.ts +5 -0
- package/dist/services/lib/couchdbHttpCache.js +36 -0
- package/dist/storage.d.ts +4 -0
- package/dist/storage.js +19 -0
- package/dist/string_and_binary/path.js +4 -7
- package/dist/worker/bgWorker.js +1 -1
- package/docs/adr/2026_11_shared_storage_path_resolution.md +73 -0
- package/docs/conflict-resolution.md +1 -1
- package/docs/design_docs/storage_path_resolution.md +305 -0
- package/docs/development.md +15 -1
- package/docs/platform-storage.md +61 -0
- package/docs/proven-in-use.md +30 -13
- package/docs/releasing.md +27 -0
- package/docs/service-feature-composition.md +7 -1
- package/docs/unicode-path-identity.md +104 -12
- package/package.json +10 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { TaggedType } from "octagonal-wheels/common/types";
|
|
2
|
+
import type { FilePath, FilePathWithPrefix, FilePathWithPrefixLC } from "../common/models/db.type.js";
|
|
3
|
+
import type { PathNormalizationVersion } from "../common/models/setting.type.js";
|
|
4
|
+
import type { PathNamespace } from "../common/pathNamespace.js";
|
|
5
|
+
/**
|
|
6
|
+
* An original file path without a Metadata namespace.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* This compatibility type does not establish NFC form, physical spelling, or
|
|
10
|
+
* existence. Validate raw storage input with `parseFilePath`.
|
|
11
|
+
*/
|
|
12
|
+
export type { FilePath };
|
|
13
|
+
/**
|
|
14
|
+
* An original document path with an optional recognised Metadata namespace.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Excludes the legacy comparison-key variant. Route the namespace before
|
|
18
|
+
* validating the remaining file path for storage access.
|
|
19
|
+
*/
|
|
20
|
+
export type DocumentPath = Exclude<FilePathWithPrefix, FilePathWithPrefixLC>;
|
|
21
|
+
declare const unicodeForm: unique symbol;
|
|
22
|
+
declare const storagePath: unique symbol;
|
|
23
|
+
/**
|
|
24
|
+
* A file path in Unicode NFC form, without case conversion.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* NFC form alone does not establish physical storage spelling or existence.
|
|
28
|
+
* This refinement is used within normalisation helpers and is not re-exported
|
|
29
|
+
* by the focused storage entry point.
|
|
30
|
+
*/
|
|
31
|
+
export type NfcFilePath = FilePath & {
|
|
32
|
+
readonly [unicodeForm]: "NFC";
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* A root-relative storage target whose existing components use resolved spelling.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* Produced by resolution or supported physical enumeration. A write target may
|
|
39
|
+
* contain missing components; this type does not guarantee existence, entry
|
|
40
|
+
* kind, or protection against concurrent changes. The empty path denotes the
|
|
41
|
+
* root, subject to each operation's root rules.
|
|
42
|
+
*/
|
|
43
|
+
export type StoragePath = FilePath & {
|
|
44
|
+
readonly [storagePath]: true;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* A path identity key produced by the configured Unicode and case comparison.
|
|
48
|
+
*
|
|
49
|
+
* @typeParam Domain - `storage` compares within one storage root; `document`
|
|
50
|
+
* retains the Metadata namespace so different document kinds remain distinct.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* Retain the original path separately. A comparison key cannot establish an
|
|
54
|
+
* original filename and must not be passed to storage or namespace operations.
|
|
55
|
+
*/
|
|
56
|
+
export type PathComparisonKey<Domain extends "storage" | "document"> = TaggedType<string, `PathComparisonKey:${Domain}`>;
|
|
57
|
+
/** Unicode and case settings used to derive path comparison keys. */
|
|
58
|
+
export type PathComparisonSettings = Readonly<{
|
|
59
|
+
/** `0` retains Legacy Unicode handling; `1` compares Unicode NFC forms. */
|
|
60
|
+
pathNormalizationVersion: PathNormalizationVersion;
|
|
61
|
+
/** Preserve letter case when true; otherwise apply the existing `toLowerCase()` rule. */
|
|
62
|
+
handleFilenameCaseSensitive: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
/** The namespace and original file path obtained by splitting one document path. */
|
|
65
|
+
export type DocumentPathParts = Readonly<{
|
|
66
|
+
/** One recognised Metadata namespace, or the empty string for an ordinary file. */
|
|
67
|
+
prefix: PathNamespace;
|
|
68
|
+
/** Original spelling after removing that one namespace. */
|
|
69
|
+
path: FilePath;
|
|
70
|
+
}>;
|
|
71
|
+
/** One immediate physical child returned by a strict directory reader. */
|
|
72
|
+
export type DirectoryEntry = Readonly<{
|
|
73
|
+
/** Unmodified child name, rather than a relative path containing multiple components. */
|
|
74
|
+
name: string;
|
|
75
|
+
/** Entry kind reported by the storage host. */
|
|
76
|
+
kind: "file" | "directory";
|
|
77
|
+
}>;
|
|
78
|
+
/** A physical child path and its kind, ready for a storage scan to consume. */
|
|
79
|
+
export type StorageEntry = Readonly<{
|
|
80
|
+
/** Root-relative path retaining the directory listing's actual spelling. */
|
|
81
|
+
path: StoragePath;
|
|
82
|
+
/** Entry kind from the same directory listing. */
|
|
83
|
+
kind: DirectoryEntry["kind"];
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Read a directory's complete immediate children using actual names and kinds.
|
|
87
|
+
*
|
|
88
|
+
* @param physicalDirectory - Resolved directory path relative to the host's root.
|
|
89
|
+
* @returns The children, including an empty array for an empty directory, or
|
|
90
|
+
* `null` only when the directory is absent.
|
|
91
|
+
* @throws Propagates permission, non-directory, and other I/O failures instead
|
|
92
|
+
* of reporting absence or a successful partial listing.
|
|
93
|
+
*/
|
|
94
|
+
export type ReadDirectory = (physicalDirectory: StoragePath) => Promise<readonly DirectoryEntry[] | null>;
|
|
95
|
+
/** Resolved paths for a rename which the storage host has yet to perform. */
|
|
96
|
+
export type ResolvedRename = Readonly<{
|
|
97
|
+
/** Existing source spelling. */
|
|
98
|
+
source: StoragePath;
|
|
99
|
+
/** Resolved target, retaining a requested leaf spelling change where applicable. */
|
|
100
|
+
target: StoragePath;
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* Resolve NFC-equivalent names through strict directory reads under one policy.
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* Operations reject ambiguous candidates even when an exact spelling exists,
|
|
107
|
+
* propagate reader failures, and perform no filesystem mutation. Resolution
|
|
108
|
+
* does not reserve a path against concurrent changes; hosts own that lifecycle.
|
|
109
|
+
*/
|
|
110
|
+
export interface StoragePathResolver {
|
|
111
|
+
/**
|
|
112
|
+
* Resolve an existing file or directory using its physical spelling.
|
|
113
|
+
*
|
|
114
|
+
* @param path - Original path relative to the selected storage root.
|
|
115
|
+
* @returns The resolved path, or `null` when a component is absent.
|
|
116
|
+
*/
|
|
117
|
+
resolveExisting(path: FilePath): Promise<StoragePath | null>;
|
|
118
|
+
/**
|
|
119
|
+
* Resolve a write target while preserving any missing suffix as requested.
|
|
120
|
+
*
|
|
121
|
+
* @param path - Original target path relative to the selected storage root.
|
|
122
|
+
* @returns The resolved target; this operation creates no missing entries.
|
|
123
|
+
*/
|
|
124
|
+
resolveForWrite(path: FilePath): Promise<StoragePath>;
|
|
125
|
+
/**
|
|
126
|
+
* Resolve both sides of an explicit rename, including a leaf spelling change.
|
|
127
|
+
*
|
|
128
|
+
* @param source - Original source entry path; the root is not an entry.
|
|
129
|
+
* @param target - Requested target entry path; the root is not an entry.
|
|
130
|
+
* @returns Both paths, or `null` when the source is absent. The host retains
|
|
131
|
+
* responsibility for performing the rename and handling replacement.
|
|
132
|
+
*/
|
|
133
|
+
resolveRename(source: FilePath, target: FilePath): Promise<ResolvedRename | null>;
|
|
134
|
+
/**
|
|
135
|
+
* Enumerate an already resolved directory without resolving each child again.
|
|
136
|
+
*
|
|
137
|
+
* @param path - Physical directory path relative to the selected root.
|
|
138
|
+
* @returns Child paths and kinds, or `null` when the directory is absent.
|
|
139
|
+
* @throws Rejects equivalent child names as ambiguous before returning a listing.
|
|
140
|
+
*/
|
|
141
|
+
listResolvedDirectory(path: StoragePath): Promise<readonly StorageEntry[] | null>;
|
|
142
|
+
}
|
|
143
|
+
/** Dependencies and comparison policy for constructing an NFC path resolver. */
|
|
144
|
+
export type StoragePathResolverOptions = Readonly<{
|
|
145
|
+
/** Strict host enumeration; it must distinguish absence from I/O failure. */
|
|
146
|
+
readDirectory: ReadDirectory;
|
|
147
|
+
/** Policy copied at construction; construct a new resolver when settings change. */
|
|
148
|
+
policy: Readonly<{
|
|
149
|
+
/** The shared resolver accepts NFC mode only; Legacy hosts retain their existing paths. */
|
|
150
|
+
pathNormalizationVersion: 1;
|
|
151
|
+
/** Preserve letter case when true; otherwise compare using `toLowerCase()`. */
|
|
152
|
+
handleFilenameCaseSensitive: boolean;
|
|
153
|
+
}>;
|
|
154
|
+
}>;
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StoragePathResolver as ResolverContract, StoragePathResolverOptions } from "./pathTypes.js";
|
|
2
|
+
export type { DirectoryEntry, ReadDirectory, ResolvedRename, StorageEntry, StoragePathResolver, StoragePathResolverOptions, } from "./pathTypes.js";
|
|
3
|
+
export { AmbiguousNormalisedPathError } from "./AmbiguousNormalisedPathError.js";
|
|
4
|
+
/**
|
|
5
|
+
* Bind strict directory reads to a copied NFC resolution policy.
|
|
6
|
+
*
|
|
7
|
+
* @param options - Host enumeration and explicit NFC/case settings.
|
|
8
|
+
* @returns A resolver which reads current listings without retaining a directory cache.
|
|
9
|
+
* @throws Rejects a path version other than `1` or a non-boolean case setting.
|
|
10
|
+
* @remarks Construction performs no I/O. Resolution performs no mutation or
|
|
11
|
+
* atomic reservation; construct a new resolver when its comparison settings change.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createStoragePathResolver(options: StoragePathResolverOptions): ResolverContract;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { StoragePathResolver } from "./StoragePathResolver.js";
|
|
2
|
+
import { AmbiguousNormalisedPathError } from "./AmbiguousNormalisedPathError.js";
|
|
3
|
+
function createStoragePathResolver(options) {
|
|
4
|
+
return new StoragePathResolver(options);
|
|
5
|
+
}
|
|
6
|
+
export {
|
|
7
|
+
AmbiguousNormalisedPathError,
|
|
8
|
+
createStoragePathResolver
|
|
9
|
+
};
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "../common/types.js";
|
|
7
7
|
import { extractObject, isObjectDifferent, resolveWithIgnoreKnownError } from "../common/utils.js";
|
|
8
8
|
import { assessTweakCompatibility } from "../common/models/tweak.compatibility.js";
|
|
9
|
+
import { getPathNormalizationVersion } from "../common/pathNormalization.js";
|
|
9
10
|
import { usesEncryptedInternalMetadata } from "./remoteFeatureCompatibility.js";
|
|
10
11
|
function getEffectiveTweakValues(setting) {
|
|
11
12
|
return {
|
|
@@ -56,6 +57,10 @@ async function ensureRemoteIsCompatible(infoSrc, setting, deviceNodeID, currentV
|
|
|
56
57
|
if (tweakAssessment.entries.some(({ key, relation }) => key === "idDerivationVersion" && relation === "different")) {
|
|
57
58
|
return "ID_KEY_MISMATCH";
|
|
58
59
|
}
|
|
60
|
+
if (getPathNormalizationVersion(currentTweakValues) !== getPathNormalizationVersion(preferred_tweak)) {
|
|
61
|
+
recordTweakAssessment?.(tweakAssessment);
|
|
62
|
+
return ["MISMATCHED", preferred_tweak];
|
|
63
|
+
}
|
|
59
64
|
if (!setting.disableCheckingConfigMismatch) {
|
|
60
65
|
recordTweakAssessment?.(tweakAssessment);
|
|
61
66
|
if (tweakAssessment.alignment === "mismatched") {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type EntryDoc, type EntryLeaf, type Credential, type RemoteDBSettings, type DocumentID, type FilePathWithPrefix, type FilePath, type DatabaseEntry, type LoadedEntry, type MetaEntry, type SavingEntry, type diff_result_leaf } from "../common/types.js";
|
|
2
2
|
import type { LiveSyncEventHub } from "../hub/hub.js";
|
|
3
3
|
import { LiveSyncManagers } from "../managers/LiveSyncManagers.js";
|
|
4
|
+
import type { ChunkFetchSource } from "../managers/chunkFetchSource.js";
|
|
4
5
|
import type { AutoMergeResult } from "../managers/ConflictManager.js";
|
|
5
6
|
import type { IServiceHub } from "../services/base/IService.js";
|
|
6
7
|
import type { ServiceContext } from "../services/base/ServiceBase.js";
|
|
@@ -33,7 +34,12 @@ export type GeneratedChunk = {
|
|
|
33
34
|
id: DocumentID;
|
|
34
35
|
piece: string;
|
|
35
36
|
};
|
|
37
|
+
/** Optional host IO bound afresh to each prepared database instance. */
|
|
38
|
+
export interface LiveSyncLocalDBRuntimeOptions {
|
|
39
|
+
createChunkFetchSource?: (database: PouchDB.Database<EntryDoc>) => ChunkFetchSource;
|
|
40
|
+
}
|
|
36
41
|
export declare class LiveSyncLocalDB {
|
|
42
|
+
private readonly runtimeOptions;
|
|
37
43
|
auth: Credential;
|
|
38
44
|
dbname: string;
|
|
39
45
|
settings: RemoteDBSettings;
|
|
@@ -52,7 +58,7 @@ export declare class LiveSyncLocalDB {
|
|
|
52
58
|
/** Share owner retirement across every close path for one physical database lifetime. */
|
|
53
59
|
private activeReplicationClose?;
|
|
54
60
|
private databaseUnloadNotification?;
|
|
55
|
-
constructor(dbname: string, env: LiveSyncLocalDBEnv);
|
|
61
|
+
constructor(dbname: string, env: LiveSyncLocalDBEnv, runtimeOptions?: LiveSyncLocalDBRuntimeOptions);
|
|
56
62
|
/** Notify dependent services at most once for each physical database lifecycle. */
|
|
57
63
|
private notifyDatabaseUnload;
|
|
58
64
|
onunload(): void;
|
|
@@ -16,6 +16,18 @@ function getNoFromRev(rev) {
|
|
|
16
16
|
return parseInt(rev.split("-")[0]);
|
|
17
17
|
}
|
|
18
18
|
class LiveSyncLocalDB {
|
|
19
|
+
constructor(dbname, env, runtimeOptions = {}) {
|
|
20
|
+
this.runtimeOptions = runtimeOptions;
|
|
21
|
+
this.auth = {
|
|
22
|
+
username: "",
|
|
23
|
+
password: ""
|
|
24
|
+
};
|
|
25
|
+
this.dbname = dbname;
|
|
26
|
+
this.env = env;
|
|
27
|
+
this._log = createInstanceLogFunction("LiveSyncLocalDB", this.env.services.API);
|
|
28
|
+
this.refreshSettings();
|
|
29
|
+
}
|
|
30
|
+
runtimeOptions;
|
|
19
31
|
auth;
|
|
20
32
|
dbname;
|
|
21
33
|
settings;
|
|
@@ -47,16 +59,6 @@ class LiveSyncLocalDB {
|
|
|
47
59
|
/** Share owner retirement across every close path for one physical database lifetime. */
|
|
48
60
|
activeReplicationClose;
|
|
49
61
|
databaseUnloadNotification;
|
|
50
|
-
constructor(dbname, env) {
|
|
51
|
-
this.auth = {
|
|
52
|
-
username: "",
|
|
53
|
-
password: ""
|
|
54
|
-
};
|
|
55
|
-
this.dbname = dbname;
|
|
56
|
-
this.env = env;
|
|
57
|
-
this._log = createInstanceLogFunction("LiveSyncLocalDB", this.env.services.API);
|
|
58
|
-
this.refreshSettings();
|
|
59
|
-
}
|
|
60
62
|
/** Notify dependent services at most once for each physical database lifecycle. */
|
|
61
63
|
notifyDatabaseUnload() {
|
|
62
64
|
if (!this.databaseUnloadNotification) {
|
|
@@ -170,6 +172,7 @@ class LiveSyncLocalDB {
|
|
|
170
172
|
this.databaseUnloadNotification = void 0;
|
|
171
173
|
const manager = new LiveSyncManagers({
|
|
172
174
|
database: this.localDatabase,
|
|
175
|
+
chunkFetchSource: this.runtimeOptions?.createChunkFetchSource?.(this.localDatabase),
|
|
173
176
|
APIService: this.env.services.API,
|
|
174
177
|
pathService: this.env.services.path,
|
|
175
178
|
replicatorService: this.env.services.replicator,
|
|
@@ -2,6 +2,14 @@ import { type EntryDoc } from "../common/types.js";
|
|
|
2
2
|
type PathIdentitySettings = {
|
|
3
3
|
pathNormalizationVersion?: number;
|
|
4
4
|
};
|
|
5
|
+
export type PathIdentityInspection = {
|
|
6
|
+
readonly status: "empty";
|
|
7
|
+
} | {
|
|
8
|
+
readonly status: "configured";
|
|
9
|
+
readonly pathNormalizationVersion: 0 | 1;
|
|
10
|
+
};
|
|
11
|
+
/** Read the existing declaration without changing a remote or declaring a new identity. */
|
|
12
|
+
export declare function inspectDatabasePathIdentity(database: PouchDB.Database<EntryDoc>): Promise<PathIdentityInspection>;
|
|
5
13
|
export declare function assertMatchingPathIdentityFeature(document: unknown, settings: PathIdentitySettings): void;
|
|
6
14
|
/** Refuse an implicit migration. A fresh database may declare its identity before its first Metadata write. */
|
|
7
15
|
export declare function assertDatabasePathIdentity(database: PouchDB.Database<EntryDoc>, settings: PathIdentitySettings, declare?: boolean): Promise<void>;
|
|
@@ -2,11 +2,28 @@ import { VERSIONING_DOCID } from "../common/types.js";
|
|
|
2
2
|
import { getPathNormalizationVersion } from "../common/pathNormalization.js";
|
|
3
3
|
import {
|
|
4
4
|
assessRemoteFeatureDocument,
|
|
5
|
+
describeRemoteFeatureRejection,
|
|
5
6
|
NFC_PATH_IDENTITY_FEATURE,
|
|
6
7
|
REMOTE_FEATURE_GENERATION
|
|
7
8
|
} from "./remoteFeatureCompatibility.js";
|
|
8
9
|
import { isErrorOfMissingDoc } from "./utils_couchdb.js";
|
|
9
10
|
import { bumpRemoteVersion } from "./negotiation.js";
|
|
11
|
+
async function inspectDatabasePathIdentity(database) {
|
|
12
|
+
let document;
|
|
13
|
+
try {
|
|
14
|
+
document = await database.get(VERSIONING_DOCID);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (!isErrorOfMissingDoc(error)) throw error;
|
|
17
|
+
return (await database.info()).doc_count === 0 ? { status: "empty" } : { status: "configured", pathNormalizationVersion: 0 };
|
|
18
|
+
}
|
|
19
|
+
const assessment = assessRemoteFeatureDocument(document);
|
|
20
|
+
if (assessment.status === "older-generation") return { status: "configured", pathNormalizationVersion: 0 };
|
|
21
|
+
if (assessment.status !== "supported") throw new Error(describeRemoteFeatureRejection(assessment));
|
|
22
|
+
return {
|
|
23
|
+
status: "configured",
|
|
24
|
+
pathNormalizationVersion: assessment.usedFeatures.includes(NFC_PATH_IDENTITY_FEATURE) ? 1 : 0
|
|
25
|
+
};
|
|
26
|
+
}
|
|
10
27
|
function assertMatchingPathIdentityFeature(document, settings) {
|
|
11
28
|
const assessment = assessRemoteFeatureDocument(document);
|
|
12
29
|
const declared = assessment.status === "supported" && assessment.usedFeatures.includes(NFC_PATH_IDENTITY_FEATURE);
|
|
@@ -60,5 +77,6 @@ async function assertDatabasePathIdentity(database, settings, declare = false) {
|
|
|
60
77
|
}
|
|
61
78
|
export {
|
|
62
79
|
assertDatabasePathIdentity,
|
|
63
|
-
assertMatchingPathIdentityFeature
|
|
80
|
+
assertMatchingPathIdentityFeature,
|
|
81
|
+
inspectDatabasePathIdentity
|
|
64
82
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PathIdentityInspection } from "../pouchdb/pathIdentityCompatibility.js";
|
|
1
2
|
import type { RemoteDBSettings, RemotePreferredTweakResult } from "../common/types.js";
|
|
2
3
|
import type { JoinedDevicesSnapshot, ForgetJoinedDeviceResult } from "./joinedDevices.js";
|
|
3
4
|
import type { RemoteDBStatus } from "./LiveSyncAbstractReplicator.js";
|
|
@@ -36,6 +37,8 @@ export type RemoteConnectionProbeResult = {
|
|
|
36
37
|
export interface RemoteConnectionProbe {
|
|
37
38
|
check(options?: RemoteConnectionProbeOptions): Promise<RemoteConnectionProbeResult>;
|
|
38
39
|
getStatus(): Promise<false | RemoteDBStatus>;
|
|
40
|
+
/** Inspect a central remote before choosing initial host settings. */
|
|
41
|
+
inspectPathIdentity?(): Promise<PathIdentityInspection>;
|
|
39
42
|
dispose(): Promise<void>;
|
|
40
43
|
}
|
|
41
44
|
/** A finite preferred-tweak reader bound to one trial configuration. */
|
|
@@ -169,7 +169,8 @@ class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator {
|
|
|
169
169
|
value.enableCompression,
|
|
170
170
|
value.pathNormalizationVersion,
|
|
171
171
|
value.readChunksOnline,
|
|
172
|
-
value.useRequestAPI
|
|
172
|
+
value.useRequestAPI,
|
|
173
|
+
value.useCouchDBCacheControlHeadersOnly === true
|
|
173
174
|
]);
|
|
174
175
|
const binding = identity(snapshot);
|
|
175
176
|
let connection;
|
|
@@ -721,7 +722,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
721
722
|
if (cancellationRequested()) {
|
|
722
723
|
return false;
|
|
723
724
|
}
|
|
724
|
-
if (await this.ensurePBKDF2Salt(setting, showResult,
|
|
725
|
+
if (await this.ensurePBKDF2Salt(setting, showResult, false) === false || cancellationRequested()) {
|
|
725
726
|
return false;
|
|
726
727
|
}
|
|
727
728
|
if (this.controller) {
|
|
@@ -1442,7 +1443,8 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1442
1443
|
{
|
|
1443
1444
|
...connectionOptions,
|
|
1444
1445
|
encryptionAlgorithm: settings.E2EEAlgorithm,
|
|
1445
|
-
encryptInternalMetadata: usesEncryptedInternalMetadata(settings)
|
|
1446
|
+
encryptInternalMetadata: usesEncryptedInternalMetadata(settings),
|
|
1447
|
+
useCouchDBCacheControlHeadersOnly: settings.useCouchDBCacheControlHeadersOnly === true
|
|
1446
1448
|
}
|
|
1447
1449
|
);
|
|
1448
1450
|
}
|
|
@@ -13,3 +13,5 @@ export { assessRemoteFeatureDocument, describeRemoteFeatureRejection, ENCRYPTED_
|
|
|
13
13
|
export type { RemoteFeatureAssessment } from "../pouchdb/remoteFeatureCompatibility.js";
|
|
14
14
|
export type { JoinedDevice, JoinedDevicesSnapshot, ForgetJoinedDeviceResult } from "./joinedDevices.js";
|
|
15
15
|
export type { ReplicationStatics, CouchDBReplicationPhase, CouchDBReplicationProgress, ReplicationCheckpointProgress, } from "../common/models/shared.definition.js";
|
|
16
|
+
export { inspectDatabasePathIdentity } from "../pouchdb/pathIdentityCompatibility.js";
|
|
17
|
+
export type { PathIdentityInspection } from "../pouchdb/pathIdentityCompatibility.js";
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
REMOTE_FEATURE_GENERATION,
|
|
44
44
|
usesEncryptedInternalMetadata
|
|
45
45
|
} from "../pouchdb/remoteFeatureCompatibility.js";
|
|
46
|
+
import { inspectDatabasePathIdentity } from "../pouchdb/pathIdentityCompatibility.js";
|
|
46
47
|
export {
|
|
47
48
|
CAPABILITY_NOT_APPLICABLE,
|
|
48
49
|
CAPABILITY_NOT_IMPLEMENTED,
|
|
@@ -72,6 +73,7 @@ export {
|
|
|
72
73
|
centralRemoteAdministrationVerified,
|
|
73
74
|
defineReplicatorProviderDefinitions,
|
|
74
75
|
describeRemoteFeatureRejection,
|
|
76
|
+
inspectDatabasePathIdentity,
|
|
75
77
|
isCentralRemoteAdministrationVerified,
|
|
76
78
|
isReplicationCompleted,
|
|
77
79
|
milestoneSatisfiesCentralRemoteAdministration,
|
|
@@ -62,6 +62,7 @@ export declare class JournalSyncCore {
|
|
|
62
62
|
private _createSendCompressTransformStream;
|
|
63
63
|
private _createSendUploadWritableStream;
|
|
64
64
|
sendLocalJournal(showMessage?: boolean): Promise<boolean>;
|
|
65
|
+
hasRemoteJournals(): Promise<boolean>;
|
|
65
66
|
_getRemoteJournals(): Promise<string[]>;
|
|
66
67
|
processDocuments(allDocs: ProcessingEntry[]): Promise<boolean>;
|
|
67
68
|
private _createReceiveReadableStream;
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
encryptBinary as encryptBinaryHKDF,
|
|
34
34
|
decryptBinary as decryptBinaryHKDF
|
|
35
35
|
} from "octagonal-wheels/encryption/hkdf";
|
|
36
|
+
import { assertDatabasePathIdentity } from "../../pouchdb/pathIdentityCompatibility.js";
|
|
36
37
|
const RECORD_SPLIT = `
|
|
37
38
|
`;
|
|
38
39
|
const UNIT_SPLIT = ``;
|
|
@@ -45,6 +46,9 @@ function serializeDoc(doc) {
|
|
|
45
46
|
}
|
|
46
47
|
return te.encode(JSON.stringify(doc) + RECORD_SPLIT);
|
|
47
48
|
}
|
|
49
|
+
function isJournalKey(key) {
|
|
50
|
+
return !key.startsWith("_");
|
|
51
|
+
}
|
|
48
52
|
class JournalSyncCore {
|
|
49
53
|
_settings;
|
|
50
54
|
storage;
|
|
@@ -217,7 +221,13 @@ class JournalSyncCore {
|
|
|
217
221
|
this.syncParamsHandler = void 0;
|
|
218
222
|
}
|
|
219
223
|
async resetCheckpointInfo() {
|
|
220
|
-
await this.updateCheckPointInfo((
|
|
224
|
+
await this.updateCheckPointInfo(() => ({
|
|
225
|
+
...CheckPointInfoDefault,
|
|
226
|
+
knownIDs: /* @__PURE__ */ new Set(),
|
|
227
|
+
sentIDs: /* @__PURE__ */ new Set(),
|
|
228
|
+
receivedFiles: /* @__PURE__ */ new Set(),
|
|
229
|
+
sentFiles: /* @__PURE__ */ new Set()
|
|
230
|
+
}));
|
|
221
231
|
}
|
|
222
232
|
getJournalEpochFromSyncParams(params) {
|
|
223
233
|
return `${params.protocolVersion}:${params.pbkdf2salt}`;
|
|
@@ -262,32 +272,33 @@ class JournalSyncCore {
|
|
|
262
272
|
}
|
|
263
273
|
async resetBucket() {
|
|
264
274
|
this.resetAllCaches();
|
|
265
|
-
let files = [];
|
|
266
275
|
try {
|
|
276
|
+
const skippedFiles = [];
|
|
277
|
+
let from = "";
|
|
278
|
+
let files;
|
|
267
279
|
do {
|
|
268
|
-
files = await this.storage.listFiles(
|
|
269
|
-
if (files.length
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
280
|
+
files = await this.storage.listFiles(from, 100);
|
|
281
|
+
if (files.length > 0) {
|
|
282
|
+
const nextFrom = files[files.length - 1];
|
|
283
|
+
if (nextFrom <= from) {
|
|
284
|
+
throw new Error("Journal reset listing did not advance. Control documents have been retained.");
|
|
285
|
+
}
|
|
286
|
+
skippedFiles.push(...files.filter((key) => !isJournalKey(key)));
|
|
287
|
+
const journals = files.filter(isJournalKey);
|
|
288
|
+
if (journals.length > 0 && !await this.storage.deleteFiles(journals)) return false;
|
|
289
|
+
from = nextFrom;
|
|
274
290
|
}
|
|
275
|
-
} while (files.length
|
|
291
|
+
} while (files.length > 0);
|
|
292
|
+
while (skippedFiles.length > 0) {
|
|
293
|
+
if (!await this.storage.deleteFiles(skippedFiles.splice(0, 100))) return false;
|
|
294
|
+
}
|
|
276
295
|
} catch (ex) {
|
|
277
296
|
Logger(`WARNING! Could not delete files.`, LOG_LEVEL_NOTICE, "reset-bucket");
|
|
278
297
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
|
279
298
|
return false;
|
|
280
299
|
}
|
|
281
|
-
const journals = await this._getRemoteJournals();
|
|
282
|
-
if (journals.length == 0) {
|
|
283
|
-
Logger("Nothing to delete!", LOG_LEVEL_NOTICE);
|
|
284
|
-
return true;
|
|
285
|
-
}
|
|
286
|
-
if (!await this.storage.deleteFiles(journals)) {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
Logger(`${journals.length} items has been deleted!`, LOG_LEVEL_NOTICE);
|
|
290
300
|
await this.resetCheckpointInfo();
|
|
301
|
+
Logger("Remote bucket cleared", LOG_LEVEL_NOTICE);
|
|
291
302
|
return true;
|
|
292
303
|
}
|
|
293
304
|
getRemoteKey() {
|
|
@@ -502,6 +513,7 @@ class JournalSyncCore {
|
|
|
502
513
|
});
|
|
503
514
|
}
|
|
504
515
|
async sendLocalJournal(showMessage = false) {
|
|
516
|
+
await assertDatabasePathIdentity(this.db, this.currentSettings);
|
|
505
517
|
this.updateInfo({ syncStatus: "JOURNAL_SEND" });
|
|
506
518
|
return await shareRunningResult("send_journal_stream", async () => {
|
|
507
519
|
this.requestedStop = false;
|
|
@@ -544,12 +556,15 @@ class JournalSyncCore {
|
|
|
544
556
|
}
|
|
545
557
|
});
|
|
546
558
|
}
|
|
559
|
+
async hasRemoteJournals() {
|
|
560
|
+
return (await this.storage.listFiles("", 1)).some(isJournalKey);
|
|
561
|
+
}
|
|
547
562
|
async _getRemoteJournals() {
|
|
548
563
|
const checkPointInfo = await this.getCheckpointInfo();
|
|
549
564
|
const StartAfter = [...checkPointInfo.receivedFiles.keys()].sort(
|
|
550
565
|
(a, b) => b.localeCompare(a, void 0, { numeric: true })
|
|
551
566
|
)[0];
|
|
552
|
-
const files = (await this.storage.listFiles(StartAfter)).filter(
|
|
567
|
+
const files = (await this.storage.listFiles(StartAfter)).filter(isJournalKey);
|
|
553
568
|
if (!files) return [];
|
|
554
569
|
return files.sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
|
|
555
570
|
}
|
|
@@ -723,6 +738,7 @@ class JournalSyncCore {
|
|
|
723
738
|
});
|
|
724
739
|
}
|
|
725
740
|
async receiveRemoteJournal(showMessage = false) {
|
|
741
|
+
await assertDatabasePathIdentity(this.db, this.currentSettings);
|
|
726
742
|
this.updateInfo({ syncStatus: "JOURNAL_RECEIVE" });
|
|
727
743
|
return await shareRunningResult("receive_journal_stream", async () => {
|
|
728
744
|
this.requestedStop = false;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PathIdentityInspection } from "../../pouchdb/pathIdentityCompatibility.js";
|
|
1
2
|
import { type RemoteDBSettings, type ChunkVersionRange, type NodeData, type RemotePreferredTweakResult, type TweakAssessment } from "../../common/types.js";
|
|
2
3
|
import { JournalSyncCore } from "./JournalSyncCore.js";
|
|
3
4
|
import { LiveSyncAbstractReplicator, type RemoteDBStatus } from "../LiveSyncAbstractReplicator.js";
|
|
@@ -23,7 +24,16 @@ export declare class LiveSyncJournalReplicator extends LiveSyncAbstractReplicato
|
|
|
23
24
|
private journalTransferStopGeneration;
|
|
24
25
|
getReplicationPBKDF2Salt(setting: RemoteDBSettings, refresh?: boolean): Promise<Uint8Array<ArrayBuffer>>;
|
|
25
26
|
setupJournalSyncClient(settings?: RemoteDBSettings): JournalSyncCore;
|
|
26
|
-
|
|
27
|
+
private isBucketPathIdentityCompatible;
|
|
28
|
+
private isBucketIdDerivationCompatible;
|
|
29
|
+
private assertBucketIdentity;
|
|
30
|
+
/** Inspect identity without publishing this node, preparing a seed, or changing checkpoints. */
|
|
31
|
+
assertRemotePathIdentity(setting: RemoteDBSettings): Promise<void>;
|
|
32
|
+
/** Read path identity for initial setup without publishing a node or preparing a seed. */
|
|
33
|
+
inspectRemotePathIdentity(setting: RemoteDBSettings): Promise<PathIdentityInspection>;
|
|
34
|
+
/** Lock existing journals for replacement without declaring the replacement's identity. */
|
|
35
|
+
markRemoteLockedForRebuild(setting: RemoteDBSettings): Promise<void>;
|
|
36
|
+
ensureBucketIsCompatible(deviceNodeID: string, currentVersionRange: ChunkVersionRange, setting?: RemoteDBSettings, client?: Pick<JournalSyncCore, "downloadJsonWithResult" | "getCheckpointInfo" | "uploadJson" | "hasRemoteJournals">, recordTweakAssessment?: (assessment: TweakAssessment) => void): Promise<ENSURE_DB_RESULT>;
|
|
27
37
|
constructor(env: LiveSyncJournalReplicatorEnv);
|
|
28
38
|
migrate(from: number, to: number): Promise<boolean>;
|
|
29
39
|
/**
|
|
@@ -62,8 +72,6 @@ export declare class LiveSyncJournalReplicator extends LiveSyncAbstractReplicato
|
|
|
62
72
|
setPreferredRemoteTweakSettings(setting: RemoteDBSettings): Promise<void>;
|
|
63
73
|
getRemotePreferredTweakValues(setting: RemoteDBSettings): Promise<RemotePreferredTweakResult>;
|
|
64
74
|
getRemoteStatus(setting: RemoteDBSettings): Promise<false | RemoteDBStatus>;
|
|
65
|
-
private supportsLegacyPathIdentity;
|
|
66
|
-
private assertLegacyPathIdentity;
|
|
67
75
|
private getOwnJoinedDeviceId;
|
|
68
76
|
getJoinedDevices(setting: RemoteDBSettings): Promise<JoinedDevicesSnapshot>;
|
|
69
77
|
forgetJoinedDevice(setting: RemoteDBSettings, deviceId: string): Promise<ForgetJoinedDeviceResult>;
|