@vrtmrz/livesync-commonlib 0.1.25 → 0.1.26
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/dist/interfaces/DatabaseFileAccess.d.ts +31 -3
- package/dist/interfaces/FileReflectionProvenance.d.ts +2 -2
- package/dist/managers/EntryManager/EntryManager.d.ts +4 -2
- package/dist/managers/EntryManager/EntryManager.js +9 -4
- package/dist/managers/EntryManager/EntryManagerImpls.d.ts +12 -4
- package/dist/managers/EntryManager/EntryManagerImpls.js +27 -9
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +4 -2
- package/dist/pouchdb/LiveSyncLocalDB.js +8 -4
- package/dist/serviceModules/ServiceDatabaseFileAccessBase.d.ts +26 -2
- package/dist/serviceModules/ServiceDatabaseFileAccessBase.js +57 -5
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +119 -1
- package/dist/serviceModules/ServiceFileHandlerBase.js +450 -70
- package/docs/conflict-resolution.md +28 -15
- package/docs/proven-in-use.md +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,10 @@ import type { FilePathWithPrefix, LoadedEntry, MetaEntry, UXFileInfo, UXFileInfo
|
|
|
2
2
|
export interface DatabaseFileAccess {
|
|
3
3
|
delete: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string) => Promise<boolean>;
|
|
4
4
|
store: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise<boolean>;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Store a child of the exact supplied revision, even after that branch has advanced.
|
|
7
|
+
* An undefined base uses the current winner. Return the created revision, or `false`.
|
|
8
|
+
*/
|
|
6
9
|
storeWithBaseRevision: (file: UXFileInfo, baseRevision: string | undefined, skipCheck?: boolean) => Promise<string | false>;
|
|
7
10
|
/**
|
|
8
11
|
* Store a file as a child of an exact revision only while that revision remains a live leaf.
|
|
@@ -14,8 +17,18 @@ export interface DatabaseFileAccess {
|
|
|
14
17
|
* write without creating a Metadata successor.
|
|
15
18
|
*/
|
|
16
19
|
storeWithLiveBaseRevision: (file: UXFileInfo, baseRevision: string, skipCheck?: boolean) => Promise<string | false>;
|
|
20
|
+
/**
|
|
21
|
+
* Preserve content of unknown ancestry as a fresh parentless revision of the same document.
|
|
22
|
+
* Existing branches remain intact; no historical byte match is used as the parent.
|
|
23
|
+
* Return the created revision, or `false` when no Metadata revision was stored.
|
|
24
|
+
*/
|
|
25
|
+
storeIndependentRevision: (file: UXFileInfo, skipCheck?: boolean) => Promise<string | false>;
|
|
17
26
|
storeAsConflictedRevision: (file: UXFileInfo, currentRev: string, skipCheck?: boolean) => Promise<boolean>;
|
|
18
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Store a sibling of `currentRev` below its parent and return the created revision.
|
|
29
|
+
* Return `false` if that parent is unavailable. Use `storeIndependentRevision`
|
|
30
|
+
* when the storage content has no known ancestor.
|
|
31
|
+
*/
|
|
19
32
|
storeAsConflictedRevisionWithResult: (file: UXFileInfo, currentRev: string, skipCheck?: boolean) => Promise<string | false>;
|
|
20
33
|
/** Store a user deletion as a visible logical-deletion child of an exact revision. */
|
|
21
34
|
storeDeletionWithBaseRevision: (file: UXFileInfoStub | FilePathWithPrefix, baseRevision: string) => Promise<string | false>;
|
|
@@ -24,9 +37,24 @@ export interface DatabaseFileAccess {
|
|
|
24
37
|
hasContentInRevisionHistory: (file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer, currentRev?: string) => Promise<boolean>;
|
|
25
38
|
/** Return every available revision whose content exactly matches the supplied bytes. */
|
|
26
39
|
findContentRevisions: (file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer, currentRev?: string) => Promise<string[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Return current, non-deleted leaf revisions whose decoded content matches the supplied bytes.
|
|
42
|
+
* Include the winning and conflicting leaves, but never their ancestors. Read only locally
|
|
43
|
+
* available Chunks, without requesting remote data or waiting for missing Chunks to arrive.
|
|
44
|
+
*
|
|
45
|
+
* A unique match can recover provenance; several matches avoid a duplicate write but leave
|
|
46
|
+
* its origin ambiguous. An empty result means no match was confirmed, including when data
|
|
47
|
+
* could not be read. This read neither records provenance nor reserves a leaf for a later write.
|
|
48
|
+
*/
|
|
49
|
+
findLiveContentRevisions: (file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer) => Promise<string[]>;
|
|
27
50
|
fetch: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean) => Promise<UXFileInfo | false>;
|
|
28
51
|
fetchEntryFromMeta: (meta: MetaEntry, waitForReady?: boolean, skipCheck?: boolean) => Promise<LoadedEntry | false>;
|
|
29
52
|
fetchEntryMeta: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, skipCheck?: boolean) => Promise<MetaEntry | false>;
|
|
30
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Load a revision's content, or return `false` when it cannot be loaded.
|
|
55
|
+
* `localOnly` overrides `waitForReady`: missing Chunks cause failure without remote requests
|
|
56
|
+
* or waiting for delivery, so provenance checks can use only content already available locally.
|
|
57
|
+
*/
|
|
58
|
+
fetchEntry: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean, localOnly?: boolean) => Promise<LoadedEntry | false>;
|
|
31
59
|
getConflictedRevs: (file: UXFileInfoStub | FilePathWithPrefix) => Promise<string[]>;
|
|
32
60
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { FilePathWithPrefix } from "../common/types.js";
|
|
2
2
|
import type { SimpleStore } from "../common/utils.js";
|
|
3
3
|
export type FileReflectionProvenanceRecord = {
|
|
4
|
-
/** Exact
|
|
4
|
+
/** Exact revision most recently saved from or reflected in storage; the base for subsequent edits. */
|
|
5
5
|
revision: string;
|
|
6
|
-
/** Raw modification time
|
|
6
|
+
/** Raw modification time of the saved snapshot or storage observed after reflection. */
|
|
7
7
|
observedStorageMtime?: number;
|
|
8
8
|
};
|
|
9
9
|
export interface FileReflectionProvenance {
|
|
@@ -30,10 +30,12 @@ export declare class EntryManager {
|
|
|
30
30
|
isTargetFile(filenameSrc: string): boolean;
|
|
31
31
|
prepareChunk(piece: string): Promise<GeneratedChunk>;
|
|
32
32
|
getDBEntryMeta(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, includeDeleted?: boolean): Promise<false | LoadedEntry>;
|
|
33
|
-
getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise<false | LoadedEntry>;
|
|
34
|
-
getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise<false | LoadedEntry>;
|
|
33
|
+
getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean, localOnly?: boolean): Promise<false | LoadedEntry>;
|
|
34
|
+
getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean, localOnly?: boolean): Promise<false | LoadedEntry>;
|
|
35
35
|
deleteDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise<boolean>;
|
|
36
36
|
storeDeletionAtRevision(path: FilePathWithPrefix | FilePath, baseRevision: string): Promise<false | PouchDB.Core.Response>;
|
|
37
37
|
putDBEntry(note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise<false | PouchDB.Core.Response>;
|
|
38
38
|
putDBEntryWithLiveBaseRevision(note: SavingEntry, baseRevision: string, onlyChunks?: boolean): Promise<false | PouchDB.Core.Response>;
|
|
39
|
+
/** Store unknown ancestry as a fresh root; return the Metadata write result or `false`. */
|
|
40
|
+
putDBEntryAsIndependentRoot(note: SavingEntry): Promise<false | PouchDB.Core.Response>;
|
|
39
41
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
prepareChunk,
|
|
11
11
|
putDBEntry,
|
|
12
12
|
putDBEntryWithLiveBaseRevision,
|
|
13
|
+
putDBEntryAsIndependentRoot,
|
|
13
14
|
storeDeletionByPathAtRevision
|
|
14
15
|
} from "./EntryManagerImpls.js";
|
|
15
16
|
class EntryManager {
|
|
@@ -57,11 +58,11 @@ class EntryManager {
|
|
|
57
58
|
async getDBEntryMeta(path, opt, includeDeleted = false) {
|
|
58
59
|
return await getDBEntryMetaByPath(this.serviceHost, this, path, opt, includeDeleted);
|
|
59
60
|
}
|
|
60
|
-
async getDBEntry(path, opt, dump = false, waitForReady = true, includeDeleted = false) {
|
|
61
|
-
return await getDBEntryByPath(this.serviceHost, this, path, opt, dump, waitForReady, includeDeleted);
|
|
61
|
+
async getDBEntry(path, opt, dump = false, waitForReady = true, includeDeleted = false, localOnly = false) {
|
|
62
|
+
return await getDBEntryByPath(this.serviceHost, this, path, opt, dump, waitForReady, includeDeleted, localOnly);
|
|
62
63
|
}
|
|
63
|
-
async getDBEntryFromMeta(meta, dump = false, waitForReady = true) {
|
|
64
|
-
return await getDBEntryFromMeta(this.serviceHost, this, meta, dump, waitForReady);
|
|
64
|
+
async getDBEntryFromMeta(meta, dump = false, waitForReady = true, localOnly = false) {
|
|
65
|
+
return await getDBEntryFromMeta(this.serviceHost, this, meta, dump, waitForReady, localOnly);
|
|
65
66
|
}
|
|
66
67
|
async deleteDBEntry(path, opt) {
|
|
67
68
|
return await deleteDBEntryByPath(this.serviceHost, this, path, opt);
|
|
@@ -75,6 +76,10 @@ class EntryManager {
|
|
|
75
76
|
async putDBEntryWithLiveBaseRevision(note, baseRevision, onlyChunks) {
|
|
76
77
|
return await putDBEntryWithLiveBaseRevision(this.serviceHost, this, note, baseRevision, onlyChunks);
|
|
77
78
|
}
|
|
79
|
+
/** Store unknown ancestry as a fresh root; return the Metadata write result or `false`. */
|
|
80
|
+
async putDBEntryAsIndependentRoot(note) {
|
|
81
|
+
return await putDBEntryAsIndependentRoot(this.serviceHost, this, note);
|
|
82
|
+
}
|
|
78
83
|
}
|
|
79
84
|
export {
|
|
80
85
|
EntryManager
|
|
@@ -20,20 +20,28 @@ export declare function putDBEntry(host: NecessaryServicesInterfaces<"path" | "s
|
|
|
20
20
|
* base has already been advanced, so the caller receives `false` and can retry from fresh state.
|
|
21
21
|
*/
|
|
22
22
|
export declare function putDBEntryWithLiveBaseRevision(host: NecessaryServicesInterfaces<"path" | "setting", never>, managers: NecessaryManagers<"localDatabase" | "chunkManager" | "hashManager" | "splitter">, note: SavingEntry, baseRevision: string, onlyChunks?: boolean): Promise<false | PouchDB.Core.Response>;
|
|
23
|
+
/**
|
|
24
|
+
* Preserve content with unknown ancestry as a fresh parentless revision of the same document.
|
|
25
|
+
* Store the body as Chunks as usual, then insert a Metadata root with a random revision ID using
|
|
26
|
+
* `new_edits: false`. This retains existing branches without reusing an identical historical root.
|
|
27
|
+
* Return the Metadata write result, or `false` when the entry cannot be stored.
|
|
28
|
+
*/
|
|
29
|
+
export declare function putDBEntryAsIndependentRoot(host: NecessaryServicesInterfaces<"path" | "setting", never>, managers: NecessaryManagers<"localDatabase" | "chunkManager" | "hashManager" | "splitter">, note: SavingEntry): Promise<false | PouchDB.Core.Response>;
|
|
23
30
|
export declare function isTargetFile(host: NecessaryServicesInterfaces<"setting", never>, filenameSrc: string): boolean;
|
|
24
31
|
export declare function prepareChunk({ chunkManager, hashManager }: NecessaryManagers<"chunkManager" | "hashManager">, piece: string): Promise<GeneratedChunk>;
|
|
25
32
|
export declare function getDBEntryMetaByPath(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase }: NecessaryManagers<"localDatabase">, path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, includeDeleted?: boolean): Promise<false | LoadedEntry>;
|
|
26
33
|
export declare function isLegacyNote(meta: LoadedEntry | MetaEntry): boolean;
|
|
27
34
|
export declare function canUseOnDemandChunking(settings: ObsidianLiveSyncSettings): boolean;
|
|
28
35
|
/**
|
|
29
|
-
* Decide how to retrieve chunks based on settings and
|
|
36
|
+
* Decide how to retrieve chunks based on settings and the caller's read policy.
|
|
30
37
|
* `waitForReady` allows an already-observable finite delivery lifecycle to finish.
|
|
38
|
+
* `localOnly` overrides both settings and waiting for provenance comparisons.
|
|
31
39
|
*/
|
|
32
|
-
export declare function computeChunkRetrievalMethod(waitForReady: boolean, settings: ObsidianLiveSyncSettings): {
|
|
40
|
+
export declare function computeChunkRetrievalMethod(waitForReady: boolean, settings: ObsidianLiveSyncSettings, localOnly?: boolean): {
|
|
33
41
|
waitForDelivery: boolean;
|
|
34
42
|
preventRemoteRequest: boolean;
|
|
35
43
|
};
|
|
36
|
-
export declare function getDBEntryFromMeta(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase, chunkManager }: NecessaryManagers<"localDatabase" | "chunkManager">, meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise<false | (import("../../common/types.js").DatabaseEntry & EntryBase & import("../../common/types.js").EntryWithEden & {
|
|
44
|
+
export declare function getDBEntryFromMeta(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase, chunkManager }: NecessaryManagers<"localDatabase" | "chunkManager">, meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean, localOnly?: boolean): Promise<false | (import("../../common/types.js").DatabaseEntry & EntryBase & import("../../common/types.js").EntryWithEden & {
|
|
37
45
|
path: FilePathWithPrefix;
|
|
38
46
|
data: string | string[];
|
|
39
47
|
type: import("../../common/models/db.type.js").EntryTypes["NOTE_LEGACY"];
|
|
@@ -55,7 +63,7 @@ export declare function getDBEntryFromMeta(host: NecessaryServicesInterfaces<"pa
|
|
|
55
63
|
data: string | string[];
|
|
56
64
|
datatype: import("../../common/types.js").EntryTypeNotes;
|
|
57
65
|
} & PouchDB.Core.IdMeta)>;
|
|
58
|
-
export declare function getDBEntryByPath(host: NecessaryServicesInterfaces<"path" | "setting", never>, managers: NecessaryManagers<"localDatabase" | "chunkManager">, path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise<false | (import("../../common/types.js").DatabaseEntry & EntryBase & import("../../common/types.js").EntryWithEden & {
|
|
66
|
+
export declare function getDBEntryByPath(host: NecessaryServicesInterfaces<"path" | "setting", never>, managers: NecessaryManagers<"localDatabase" | "chunkManager">, path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean, localOnly?: boolean): Promise<false | (import("../../common/types.js").DatabaseEntry & EntryBase & import("../../common/types.js").EntryWithEden & {
|
|
59
67
|
path: FilePathWithPrefix;
|
|
60
68
|
data: string | string[];
|
|
61
69
|
type: import("../../common/models/db.type.js").EntryTypes["NOTE_LEGACY"];
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "../../common/types.js";
|
|
7
7
|
import { serialized } from "octagonal-wheels/concurrency/lock";
|
|
8
8
|
import { createTextBlob, getFileRegExp, isTextBlob } from "../../common/utils.js";
|
|
9
|
+
import { compatGlobal } from "../../common/coreEnvFunctions.js";
|
|
9
10
|
import { isErrorOfMissingDoc } from "../../pouchdb/utils_couchdb.js";
|
|
10
11
|
import { stripAllPrefixes } from "../../string_and_binary/path.js";
|
|
11
12
|
import { ICHeader, ICXHeader, PSCHeader } from "../../common/models/fileaccess.const.js";
|
|
@@ -126,6 +127,9 @@ async function putDBEntryWithLiveBaseRevision(host, managers, note, baseRevision
|
|
|
126
127
|
throw ex;
|
|
127
128
|
}
|
|
128
129
|
}
|
|
130
|
+
async function putDBEntryAsIndependentRoot(host, managers, note) {
|
|
131
|
+
return await putDBEntryInternal(host, managers, note, false, { mode: "independent-root" });
|
|
132
|
+
}
|
|
129
133
|
async function putDBEntryInternal(host, managers, note, onlyChunks, revisionTarget) {
|
|
130
134
|
const { localDatabase, chunkManager, splitter } = managers;
|
|
131
135
|
const filename = host.services.path.id2path(note._id, note);
|
|
@@ -163,9 +167,9 @@ async function putDBEntryInternal(host, managers, note, onlyChunks, revisionTarg
|
|
|
163
167
|
eden: {}
|
|
164
168
|
};
|
|
165
169
|
return await serialized("file:" + filename, async () => {
|
|
166
|
-
if (revisionTarget.mode
|
|
170
|
+
if (revisionTarget.mode === "force-base" || revisionTarget.mode === "live-base") {
|
|
167
171
|
newDoc._rev = revisionTarget.baseRevision;
|
|
168
|
-
} else {
|
|
172
|
+
} else if (revisionTarget.mode === "latest") {
|
|
169
173
|
try {
|
|
170
174
|
const old = await localDatabase.get(newDoc._id);
|
|
171
175
|
newDoc._rev = old._rev;
|
|
@@ -176,6 +180,15 @@ async function putDBEntryInternal(host, managers, note, onlyChunks, revisionTarg
|
|
|
176
180
|
}
|
|
177
181
|
}
|
|
178
182
|
}
|
|
183
|
+
if (revisionTarget.mode === "independent-root") {
|
|
184
|
+
const randomBytes = compatGlobal.crypto.getRandomValues(new Uint8Array(16));
|
|
185
|
+
const rootId = Array.from(randomBytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
186
|
+
newDoc._rev = `1-${rootId}`;
|
|
187
|
+
const imported = { ...newDoc, _revisions: { start: 1, ids: [rootId] } };
|
|
188
|
+
await localDatabase.bulkDocs([imported], { new_edits: false });
|
|
189
|
+
const stored = await localDatabase.get(newDoc._id, { rev: newDoc._rev });
|
|
190
|
+
return stored._rev === newDoc._rev ? { id: newDoc._id, ok: true, rev: newDoc._rev } : false;
|
|
191
|
+
}
|
|
179
192
|
const r = revisionTarget.mode === "live-base" ? await localDatabase.put(newDoc) : await localDatabase.put(newDoc, { force: true });
|
|
180
193
|
if (r.ok) {
|
|
181
194
|
return r;
|
|
@@ -324,7 +337,10 @@ function canFetchRemotely(settings) {
|
|
|
324
337
|
}
|
|
325
338
|
return true;
|
|
326
339
|
}
|
|
327
|
-
function computeChunkRetrievalMethod(waitForReady, settings) {
|
|
340
|
+
function computeChunkRetrievalMethod(waitForReady, settings, localOnly = false) {
|
|
341
|
+
if (localOnly) {
|
|
342
|
+
return { waitForDelivery: false, preventRemoteRequest: true };
|
|
343
|
+
}
|
|
328
344
|
const isOnDemandFetchEnabled = canFetchRemotely(settings);
|
|
329
345
|
if (!waitForReady) {
|
|
330
346
|
if (isOnDemandFetchEnabled) {
|
|
@@ -343,7 +359,7 @@ function computeChunkRetrievalMethod(waitForReady, settings) {
|
|
|
343
359
|
preventRemoteRequest: !isOnDemandFetchEnabled
|
|
344
360
|
};
|
|
345
361
|
}
|
|
346
|
-
async function respondEntryFromMeta({ localDatabase, chunkManager }, settings, filename, meta, dump, waitForReady) {
|
|
362
|
+
async function respondEntryFromMeta({ localDatabase, chunkManager }, settings, filename, meta, dump, waitForReady, localOnly = false) {
|
|
347
363
|
const dispFilename = stripAllPrefixes(filename);
|
|
348
364
|
const deleted = meta.deleted ?? meta._deleted ?? void 0;
|
|
349
365
|
if (dump) {
|
|
@@ -371,7 +387,7 @@ async function respondEntryFromMeta({ localDatabase, chunkManager }, settings, f
|
|
|
371
387
|
}));
|
|
372
388
|
edenChunks = Object.fromEntries(chunks2.map((e) => [e._id, e]));
|
|
373
389
|
}
|
|
374
|
-
const { waitForDelivery, preventRemoteRequest } = computeChunkRetrievalMethod(waitForReady, settings);
|
|
390
|
+
const { waitForDelivery, preventRemoteRequest } = computeChunkRetrievalMethod(waitForReady, settings, localOnly);
|
|
375
391
|
const childrenKeys = [...meta.children];
|
|
376
392
|
const chunks = await chunkManager.read(
|
|
377
393
|
childrenKeys,
|
|
@@ -421,7 +437,7 @@ async function respondEntryFromMeta({ localDatabase, chunkManager }, settings, f
|
|
|
421
437
|
}
|
|
422
438
|
return false;
|
|
423
439
|
}
|
|
424
|
-
async function getDBEntryFromMeta(host, { localDatabase, chunkManager }, meta, dump = false, waitForReady = true) {
|
|
440
|
+
async function getDBEntryFromMeta(host, { localDatabase, chunkManager }, meta, dump = false, waitForReady = true, localOnly = false) {
|
|
425
441
|
const filename = host.services.path.id2path(meta._id, meta);
|
|
426
442
|
if (!isTargetFile(host, filename)) {
|
|
427
443
|
return false;
|
|
@@ -438,15 +454,16 @@ async function getDBEntryFromMeta(host, { localDatabase, chunkManager }, meta, d
|
|
|
438
454
|
filename,
|
|
439
455
|
meta,
|
|
440
456
|
dump,
|
|
441
|
-
waitForReady
|
|
457
|
+
waitForReady,
|
|
458
|
+
localOnly
|
|
442
459
|
);
|
|
443
460
|
}
|
|
444
461
|
return false;
|
|
445
462
|
}
|
|
446
|
-
async function getDBEntryByPath(host, managers, path, opt, dump = false, waitForReady = true, includeDeleted = false) {
|
|
463
|
+
async function getDBEntryByPath(host, managers, path, opt, dump = false, waitForReady = true, includeDeleted = false, localOnly = false) {
|
|
447
464
|
const meta = await getDBEntryMetaByPath(host, managers, path, opt, includeDeleted);
|
|
448
465
|
if (meta) {
|
|
449
|
-
return await getDBEntryFromMeta(host, managers, meta, dump, waitForReady);
|
|
466
|
+
return await getDBEntryFromMeta(host, managers, meta, dump, waitForReady, localOnly);
|
|
450
467
|
} else {
|
|
451
468
|
return false;
|
|
452
469
|
}
|
|
@@ -540,6 +557,7 @@ export {
|
|
|
540
557
|
isTargetFile,
|
|
541
558
|
prepareChunk,
|
|
542
559
|
putDBEntry,
|
|
560
|
+
putDBEntryAsIndependentRoot,
|
|
543
561
|
putDBEntryWithLiveBaseRevision,
|
|
544
562
|
storeDeletionByPathAtRevision
|
|
545
563
|
};
|
|
@@ -196,12 +196,14 @@ export declare class LiveSyncLocalDB {
|
|
|
196
196
|
bulkDocsRaw<T extends EntryDoc>(docs: Array<PouchDB.Core.PutDocument<T>>, options?: PouchDB.Core.BulkDocsOptions): Promise<Array<PouchDB.Core.Response | PouchDB.Core.Error>>;
|
|
197
197
|
isTargetFile(filenameSrc: string): boolean;
|
|
198
198
|
getDBEntryMeta(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, includeDeleted?: boolean): Promise<false | LoadedEntry>;
|
|
199
|
-
getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise<false | LoadedEntry>;
|
|
200
|
-
getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise<false | LoadedEntry>;
|
|
199
|
+
getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean, localOnly?: boolean): Promise<false | LoadedEntry>;
|
|
200
|
+
getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean, localOnly?: boolean): Promise<false | LoadedEntry>;
|
|
201
201
|
deleteDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise<boolean>;
|
|
202
202
|
storeDeletionAtRevision(path: FilePathWithPrefix | FilePath, baseRevision: string): Promise<false | PouchDB.Core.Response>;
|
|
203
203
|
putDBEntry(note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise<false | PouchDB.Core.Response>;
|
|
204
204
|
putDBEntryWithLiveBaseRevision(note: SavingEntry, baseRevision: string, onlyChunks?: boolean): Promise<false | PouchDB.Core.Response>;
|
|
205
|
+
/** Preserve existing branches and delegate a fresh root write to the entry manager. */
|
|
206
|
+
putDBEntryAsIndependentRoot(note: SavingEntry): Promise<false | PouchDB.Core.Response>;
|
|
205
207
|
getConflictedDoc(path: FilePathWithPrefix, rev: string): Promise<false | diff_result_leaf>;
|
|
206
208
|
tryAutoMerge(path: FilePathWithPrefix, enableMarkdownAutoMerge: boolean): AutoMergeResult;
|
|
207
209
|
}
|
|
@@ -457,11 +457,11 @@ class LiveSyncLocalDB {
|
|
|
457
457
|
async getDBEntryMeta(path, opt, includeDeleted = false) {
|
|
458
458
|
return await this.managers.entryManager.getDBEntryMeta(path, opt, includeDeleted);
|
|
459
459
|
}
|
|
460
|
-
async getDBEntry(path, opt, dump = false, waitForReady = true, includeDeleted = false) {
|
|
461
|
-
return await this.managers.entryManager.getDBEntry(path, opt, dump, waitForReady, includeDeleted);
|
|
460
|
+
async getDBEntry(path, opt, dump = false, waitForReady = true, includeDeleted = false, localOnly = false) {
|
|
461
|
+
return await this.managers.entryManager.getDBEntry(path, opt, dump, waitForReady, includeDeleted, localOnly);
|
|
462
462
|
}
|
|
463
|
-
async getDBEntryFromMeta(meta, dump = false, waitForReady = true) {
|
|
464
|
-
return await this.managers.entryManager.getDBEntryFromMeta(meta, dump, waitForReady);
|
|
463
|
+
async getDBEntryFromMeta(meta, dump = false, waitForReady = true, localOnly = false) {
|
|
464
|
+
return await this.managers.entryManager.getDBEntryFromMeta(meta, dump, waitForReady, localOnly);
|
|
465
465
|
}
|
|
466
466
|
async deleteDBEntry(path, opt) {
|
|
467
467
|
return await this.managers.entryManager.deleteDBEntry(path, opt);
|
|
@@ -475,6 +475,10 @@ class LiveSyncLocalDB {
|
|
|
475
475
|
async putDBEntryWithLiveBaseRevision(note, baseRevision, onlyChunks) {
|
|
476
476
|
return await this.managers.entryManager.putDBEntryWithLiveBaseRevision(note, baseRevision, onlyChunks);
|
|
477
477
|
}
|
|
478
|
+
/** Preserve existing branches and delegate a fresh root write to the entry manager. */
|
|
479
|
+
async putDBEntryAsIndependentRoot(note) {
|
|
480
|
+
return await this.managers.entryManager.putDBEntryAsIndependentRoot(note);
|
|
481
|
+
}
|
|
478
482
|
async getConflictedDoc(path, rev) {
|
|
479
483
|
return await this.managers.conflictManager.getConflictedDoc(path, rev);
|
|
480
484
|
}
|
|
@@ -26,21 +26,45 @@ export declare class ServiceDatabaseFileAccessBase extends ServiceModuleBase<Ser
|
|
|
26
26
|
delete(file: UXFileInfoStub | FilePathWithPrefix, rev?: string): Promise<boolean>;
|
|
27
27
|
createChunks(file: UXFileInfo, force?: boolean, skipCheck?: boolean): Promise<boolean>;
|
|
28
28
|
store(file: UXFileInfo, force?: boolean, skipCheck?: boolean): Promise<boolean>;
|
|
29
|
+
/** Force a child below the supplied base, even if advanced; an undefined base uses the winner. */
|
|
29
30
|
storeWithBaseRevision(file: UXFileInfo, baseRevision: string | undefined, skipCheck?: boolean): Promise<string | false>;
|
|
31
|
+
/** Extend a current leaf with ordinary MVCC; an advanced base returns `false`. */
|
|
30
32
|
storeWithLiveBaseRevision(file: UXFileInfo, baseRevision: string, skipCheck?: boolean): Promise<string | false>;
|
|
33
|
+
/**
|
|
34
|
+
* Store Chunks and a fresh Metadata root for content with unknown ancestry.
|
|
35
|
+
* Return its exact revision for provenance, or `false` if no Metadata write succeeded.
|
|
36
|
+
*/
|
|
37
|
+
storeIndependentRevision(file: UXFileInfo, skipCheck?: boolean): Promise<string | false>;
|
|
31
38
|
storeAsConflictedRevision(file: UXFileInfo, currentRev: string, skipCheck?: boolean): Promise<boolean>;
|
|
39
|
+
/** Create a sibling of the selected revision; refuse when its parent cannot be found. */
|
|
32
40
|
storeAsConflictedRevisionWithResult(file: UXFileInfo, currentRev: string, skipCheck?: boolean): Promise<string | false>;
|
|
41
|
+
/** Record a logical deletion below the displayed branch and return its exact revision. */
|
|
33
42
|
storeDeletionWithBaseRevision(file: UXFileInfoStub | FilePathWithPrefix, baseRevision: string): Promise<string | false>;
|
|
34
43
|
storeContent(path: FilePathWithPrefix, content: string): Promise<boolean>;
|
|
35
44
|
private __store;
|
|
45
|
+
/** Reconstruct the immediate parent ID from revision history, or return `false` if unavailable. */
|
|
36
46
|
private getParentRev;
|
|
47
|
+
/**
|
|
48
|
+
* Search available revision bodies across branches and their ancestors.
|
|
49
|
+
* `stopAfterFirstMatch` serves existence checks without decoding the remaining candidates.
|
|
50
|
+
* Historical matches alone do not establish the origin of current storage content.
|
|
51
|
+
*/
|
|
37
52
|
private findContentRevisionsInternal;
|
|
53
|
+
/** Find matching content throughout available history, including non-leaf revisions. */
|
|
38
54
|
findContentRevisions(file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer, currentRev?: string): Promise<string[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Find live leaves which already represent these bytes, for deduplication when provenance
|
|
57
|
+
* is unknown. `open_revs` includes conflicting branches; both deletion markers are excluded.
|
|
58
|
+
* Unlike the historical search, ancestors cannot match and missing Chunks are not fetched.
|
|
59
|
+
* A read failure yields no confirmed match. Callers decide whether the result identifies a
|
|
60
|
+
* unique origin; this method does not mutate provenance or hold the revision tree stable.
|
|
61
|
+
*/
|
|
62
|
+
findLiveContentRevisions(file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer): Promise<string[]>;
|
|
39
63
|
hasContentInRevisionHistory(file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer, currentRev?: string): Promise<boolean>;
|
|
40
64
|
getConflictedRevs(file: UXFileInfoStub | FilePathWithPrefix): Promise<string[]>;
|
|
41
65
|
fetch(file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean): Promise<UXFileInfo | false>;
|
|
42
66
|
fetchEntryMeta(file: UXFileInfoStub | FilePathWithPrefix, rev?: string, skipCheck?: boolean): Promise<MetaEntry | false>;
|
|
43
|
-
fetchEntryFromMeta(meta: MetaEntry, waitForReady?: boolean, skipCheck?: boolean): Promise<LoadedEntry | false>;
|
|
44
|
-
fetchEntry(file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean): Promise<LoadedEntry | false>;
|
|
67
|
+
fetchEntryFromMeta(meta: MetaEntry, waitForReady?: boolean, skipCheck?: boolean, localOnly?: boolean): Promise<LoadedEntry | false>;
|
|
68
|
+
fetchEntry(file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean, localOnly?: boolean): Promise<LoadedEntry | false>;
|
|
45
69
|
deleteFromDBbyPath(fullPath: FilePath | FilePathWithPrefix, rev?: string): Promise<boolean>;
|
|
46
70
|
}
|
|
@@ -52,6 +52,7 @@ class ServiceDatabaseFileAccessBase extends ServiceModuleBase {
|
|
|
52
52
|
async store(file, force = false, skipCheck) {
|
|
53
53
|
return await this.__store(file, force, skipCheck, false) !== false;
|
|
54
54
|
}
|
|
55
|
+
/** Force a child below the supplied base, even if advanced; an undefined base uses the winner. */
|
|
55
56
|
async storeWithBaseRevision(file, baseRevision, skipCheck) {
|
|
56
57
|
const result = await this.__store(file, true, skipCheck, false, {
|
|
57
58
|
mode: "force-base",
|
|
@@ -59,6 +60,7 @@ class ServiceDatabaseFileAccessBase extends ServiceModuleBase {
|
|
|
59
60
|
});
|
|
60
61
|
return result !== null && typeof result === "object" && "rev" in result ? result.rev : false;
|
|
61
62
|
}
|
|
63
|
+
/** Extend a current leaf with ordinary MVCC; an advanced base returns `false`. */
|
|
62
64
|
async storeWithLiveBaseRevision(file, baseRevision, skipCheck) {
|
|
63
65
|
const result = await this.__store(file, true, skipCheck, false, {
|
|
64
66
|
mode: "live-base",
|
|
@@ -66,9 +68,18 @@ class ServiceDatabaseFileAccessBase extends ServiceModuleBase {
|
|
|
66
68
|
});
|
|
67
69
|
return result !== null && typeof result === "object" && "rev" in result ? result.rev : false;
|
|
68
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Store Chunks and a fresh Metadata root for content with unknown ancestry.
|
|
73
|
+
* Return its exact revision for provenance, or `false` if no Metadata write succeeded.
|
|
74
|
+
*/
|
|
75
|
+
async storeIndependentRevision(file, skipCheck) {
|
|
76
|
+
const result = await this.__store(file, true, skipCheck, false, { mode: "independent-root" });
|
|
77
|
+
return result !== null && typeof result === "object" && "rev" in result ? result.rev : false;
|
|
78
|
+
}
|
|
69
79
|
async storeAsConflictedRevision(file, currentRev, skipCheck) {
|
|
70
80
|
return await this.storeAsConflictedRevisionWithResult(file, currentRev, skipCheck) !== false;
|
|
71
81
|
}
|
|
82
|
+
/** Create a sibling of the selected revision; refuse when its parent cannot be found. */
|
|
72
83
|
async storeAsConflictedRevisionWithResult(file, currentRev, skipCheck) {
|
|
73
84
|
const conflictBaseRev = await this.getParentRev(file, currentRev);
|
|
74
85
|
if (!conflictBaseRev) {
|
|
@@ -77,6 +88,7 @@ class ServiceDatabaseFileAccessBase extends ServiceModuleBase {
|
|
|
77
88
|
}
|
|
78
89
|
return await this.storeWithBaseRevision(file, conflictBaseRev, skipCheck);
|
|
79
90
|
}
|
|
91
|
+
/** Record a logical deletion below the displayed branch and return its exact revision. */
|
|
80
92
|
async storeDeletionWithBaseRevision(file, baseRevision) {
|
|
81
93
|
if (!await this.checkIsTargetFile(file)) {
|
|
82
94
|
return false;
|
|
@@ -193,7 +205,7 @@ class ServiceDatabaseFileAccessBase extends ServiceModuleBase {
|
|
|
193
205
|
this._log(msg + " Skip " + fullPath, LOG_LEVEL_VERBOSE);
|
|
194
206
|
return true;
|
|
195
207
|
}
|
|
196
|
-
const ret = revisionTarget.mode === "live-base" ? await this.database.localDatabase.putDBEntryWithLiveBaseRevision(
|
|
208
|
+
const ret = revisionTarget.mode === "independent-root" ? await this.database.localDatabase.putDBEntryAsIndependentRoot(d) : revisionTarget.mode === "live-base" ? await this.database.localDatabase.putDBEntryWithLiveBaseRevision(
|
|
197
209
|
d,
|
|
198
210
|
revisionTarget.baseRevision,
|
|
199
211
|
onlyChunks
|
|
@@ -208,6 +220,7 @@ class ServiceDatabaseFileAccessBase extends ServiceModuleBase {
|
|
|
208
220
|
}
|
|
209
221
|
return ret;
|
|
210
222
|
}
|
|
223
|
+
/** Reconstruct the immediate parent ID from revision history, or return `false` if unavailable. */
|
|
211
224
|
async getParentRev(file, rev) {
|
|
212
225
|
const filename = getDatabasePathFromUXFileInfo(file);
|
|
213
226
|
try {
|
|
@@ -226,6 +239,11 @@ class ServiceDatabaseFileAccessBase extends ServiceModuleBase {
|
|
|
226
239
|
return false;
|
|
227
240
|
}
|
|
228
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Search available revision bodies across branches and their ancestors.
|
|
244
|
+
* `stopAfterFirstMatch` serves existence checks without decoding the remaining candidates.
|
|
245
|
+
* Historical matches alone do not establish the origin of current storage content.
|
|
246
|
+
*/
|
|
229
247
|
async findContentRevisionsInternal(file, content, currentRev, stopAfterFirstMatch = false) {
|
|
230
248
|
const filename = getDatabasePathFromUXFileInfo(file);
|
|
231
249
|
try {
|
|
@@ -285,12 +303,46 @@ class ServiceDatabaseFileAccessBase extends ServiceModuleBase {
|
|
|
285
303
|
}
|
|
286
304
|
return [];
|
|
287
305
|
}
|
|
306
|
+
/** Find matching content throughout available history, including non-leaf revisions. */
|
|
288
307
|
async findContentRevisions(file, content, currentRev) {
|
|
289
308
|
if (!await this.checkIsTargetFile(file)) {
|
|
290
309
|
return [];
|
|
291
310
|
}
|
|
292
311
|
return await this.findContentRevisionsInternal(file, content, currentRev);
|
|
293
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Find live leaves which already represent these bytes, for deduplication when provenance
|
|
315
|
+
* is unknown. `open_revs` includes conflicting branches; both deletion markers are excluded.
|
|
316
|
+
* Unlike the historical search, ancestors cannot match and missing Chunks are not fetched.
|
|
317
|
+
* A read failure yields no confirmed match. Callers decide whether the result identifies a
|
|
318
|
+
* unique origin; this method does not mutate provenance or hold the revision tree stable.
|
|
319
|
+
*/
|
|
320
|
+
async findLiveContentRevisions(file, content) {
|
|
321
|
+
if (!await this.checkIsTargetFile(file)) return [];
|
|
322
|
+
const filename = getDatabasePathFromUXFileInfo(file);
|
|
323
|
+
try {
|
|
324
|
+
const current = await this.database.localDatabase.getDBEntryMeta(filename, void 0, true);
|
|
325
|
+
if (current === false) return [];
|
|
326
|
+
const leaves = await this.database.localDatabase.getRaw(current._id, {
|
|
327
|
+
open_revs: "all"
|
|
328
|
+
});
|
|
329
|
+
if (!Array.isArray(leaves)) return [];
|
|
330
|
+
const matches = [];
|
|
331
|
+
for (const leaf of leaves) {
|
|
332
|
+
const rev = leaf.ok?._rev;
|
|
333
|
+
if (!rev || leaf.ok?.deleted || leaf.ok?._deleted) continue;
|
|
334
|
+
const entry = await this.database.localDatabase.getDBEntry(filename, { rev }, false, true, true, true);
|
|
335
|
+
if (entry !== false && !entry.deleted && !entry._deleted && await isDocContentSame(readContent(entry), content)) {
|
|
336
|
+
matches.push(rev);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return matches;
|
|
340
|
+
} catch (ex) {
|
|
341
|
+
this._log(`Could not check live revision content for ${filename}`, LOG_LEVEL_VERBOSE);
|
|
342
|
+
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
294
346
|
async hasContentInRevisionHistory(file, content, currentRev) {
|
|
295
347
|
if (!await this.checkIsTargetFile(file)) {
|
|
296
348
|
return true;
|
|
@@ -346,17 +398,17 @@ class ServiceDatabaseFileAccessBase extends ServiceModuleBase {
|
|
|
346
398
|
}
|
|
347
399
|
return doc;
|
|
348
400
|
}
|
|
349
|
-
async fetchEntryFromMeta(meta, waitForReady = true, skipCheck = false) {
|
|
401
|
+
async fetchEntryFromMeta(meta, waitForReady = true, skipCheck = false, localOnly = false) {
|
|
350
402
|
if (skipCheck && !await this.checkIsTargetFile(meta.path)) {
|
|
351
403
|
return false;
|
|
352
404
|
}
|
|
353
|
-
const doc = await this.database.localDatabase.getDBEntryFromMeta(meta, false, waitForReady);
|
|
405
|
+
const doc = await this.database.localDatabase.getDBEntryFromMeta(meta, false, waitForReady, localOnly);
|
|
354
406
|
if (doc === false) {
|
|
355
407
|
return false;
|
|
356
408
|
}
|
|
357
409
|
return doc;
|
|
358
410
|
}
|
|
359
|
-
async fetchEntry(file, rev, waitForReady = true, skipCheck = false) {
|
|
411
|
+
async fetchEntry(file, rev, waitForReady = true, skipCheck = false, localOnly = false) {
|
|
360
412
|
if (skipCheck && !await this.checkIsTargetFile(file)) {
|
|
361
413
|
return false;
|
|
362
414
|
}
|
|
@@ -364,7 +416,7 @@ class ServiceDatabaseFileAccessBase extends ServiceModuleBase {
|
|
|
364
416
|
if (entry === false) {
|
|
365
417
|
return false;
|
|
366
418
|
}
|
|
367
|
-
const doc = await this.fetchEntryFromMeta(entry, waitForReady, true);
|
|
419
|
+
const doc = await this.fetchEntryFromMeta(entry, waitForReady, true, localOnly);
|
|
368
420
|
return doc;
|
|
369
421
|
}
|
|
370
422
|
async deleteFromDBbyPath(fullPath, rev) {
|