@vrtmrz/livesync-commonlib 0.1.25 → 0.1.27

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
  }
@@ -131,12 +131,16 @@ export type ReplicationFailed = {
131
131
  export type ReplicationOutcome = ReplicationCompleted | ReplicationCancelled | ReplicationBlocked | ReplicationPartial | ReplicationFailed;
132
132
  /** Preparation required before an ordinary operation may use a provider. */
133
133
  export interface ReplicationReadinessRequirements {
134
- readonly centralRemotePreparation: "required" | "not-applicable";
134
+ readonly centralRemotePreparation: "required" | "provider-owned" | "not-applicable";
135
135
  }
136
136
  /** Shared readiness contract for providers backed by the central remote. */
137
137
  export declare const CENTRAL_REMOTE_REPLICATION_READINESS: Readonly<{
138
138
  readonly centralRemotePreparation: "required";
139
139
  }>;
140
+ /** Central provider which prepares its Security Seed within each transfer. */
141
+ export declare const PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS: Readonly<{
142
+ readonly centralRemotePreparation: "provider-owned";
143
+ }>;
140
144
  /** Shared readiness contract for peer-to-peer providers without a central remote. */
141
145
  export declare const PEER_REPLICATION_READINESS: Readonly<{
142
146
  readonly centralRemotePreparation: "not-applicable";
@@ -25,6 +25,9 @@ const REPLICATION_CANCELLED = Object.freeze({ status: "cancelled" });
25
25
  const CENTRAL_REMOTE_REPLICATION_READINESS = Object.freeze({
26
26
  centralRemotePreparation: "required"
27
27
  });
28
+ const PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS = Object.freeze({
29
+ centralRemotePreparation: "provider-owned"
30
+ });
28
31
  const PEER_REPLICATION_READINESS = Object.freeze({
29
32
  centralRemotePreparation: "not-applicable"
30
33
  });
@@ -131,6 +134,7 @@ export {
131
134
  CENTRAL_REMOTE_REPLICATION_READINESS,
132
135
  NO_INTERACTION,
133
136
  PEER_REPLICATION_READINESS,
137
+ PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS,
134
138
  REPLICATION_CANCELLED,
135
139
  REPLICATION_COMPLETED,
136
140
  REPLICATION_PROGRESS_PRESENTATIONS,
@@ -32,4 +32,6 @@ export declare class SyncParamsNotFoundError extends SyncParamsHandlerError {
32
32
  }
33
33
  export declare class SyncParamsUpdateError extends SyncParamsHandlerError {
34
34
  }
35
+ /** Create a handler owned by one operation rather than the process-wide server cache. */
36
+ export declare function createSyncParamsHandler({ put, get, create }: CreateSyncParamsHanderOptions): SyncParamsHandler;
35
37
  export {};
@@ -114,5 +114,6 @@ export {
114
114
  SyncParamsNotFoundError,
115
115
  SyncParamsUpdateError,
116
116
  clearHandlers,
117
- createSyncParamsHanderForServer
117
+ createSyncParamsHanderForServer,
118
+ createSyncParamsHandler
118
119
  };
@@ -1,4 +1,4 @@
1
- export { CAPABILITY_NOT_APPLICABLE, CAPABILITY_NOT_IMPLEMENTED, CENTRAL_REMOTE_REPLICATION_READINESS, NO_INTERACTION, PEER_REPLICATION_READINESS, REPLICATION_CANCELLED, REPLICATION_COMPLETED, REPLICATION_PROGRESS_PRESENTATIONS, USER_INITIATED_REPLICATION_AUTHORITY, defineReplicatorProviderDefinitions, isReplicationCompleted, outcomeFromContinuousOpenReplication, outcomeFromFiniteOpenReplication, replicationBlocked, replicationFailed, supportedOpenReplicationContinuous, supportedOpenReplicationOneShot, supportedOpenReplicationUnattended, supportedStopActiveTransfer, supportedCapability, } from "./ReplicatorProvider.js";
1
+ export { CAPABILITY_NOT_APPLICABLE, CAPABILITY_NOT_IMPLEMENTED, CENTRAL_REMOTE_REPLICATION_READINESS, NO_INTERACTION, PEER_REPLICATION_READINESS, PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS, REPLICATION_CANCELLED, REPLICATION_COMPLETED, REPLICATION_PROGRESS_PRESENTATIONS, USER_INITIATED_REPLICATION_AUTHORITY, defineReplicatorProviderDefinitions, isReplicationCompleted, outcomeFromContinuousOpenReplication, outcomeFromFiniteOpenReplication, replicationBlocked, replicationFailed, supportedOpenReplicationContinuous, supportedOpenReplicationOneShot, supportedOpenReplicationUnattended, supportedStopActiveTransfer, supportedCapability, } from "./ReplicatorProvider.js";
2
2
  export type { ReplicatorInstance } from "./ReplicatorInstance.js";
3
3
  export type { ActiveReplicatorContext, CapabilitySupport, ContinuousReplicationRequest, ContinuousReplicationTrigger, ContinuousRunner, InteractionAuthority, InteractionPermissions, NotApplicableCapability, NotImplementedCapability, NoInteraction, PermittedInteractionPermissions, ReplicationBlockReason, ReplicationAttemptFailure, ReplicationCancelled, ReplicationCompleted, ReplicationFailed, ReplicationInteraction, ReplicationOutcome, ReplicationProgressPresentation, ReplicationReadinessRequirements, ReplicationPartial, ReplicationFailureRequest, ReplicatorConfigurationIdentity, ReplicatorProviderDefinition, ReplicatorProviderDefinitionMap, StopActiveTransferRunner, SupportedCapability, UnattendedOneShotRequest, UnattendedReplicationTrigger, UnattendedOneShotRunner, UserInitiatedOneShotRequest, UserInitiatedReplicationAuthority, UserInitiatedOneShotRunner, } from "./ReplicatorProvider.js";
4
4
  export { CAPABILITY_SUPPORT_KINDS, CAPABILITY_UNAVAILABLE_REASONS } from "./ProviderCapability.js";
@@ -4,6 +4,7 @@ import {
4
4
  CENTRAL_REMOTE_REPLICATION_READINESS,
5
5
  NO_INTERACTION,
6
6
  PEER_REPLICATION_READINESS,
7
+ PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS,
7
8
  REPLICATION_CANCELLED,
8
9
  REPLICATION_COMPLETED,
9
10
  REPLICATION_PROGRESS_PRESENTATIONS,
@@ -48,6 +49,7 @@ export {
48
49
  NO_INTERACTION,
49
50
  NO_REMOTE_RESOURCE_CAPABILITIES,
50
51
  PEER_REPLICATION_READINESS,
52
+ PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS,
51
53
  REMOTE_RESOURCE_KINDS,
52
54
  REPLICATION_CANCELLED,
53
55
  REPLICATION_COMPLETED,
@@ -16,6 +16,7 @@ export declare class JournalSyncCore {
16
16
  env: LiveSyncJournalReplicatorEnv;
17
17
  store: SimpleStore<CheckPointInfo>;
18
18
  requestedStop: boolean;
19
+ private syncParamsHandler?;
19
20
  getInitialSyncParameters(): Promise<SyncParameters>;
20
21
  getSyncParameters(): Promise<SyncParameters>;
21
22
  putSyncParameters(params: SyncParameters): Promise<boolean>;
@@ -44,6 +45,7 @@ export declare class JournalSyncCore {
44
45
  resetBucket(): Promise<boolean>;
45
46
  getRemoteKey(): string;
46
47
  getReplicationPBKDF2Salt(refresh?: boolean): Promise<Uint8Array<ArrayBuffer>>;
48
+ private getSyncParamsHandler;
47
49
  isEncryptionPrevented(fileName: string): boolean;
48
50
  private decryptDataV2;
49
51
  private decryptDataV1;
@@ -22,8 +22,7 @@ import {
22
22
  JournalStorageReadStatuses
23
23
  } from "./objectstore/JournalStorageAdapter.js";
24
24
  import {
25
- clearHandlers,
26
- createSyncParamsHanderForServer,
25
+ createSyncParamsHandler,
27
26
  SyncParamsFetchError,
28
27
  SyncParamsNotFoundError,
29
28
  SyncParamsUpdateError
@@ -61,6 +60,7 @@ class JournalSyncCore {
61
60
  env;
62
61
  store;
63
62
  requestedStop = false;
63
+ syncParamsHandler;
64
64
  getInitialSyncParameters() {
65
65
  return Promise.resolve({
66
66
  ...DEFAULT_SYNC_PARAMETERS,
@@ -109,7 +109,6 @@ class JournalSyncCore {
109
109
  this.store = store;
110
110
  this.hash = this.getHash(settings);
111
111
  this.storage = storage;
112
- clearHandlers();
113
112
  }
114
113
  async downloadJson(key) {
115
114
  try {
@@ -156,7 +155,7 @@ class JournalSyncCore {
156
155
  this.store = store;
157
156
  this.hash = this.getHash(settings);
158
157
  this.storage.applyNewConfig(settings);
159
- clearHandlers();
158
+ this.resetAllCaches();
160
159
  }
161
160
  updateInfo(info) {
162
161
  const old = this.env.services.replicator.replicationStatics.value;
@@ -205,23 +204,18 @@ class JournalSyncCore {
205
204
  return this._currentCheckPointInfo;
206
205
  }
207
206
  resetAllCaches() {
208
- clearHandlers();
207
+ this.syncParamsHandler = void 0;
209
208
  }
210
209
  async resetCheckpointInfo() {
211
210
  await this.updateCheckPointInfo((info) => ({ ...CheckPointInfoDefault }));
212
- clearHandlers();
213
211
  }
214
212
  getJournalEpochFromSyncParams(params) {
215
213
  return `${params.protocolVersion}:${params.pbkdf2salt}`;
216
214
  }
217
215
  async ensureCheckpointCachesAreFresh() {
218
- let journalEpoch = "";
219
- try {
220
- const params = await this.getSyncParameters();
221
- journalEpoch = this.getJournalEpochFromSyncParams(params);
222
- } catch {
223
- return;
224
- }
216
+ const params = await this.getSyncParamsHandler().fetch(true);
217
+ if (!params) throw new SyncParamsFetchError("Could not prepare Journal synchronisation parameters");
218
+ const journalEpoch = this.getJournalEpochFromSyncParams(params);
225
219
  const current = await this.getCheckpointInfo();
226
220
  if (current.journalEpoch === journalEpoch) {
227
221
  return;
@@ -252,12 +246,12 @@ class JournalSyncCore {
252
246
  receivedFiles: /* @__PURE__ */ new Set(),
253
247
  sentFiles: /* @__PURE__ */ new Set()
254
248
  }));
255
- clearHandlers();
256
249
  }
257
250
  async isAvailable() {
258
251
  return await this.storage.isAvailable();
259
252
  }
260
253
  async resetBucket() {
254
+ this.resetAllCaches();
261
255
  let files = [];
262
256
  try {
263
257
  do {
@@ -269,7 +263,6 @@ class JournalSyncCore {
269
263
  return false;
270
264
  }
271
265
  } while (files.length != 0);
272
- clearHandlers();
273
266
  } catch (ex) {
274
267
  Logger(`WARNING! Could not delete files.`, LOG_LEVEL_NOTICE, "reset-bucket");
275
268
  Logger(ex, LOG_LEVEL_VERBOSE);
@@ -291,13 +284,14 @@ class JournalSyncCore {
291
284
  return this.getHash(this._settings);
292
285
  }
293
286
  async getReplicationPBKDF2Salt(refresh) {
294
- const server = this.getRemoteKey();
295
- const manager = createSyncParamsHanderForServer(server, {
287
+ return await this.getSyncParamsHandler().getPBKDF2Salt(refresh);
288
+ }
289
+ getSyncParamsHandler() {
290
+ return this.syncParamsHandler ??= createSyncParamsHandler({
296
291
  put: (params) => this.putSyncParameters(params),
297
292
  get: () => this.getSyncParameters(),
298
293
  create: () => this.getInitialSyncParameters()
299
294
  });
300
- return await manager.getPBKDF2Salt(refresh);
301
295
  }
302
296
  isEncryptionPrevented(fileName) {
303
297
  if (fileName.endsWith(DOCID_JOURNAL_SYNC_PARAMETERS)) return true;
@@ -762,6 +756,7 @@ class JournalSyncCore {
762
756
  /** Stop this Journal operation owner and release its storage transport. */
763
757
  dispose() {
764
758
  this.requestStop();
759
+ this.resetAllCaches();
765
760
  this.storage.dispose?.();
766
761
  }
767
762
  }
@@ -12,7 +12,6 @@ import { MinioStorageAdapter } from "./objectstore/MinioStorageAdapter.js";
12
12
  import { LiveSyncAbstractReplicator } from "../LiveSyncAbstractReplicator.js";
13
13
  import { ensureRemoteIsCompatible } from "../../pouchdb/LiveSyncDBFunctions.js";
14
14
  import { extractObject } from "../../common/utils.js";
15
- import { clearHandlers } from "../SyncParamsHandler.js";
16
15
  import { JournalStorageReadStatuses } from "./objectstore/JournalStorageAdapter.js";
17
16
  import {
18
17
  CENTRAL_COMPATIBILITY_ACCEPTED,
@@ -154,20 +153,24 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
154
153
  async openReplication(setting, _, showResult, ignoreCleanLock = false, recordCompatibilityDecision) {
155
154
  return await this.runJournalTransfer(async (stopGeneration) => {
156
155
  const client = this.setupJournalSyncClient(setting);
157
- stopGeneration = this.journalTransferStopGeneration;
158
- if (!await this.checkReplicationConnectivity(
159
- false,
160
- ignoreCleanLock,
161
- showResult,
162
- setting,
163
- client,
164
- recordCompatibilityDecision
165
- )) {
166
- return false;
156
+ try {
157
+ stopGeneration = this.journalTransferStopGeneration;
158
+ if (!await this.checkReplicationConnectivity(
159
+ false,
160
+ ignoreCleanLock,
161
+ showResult,
162
+ setting,
163
+ client,
164
+ recordCompatibilityDecision
165
+ )) {
166
+ return false;
167
+ }
168
+ if (stopGeneration !== this.journalTransferStopGeneration) return false;
169
+ this.hasEnteredReplication = true;
170
+ return await client.sync(showResult);
171
+ } finally {
172
+ client.resetAllCaches();
167
173
  }
168
- if (stopGeneration !== this.journalTransferStopGeneration) return false;
169
- this.hasEnteredReplication = true;
170
- return await client.sync(showResult);
171
174
  });
172
175
  }
173
176
  /**
@@ -191,20 +194,24 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
191
194
  async runDirectionalReplication(setting, showingNotice, transfer, recordCompatibilityDecision) {
192
195
  return await this.runJournalTransfer(async (stopGeneration) => {
193
196
  const client = this.setupJournalSyncClient(setting);
194
- stopGeneration = this.journalTransferStopGeneration;
195
- if (!await this.checkReplicationConnectivity(
196
- false,
197
- false,
198
- !!showingNotice,
199
- setting,
200
- client,
201
- recordCompatibilityDecision
202
- )) {
203
- return false;
197
+ try {
198
+ stopGeneration = this.journalTransferStopGeneration;
199
+ if (!await this.checkReplicationConnectivity(
200
+ false,
201
+ false,
202
+ !!showingNotice,
203
+ setting,
204
+ client,
205
+ recordCompatibilityDecision
206
+ )) {
207
+ return false;
208
+ }
209
+ if (stopGeneration !== this.journalTransferStopGeneration) return false;
210
+ this.hasEnteredReplication = true;
211
+ return await transfer(client);
212
+ } finally {
213
+ client.resetAllCaches();
204
214
  }
205
- if (stopGeneration !== this.journalTransferStopGeneration) return false;
206
- this.hasEnteredReplication = true;
207
- return await transfer(client);
208
215
  });
209
216
  }
210
217
  /** Capture only the compatibility decision made by this exact borrowed client. */
@@ -344,7 +351,6 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
344
351
  if (!await this.client.resetBucket()) {
345
352
  throw new Error("Could not reset remote bucket");
346
353
  }
347
- clearHandlers();
348
354
  Logger("Remote Bucket Cleared", LOG_LEVEL_NOTICE);
349
355
  await this.tryCreateRemoteDatabase(setting);
350
356
  } catch (ex) {
@@ -356,7 +362,6 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
356
362
  async tryCreateRemoteDatabase(setting) {
357
363
  this.closeReplication();
358
364
  Logger("Remote Database Created or Connected", LOG_LEVEL_NOTICE);
359
- clearHandlers();
360
365
  if (!await this.ensurePBKDF2Salt(setting, true, false)) {
361
366
  throw new Error("Could not ensure PBKDF2 salt (Security Seed)");
362
367
  }