@vrtmrz/livesync-commonlib 0.1.0-rc.12 → 0.1.0-rc.13
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/FileHandler.d.ts +17 -1
- package/dist/serviceFeatures/remoteConfig.d.ts +2 -2
- package/dist/serviceFeatures/remoteConfig.js +2 -2
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +2 -1
- package/dist/serviceModules/ServiceFileHandlerBase.js +100 -10
- package/docs/conflict-resolution.md +4 -0
- package/docs/development.md +1 -1
- package/docs/proven-in-use.md +1 -1
- package/package.json +1 -1
|
@@ -3,11 +3,27 @@ import type { UXFileInfo, UXFileInfoStub, UXInternalFileInfoStub } from "../comm
|
|
|
3
3
|
export interface IFileHandler {
|
|
4
4
|
readFileFromStub(file: UXFileInfoStub | UXFileInfo): Promise<UXFileInfo>;
|
|
5
5
|
storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean): Promise<boolean>;
|
|
6
|
+
/**
|
|
7
|
+
* Store the current storage content as a child of an exact live database revision.
|
|
8
|
+
*
|
|
9
|
+
* This explicit branch-selection boundary is intended for user-directed
|
|
10
|
+
* reconciliation. It rechecks that the selected revision is still live,
|
|
11
|
+
* and records the resulting revision as the storage provenance. When
|
|
12
|
+
* `createIfDifferent` is false, differing storage content is rejected
|
|
13
|
+
* instead of creating a child revision.
|
|
14
|
+
*/
|
|
15
|
+
storeFileToDBWithBaseRevision(info: UXFileInfoStub | UXFileInfo | FilePathWithPrefix, baseRevision: string, createIfDifferent?: boolean): Promise<boolean>;
|
|
6
16
|
deleteFileFromDB(info: UXFileInfoStub | UXInternalFileInfoStub | FilePath): Promise<boolean>;
|
|
7
17
|
renameFileInDB(info: UXFileInfoStub | UXFileInfo, oldPath: FilePath | FilePathWithPrefix): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Create a logical deletion on one exact database revision.
|
|
20
|
+
*
|
|
21
|
+
* When device-local storage provenance names the deleted revision, the
|
|
22
|
+
* implementation clears that stale provenance without changing storage.
|
|
23
|
+
*/
|
|
8
24
|
deleteRevisionFromDB(info: UXFileInfoStub | FilePath | FilePathWithPrefix, rev: string): Promise<boolean | undefined>;
|
|
9
25
|
resolveConflictedByDeletingRevision(info: UXFileInfoStub | FilePath, rev: string): Promise<boolean | undefined>;
|
|
10
|
-
dbToStorageWithSpecificRev(info: UXFileInfoStub | UXFileInfo | FilePath | null, rev: string, force?: boolean): Promise<boolean>;
|
|
26
|
+
dbToStorageWithSpecificRev(info: UXFileInfoStub | UXFileInfo | FilePath | FilePathWithPrefix | null, rev: string, force?: boolean): Promise<boolean>;
|
|
11
27
|
dbToStorage(entryInfo: MetaEntry | FilePathWithPrefix, info: UXFileInfoStub | UXFileInfo | FilePath | null, force?: boolean): Promise<boolean>;
|
|
12
28
|
createAllChunks(showingNotice?: boolean): Promise<void>;
|
|
13
29
|
}
|
|
@@ -65,11 +65,11 @@ export declare function activateRemoteConfiguration(settings: ObsidianLiveSyncSe
|
|
|
65
65
|
*/
|
|
66
66
|
export declare function activateP2PRemoteConfiguration(settings: ObsidianLiveSyncSettings, id: string): ObsidianLiveSyncSettings | false;
|
|
67
67
|
/**
|
|
68
|
-
* Command: Switch
|
|
68
|
+
* Command: Switch active connection
|
|
69
69
|
*/
|
|
70
70
|
export declare function commandSwitchActiveRemote(host: RemoteConfigHost): Promise<void>;
|
|
71
71
|
/**
|
|
72
|
-
* Command:
|
|
72
|
+
* Command: Sync with a saved connection
|
|
73
73
|
*/
|
|
74
74
|
export declare function commandReplicateWithSpecificRemote(host: RemoteConfigHost): Promise<void>;
|
|
75
75
|
/**
|
|
@@ -311,12 +311,12 @@ function useRemoteConfigurationMigration(host) {
|
|
|
311
311
|
function useRemoteConfiguration(host) {
|
|
312
312
|
host.services.API.addCommand({
|
|
313
313
|
id: "livesync-switch-remote",
|
|
314
|
-
name: "Switch
|
|
314
|
+
name: "Switch active connection",
|
|
315
315
|
callback: () => commandSwitchActiveRemote(host)
|
|
316
316
|
});
|
|
317
317
|
host.services.API.addCommand({
|
|
318
318
|
id: "livesync-replicate-with-specific",
|
|
319
|
-
name: "
|
|
319
|
+
name: "Sync with a saved connection",
|
|
320
320
|
callback: () => commandReplicateWithSpecificRemote(host)
|
|
321
321
|
});
|
|
322
322
|
return true;
|
|
@@ -57,12 +57,13 @@ export declare abstract class ServiceFileHandlerBase extends ServiceModuleBase<S
|
|
|
57
57
|
readFileFromStub(file: UXFileInfoStub | UXFileInfo): Promise<UXFileInfo>;
|
|
58
58
|
private infoToStub;
|
|
59
59
|
storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean): Promise<boolean>;
|
|
60
|
+
storeFileToDBWithBaseRevision(info: UXFileInfoStub | UXFileInfo | FilePathWithPrefix, baseRevision: string, createIfDifferent?: boolean): Promise<boolean>;
|
|
60
61
|
private storeFileToDBFromRevision;
|
|
61
62
|
deleteFileFromDB(info: UXFileInfoStub | UXInternalFileInfoStub | FilePath): Promise<boolean>;
|
|
62
63
|
renameFileInDB(info: UXFileInfoStub | UXFileInfo, oldPath: FilePath | FilePathWithPrefix): Promise<boolean>;
|
|
63
64
|
deleteRevisionFromDB(info: UXFileInfoStub | FilePath | FilePathWithPrefix, rev: string): Promise<boolean | undefined>;
|
|
64
65
|
resolveConflictedByDeletingRevision(info: UXFileInfoStub | FilePath, rev: string): Promise<boolean | undefined>;
|
|
65
|
-
dbToStorageWithSpecificRev(info: UXFileInfoStub | UXFileInfo | FilePath | null, rev: string, force?: boolean): Promise<boolean>;
|
|
66
|
+
dbToStorageWithSpecificRev(info: UXFileInfoStub | UXFileInfo | FilePath | FilePathWithPrefix | null, rev: string, force?: boolean): Promise<boolean>;
|
|
66
67
|
dbToStorage(entryInfo: MetaEntry | FilePathWithPrefix, info: UXFileInfoStub | UXFileInfo | FilePath | null, force?: boolean): Promise<boolean>;
|
|
67
68
|
private applyDatabaseEntryToStorage;
|
|
68
69
|
private preserveUnsyncedStorageAsConflict;
|
|
@@ -149,6 +149,59 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
149
149
|
async storeFileToDB(info, force = false, onlyChunks = false) {
|
|
150
150
|
return await this.storeFileToDBFromRevision(info, force, onlyChunks);
|
|
151
151
|
}
|
|
152
|
+
async storeFileToDBWithBaseRevision(info, baseRevision, createIfDifferent = true) {
|
|
153
|
+
const file = await this.infoToStub(info);
|
|
154
|
+
if (file == null) {
|
|
155
|
+
this._log(`File ${tryGetFilePath(info)} is not exist on the storage`, LOG_LEVEL_VERBOSE);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (file.isInternal) {
|
|
159
|
+
this._log(
|
|
160
|
+
`Internal file ${file.path} is not allowed to be stored through the ordinary file handler`,
|
|
161
|
+
LOG_LEVEL_VERBOSE
|
|
162
|
+
);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
const [baseEntry, currentEntry, conflictedRevisions] = await Promise.all([
|
|
166
|
+
this.db.fetchEntryMeta(file, baseRevision, true),
|
|
167
|
+
this.db.fetchEntryMeta(file, void 0, true),
|
|
168
|
+
this.db.getConflictedRevs(file)
|
|
169
|
+
]);
|
|
170
|
+
const liveRevisions = /* @__PURE__ */ new Set([
|
|
171
|
+
...currentEntry && currentEntry._rev ? [currentEntry._rev] : [],
|
|
172
|
+
...conflictedRevisions
|
|
173
|
+
]);
|
|
174
|
+
if (!baseEntry || baseEntry._rev !== baseRevision || !liveRevisions.has(baseRevision)) {
|
|
175
|
+
this._log(
|
|
176
|
+
`Could not store ${file.path} on revision ${baseRevision}; the selected revision is no longer live`,
|
|
177
|
+
LOG_LEVEL_NOTICE
|
|
178
|
+
);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const readFile = await this.readFileFromStub(file);
|
|
182
|
+
if (!baseEntry.deleted && !baseEntry._deleted) {
|
|
183
|
+
const loadedBase = await this.db.fetchEntry(file, baseRevision, true, true);
|
|
184
|
+
if (loadedBase && await isDocContentSame(getDocDataAsArray(loadedBase.data), readFile.body)) {
|
|
185
|
+
await this.setProvenance(file.path, baseRevision, readFile.stat.mtime);
|
|
186
|
+
await this.conflict.queueCheckFor(file.path);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!createIfDifferent) {
|
|
191
|
+
this._log(
|
|
192
|
+
`Could not mark ${file.path} as revision ${baseRevision}; the storage content differs`,
|
|
193
|
+
LOG_LEVEL_NOTICE
|
|
194
|
+
);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
const storedRevision = await this.db.storeWithBaseRevision(readFile, baseRevision, true);
|
|
198
|
+
if (storedRevision === false) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
await this.setProvenance(file.path, storedRevision, readFile.stat.mtime);
|
|
202
|
+
await this.conflict.queueCheckFor(file.path);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
152
205
|
async storeFileToDBFromRevision(info, force = false, onlyChunks = false, preferredBasePath) {
|
|
153
206
|
const file = await this.infoToStub(info);
|
|
154
207
|
if (file == null) {
|
|
@@ -381,31 +434,60 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
381
434
|
return deleted;
|
|
382
435
|
}
|
|
383
436
|
async deleteRevisionFromDB(info, rev) {
|
|
384
|
-
|
|
437
|
+
const path = getStoragePathFromUXFileInfo(info);
|
|
438
|
+
const provenance = await this.getProvenance(path);
|
|
439
|
+
const deleted = await this.db.delete(info, rev);
|
|
440
|
+
if (deleted && provenance?.revision === rev) {
|
|
441
|
+
await this.deleteProvenance(path);
|
|
442
|
+
}
|
|
443
|
+
return deleted;
|
|
385
444
|
}
|
|
386
445
|
async resolveConflictedByDeletingRevision(info, rev) {
|
|
387
446
|
const path = getStoragePathFromUXFileInfo(info);
|
|
447
|
+
const file = await this.infoToStub(info);
|
|
448
|
+
const docEntry = await this.db.fetchEntryMeta(file ?? info, rev, true);
|
|
449
|
+
if (!docEntry) {
|
|
450
|
+
this._log(`Failed to read the conflicted revision ${rev} of ${path}`, LOG_LEVEL_VERBOSE);
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
388
453
|
if (!await this.deleteRevisionFromDB(info, rev)) {
|
|
389
454
|
this._log(`Failed to delete the conflicted revision ${rev} of ${path}`, LOG_LEVEL_VERBOSE);
|
|
390
455
|
return false;
|
|
391
456
|
}
|
|
392
|
-
if (!await this.
|
|
457
|
+
if (!await this.applyDatabaseEntryToStorage(docEntry, file, true)) {
|
|
393
458
|
this._log(`Failed to apply the resolved revision ${rev} of ${path} to the storage`, LOG_LEVEL_VERBOSE);
|
|
394
459
|
return false;
|
|
395
460
|
}
|
|
461
|
+
await this.deleteProvenance(path);
|
|
396
462
|
}
|
|
397
463
|
async dbToStorageWithSpecificRev(info, rev, force) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
this._log(`File ${tryGetFilePath(info)} is not exist on the storage`, LOG_LEVEL_VERBOSE);
|
|
464
|
+
if (info == null) {
|
|
465
|
+
this._log(`Cannot select database revision ${rev} without a file path`, LOG_LEVEL_VERBOSE);
|
|
401
466
|
return false;
|
|
402
467
|
}
|
|
403
|
-
const
|
|
468
|
+
const file = await this.infoToStub(info);
|
|
469
|
+
const databaseTarget = file ?? info;
|
|
470
|
+
const [docEntry, currentEntry, conflictedRevisions] = await Promise.all([
|
|
471
|
+
this.db.fetchEntryMeta(databaseTarget, rev, true),
|
|
472
|
+
this.db.fetchEntryMeta(databaseTarget, void 0, true),
|
|
473
|
+
this.db.getConflictedRevs(databaseTarget)
|
|
474
|
+
]);
|
|
404
475
|
if (!docEntry) {
|
|
405
|
-
this._log(`File ${
|
|
476
|
+
this._log(`File ${tryGetFilePath(info)} is not exist on the database`, LOG_LEVEL_VERBOSE);
|
|
406
477
|
return false;
|
|
407
478
|
}
|
|
408
|
-
|
|
479
|
+
const liveRevisions = /* @__PURE__ */ new Set([
|
|
480
|
+
...currentEntry && currentEntry._rev ? [currentEntry._rev] : [],
|
|
481
|
+
...conflictedRevisions
|
|
482
|
+
]);
|
|
483
|
+
if (!liveRevisions.has(rev)) {
|
|
484
|
+
this._log(
|
|
485
|
+
`Could not apply ${tryGetFilePath(info)} revision ${rev}; the selected revision is no longer live`,
|
|
486
|
+
LOG_LEVEL_NOTICE
|
|
487
|
+
);
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
return await this.applyDatabaseEntryToStorage(docEntry, file, force, true);
|
|
409
491
|
}
|
|
410
492
|
async dbToStorage(entryInfo, info, force) {
|
|
411
493
|
const file = await this.infoToStub(info);
|
|
@@ -417,12 +499,12 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
417
499
|
}
|
|
418
500
|
return await this.applyDatabaseEntryToStorage(docEntry, file, force);
|
|
419
501
|
}
|
|
420
|
-
async applyDatabaseEntryToStorage(docEntry, file, force) {
|
|
502
|
+
async applyDatabaseEntryToStorage(docEntry, file, force, allowExistingConflicts = false) {
|
|
421
503
|
const mode = file == null ? "create" : "modify";
|
|
422
504
|
const path = this.getPath(docEntry);
|
|
423
505
|
const settings = this.setting.currentSettings();
|
|
424
506
|
const revs = await this.db.getConflictedRevs(path);
|
|
425
|
-
if (revs.length > 0) {
|
|
507
|
+
if (revs.length > 0 && !allowExistingConflicts) {
|
|
426
508
|
if (settings.writeDocumentsIfConflicted) {
|
|
427
509
|
} else {
|
|
428
510
|
await this.conflict.queueCheckForIfOpen(path);
|
|
@@ -490,6 +572,14 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
490
572
|
}
|
|
491
573
|
}
|
|
492
574
|
const docData = readContent(docRead);
|
|
575
|
+
if (allowExistingConflicts && existDoc && !force) {
|
|
576
|
+
const readFile = await this.readFileFromStub(existDoc);
|
|
577
|
+
if (await isDocContentSame(docData, readFile.body)) {
|
|
578
|
+
await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime);
|
|
579
|
+
this.path.markChangesAreSame(docRead, docRead.mtime, existDoc.stat.mtime);
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
493
583
|
if (existDoc && !force) {
|
|
494
584
|
let shouldApplied = false;
|
|
495
585
|
const freshness = this.path.compareFileFreshness(existDoc, docEntry);
|
|
@@ -76,6 +76,10 @@ path -> { revision, observedStorageMtime? }
|
|
|
76
76
|
|
|
77
77
|
The record is updated only after a successful database-to-storage reflection or storage-to-database write. A plain read does not change provenance. A record remains authoritative when a user edits the displayed file to bytes which happen to equal another branch; content equality must not silently change the branch being extended.
|
|
78
78
|
|
|
79
|
+
User-directed reconciliation may select any exact current live revision, including a conflict leaf rather than the deterministic winner. `dbToStorageWithSpecificRev()` rechecks that the selected revision is still live before reflecting its content. `storeFileToDBWithBaseRevision()` performs the same live check before storing current storage content as a child of that revision. If the content already matches the selected non-deleted revision, it records that revision directly without creating a child. A host can set `createIfDifferent` to `false` when it intends only to record an exact content match; differing content then fails instead of changing the revision tree. A host may also create a logical deletion on an explicitly selected branch with `deleteRevisionFromDB()` after performing its own current-live-revision check. This leaves storage unchanged and clears device-local provenance only when that record names the deleted revision.
|
|
80
|
+
|
|
81
|
+
A successful non-deleted reconciliation records the exact selected or newly created revision as provenance. Applying a logical deletion removes the storage item and its provenance record. Deletion provenance is not retained indefinitely: an absent storage item and a current logical-deletion winner already agree and need no further reconciliation.
|
|
82
|
+
|
|
79
83
|
Hosts may construct the namespaced store handle during service composition, before its backing database is open. Store operations begin only after the host's storage lifecycle is ready. They fail rather than wait when that lifecycle contract is violated, because an implicit readiness wait could hang after failed initialisation or wait on its own initialisation handler. Database reset is a transient unavailable boundary; the host must avoid file processing during it and reconstruct derived provenance after reopening.
|
|
80
84
|
|
|
81
85
|
When a record is absent, the implementation reconstructs it only if the current storage bytes match exactly one available revision body. No match, or more than one match, leaves the branch identity unknown.
|
package/docs/development.md
CHANGED
|
@@ -57,7 +57,7 @@ The settings lifecycle has an additional focused unit table in `src/common/model
|
|
|
57
57
|
|
|
58
58
|
The multiple-remote profile contract has focused unit coverage in `src/serviceFeatures/remoteConfig.unit.spec.ts`. Extend it whenever profile identity, generated display names, activation, P2P selection, URI serialisation, or legacy migration changes. New hosts should import the reviewed primitives through `/remote-configurations`; `compat/serviceFeatures/remoteConfig` remains the wider migration surface used by existing clients.
|
|
59
59
|
|
|
60
|
-
The revision-tree safety rules are documented in [Conflict resolution and file provenance](conflict-resolution.md). `ConflictManager.unit.spec.ts` owns common-ancestor and conservative merge behaviour, deterministic pair selection for multiple live leaves, and reconstruction from the committed tree between stages. `ServiceDatabaseFileAccessBase.unit.spec.ts` owns all-branch content discovery and exact-revision logical deletion. `ServiceFileHandlerBase.unit.spec.ts` owns the injected provenance policy for edit, deletion, and rename operations. `LiveSyncLocalDB.unit.spec.ts` owns chunk reachability across current winners, shared chunks, live conflicts, available merge ancestry, conflict resolution, and PouchDB replication of collection and recreation. Extend the relevant sides when changing conflict resolution, compaction handling, provenance, chunk collection, or database-to-storage overwrite protection.
|
|
60
|
+
The revision-tree safety rules are documented in [Conflict resolution and file provenance](conflict-resolution.md). `ConflictManager.unit.spec.ts` owns common-ancestor and conservative merge behaviour, deterministic pair selection for multiple live leaves, and reconstruction from the committed tree between stages. `ServiceDatabaseFileAccessBase.unit.spec.ts` owns all-branch content discovery and exact-revision logical deletion. `ServiceFileHandlerBase.unit.spec.ts` owns the injected provenance policy for edit, deletion, and rename operations, together with user-selected reflection, child creation, strict matching, matching-provenance cleanup after exact branch deletion, and stale-revision refusal. `LiveSyncLocalDB.unit.spec.ts` owns chunk reachability across current winners, shared chunks, live conflicts, available merge ancestry, conflict resolution, and PouchDB replication of collection and recreation. Extend the relevant sides when changing conflict resolution, compaction handling, provenance, chunk collection, or database-to-storage overwrite protection.
|
|
61
61
|
|
|
62
62
|
`openSimpleStore()` may create an inert namespace handle during composition, before the backing key-value database has opened. Maintained hosts must complete their sequential settings lifecycle before scans, watchers, or replication invoke the handle. Operations fail on a lifecycle violation rather than waiting for readiness; do not introduce an implicit wait which can hang after failed initialisation or become self-referential from an initialisation handler. Treat database reset as a transient unavailable boundary and reconstruct derived local state after reopening.
|
|
63
63
|
|
package/docs/proven-in-use.md
CHANGED
|
@@ -12,7 +12,7 @@ The plug-in extends the neutral `ServiceContext` with Obsidian-owned capabilitie
|
|
|
12
12
|
|
|
13
13
|
The consumer contract in `test/contracts/serviceContext.ts` checks shared result semantics and exact context identity across the composed services. The real-Obsidian suite documented in `test/e2e-obsidian/README.md` then covers plug-in loading, representative Svelte dialogue mounts, Vault reflection, CouchDB and Object Storage synchronisation, a two-device P2P Setup URI round-trip, two-Vault behaviour, and other Obsidian-owned boundaries.
|
|
14
14
|
|
|
15
|
-
Commonlib's real-PouchDB and injected-boundary unit tests prove that conservative merge uses the nearest revision actually shared by both conflict leaves, multiple remaining leaves are selected in a deterministic order, each completed pair is reconstructed from the committed tree, storage content from a deleted losing branch remains recognisable after resolution, and conflict-time operations retain the exact displayed branch or preserve every branch when that identity cannot be proved. They also verify that a local edit remains a readable sibling branch when the winning body is unavailable but its exact revision and parent metadata remain, that a generation-one revision without a parent is left unresolved, and that a separately confirmed exact deletion of that unreadable live leaf permits a replacement to be stored without reading the missing chunk. Self-hosted LiveSync supplies the host composition
|
|
15
|
+
Commonlib's real-PouchDB and injected-boundary unit tests prove that conservative merge uses the nearest revision actually shared by both conflict leaves, multiple remaining leaves are selected in a deterministic order, each completed pair is reconstructed from the committed tree, storage content from a deleted losing branch remains recognisable after resolution, and conflict-time operations retain the exact displayed branch or preserve every branch when that identity cannot be proved. They also verify that a local edit remains a readable sibling branch when the winning body is unavailable but its exact revision and parent metadata remain, that a generation-one revision without a parent is left unresolved, and that a separately confirmed exact deletion of that unreadable live leaf permits a replacement to be stored without reading the missing chunk. Exact reconciliation tests cover reflecting a selected live leaf, storing Vault content as its child, recording a matching leaf without creating a revision, clearing matching provenance after exact branch deletion, and refusing to record a stale revision or a revision whose content differs. Self-hosted LiveSync supplies the host composition, persistent device-local provenance, and the per-revision repair interface. Its optional two-Vault scopes cover post-resolution propagation and edit, logical deletion, case-only rename, and cross-path rename while conflicts remain active. The conflict-operation scope passes in real Obsidian against the exact locally packed Commonlib artefact, including replicated parent revisions and preservation of the other live branches.
|
|
16
16
|
|
|
17
17
|
Commonlib's chunk-reachability tests use real PouchDB revision trees. They verify that an ordinary superseded revision releases its unique chunks, a chunk shared by current files remains reachable, live conflict branches and their nearest available shared ancestor remain protected, and losing-branch ancestry becomes eligible after resolution. They also replicate a collected chunk deletion to another PouchDB database and verify that writing the same content can recreate and replicate the content-addressed chunk. Self-hosted LiveSync owns the CouchDB-specific Garbage Collection V3 workflow, including device checks, confirmation, deletion, push-only replication, and remote compaction.
|
|
18
18
|
|
package/package.json
CHANGED