@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.
@@ -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
- /** Store a file as a child of an exact revision and return the created revision. */
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
- /** Preserve unknown storage content as a conflict and return its exact revision. */
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
- fetchEntry: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean) => Promise<LoadedEntry | false>;
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 database revision which most recently produced the storage state. */
4
+ /** Exact revision most recently saved from or reflected in storage; the base for subsequent edits. */
5
5
  revision: string;
6
- /** Raw modification time observed from this device's storage after reflection. */
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 waitForReady flag.
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 !== "latest") {
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) {