@vrtmrz/livesync-commonlib 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/API/processSetting.d.ts +1 -1
- package/dist/API/processSetting.js +10 -6
- package/dist/common/ConnectionString.js +15 -8
- package/dist/common/models/setting.const.qr.js +5 -0
- package/dist/common/models/setting.lifecycle.js +5 -3
- package/dist/common/models/setting.p2p.d.ts +10 -0
- package/dist/common/models/setting.p2p.js +13 -0
- package/dist/common/models/setting.type.d.ts +10 -0
- package/dist/common/models/tweak.definition.d.ts +5 -0
- package/dist/common/settingConstants.d.ts +5 -0
- package/dist/common/types.d.ts +3 -3
- package/dist/common/types.js +6 -0
- package/dist/common/utils.js +4 -1
- package/dist/interfaces/DatabaseFileAccess.d.ts +31 -3
- package/dist/interfaces/FileReflectionProvenance.d.ts +2 -2
- package/dist/managers/EntryManager/EntryManager.d.ts +4 -2
- package/dist/managers/EntryManager/EntryManager.js +9 -4
- package/dist/managers/EntryManager/EntryManagerImpls.d.ts +12 -4
- package/dist/managers/EntryManager/EntryManagerImpls.js +27 -9
- package/dist/p2p/P2PService.d.ts +7 -1
- package/dist/p2p/P2PService.js +13 -7
- package/dist/p2p/index.d.ts +3 -1
- package/dist/p2p/index.js +6 -0
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +4 -2
- package/dist/pouchdb/LiveSyncLocalDB.js +8 -4
- package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +22 -1
- package/dist/replication/trystero/P2PRoomSessionOwner.js +193 -13
- package/dist/replication/trystero/TrysteroReplicator.js +9 -3
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +1 -0
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +10 -3
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +6 -3
- package/dist/replication/trystero/useP2PReplicatorFeature.d.ts +6 -1
- package/dist/replication/trystero/useP2PReplicatorFeature.js +7 -4
- package/dist/rpc/transports/trysteroUtils.d.ts +2 -2
- package/dist/rpc/transports/trysteroUtils.js +32 -2
- package/dist/serviceFeatures/remoteConfig.js +3 -0
- package/dist/serviceModules/ServiceDatabaseFileAccessBase.d.ts +26 -2
- package/dist/serviceModules/ServiceDatabaseFileAccessBase.js +57 -5
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +119 -1
- package/dist/serviceModules/ServiceFileHandlerBase.js +450 -70
- package/dist/services/base/SettingService.d.ts +1 -0
- package/dist/services/base/SettingService.js +37 -9
- package/docs/conflict-resolution.md +28 -15
- package/docs/development.md +3 -3
- package/docs/p2p-transport-lifecycle.md +86 -4
- package/docs/proven-in-use.md +31 -1
- package/package.json +1 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { BaseRoomConfig } from "@trystero-p2p/nostr";
|
|
2
|
-
import type { P2PConnectionInfo } from "../../common/models/setting.type.js";
|
|
3
|
-
export declare function generateJoinRoomOptions(settings: P2PConnectionInfo): BaseRoomConfig;
|
|
2
|
+
import type { P2PConnectionInfo, P2PSyncSetting } from "../../common/models/setting.type.js";
|
|
3
|
+
export declare function generateJoinRoomOptions(settings: P2PConnectionInfo & Partial<Pick<P2PSyncSetting, "P2P_iceServers" | "P2P_iceServersExpiresAt">>): BaseRoomConfig;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { P2PConnectionPaths } from "../../common/models/setting.const.js";
|
|
2
2
|
import {
|
|
3
|
+
hasManagedP2PTurnConfiguration,
|
|
3
4
|
hasValidP2PTurnServerUrl,
|
|
4
5
|
normaliseP2PConnectionPath,
|
|
5
6
|
splitP2PRelayUrls,
|
|
@@ -8,11 +9,38 @@ import {
|
|
|
8
9
|
import { mixedHash } from "octagonal-wheels/hash/purejs";
|
|
9
10
|
import { compatGlobal } from "../../common/coreEnvFunctions.js";
|
|
10
11
|
import { createDiagRTCPeerConnectionConstructor } from "./DiagRTCPeerConnections.js";
|
|
12
|
+
function containsTurnUrl(iceServers) {
|
|
13
|
+
return iceServers.some((server) => {
|
|
14
|
+
const urls = typeof server.urls === "string" ? [server.urls] : server.urls;
|
|
15
|
+
return urls.some((url) => /^turns?:/iu.test(url));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function copyIceServers(iceServers) {
|
|
19
|
+
return iceServers.map((server) => ({
|
|
20
|
+
urls: typeof server.urls === "string" ? server.urls : [...server.urls],
|
|
21
|
+
...server.username === void 0 ? {} : { username: server.username },
|
|
22
|
+
...server.credential === void 0 ? {} : { credential: server.credential }
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
11
25
|
function generateJoinRoomOptions(settings) {
|
|
12
26
|
const passphraseNumbers = mixedHash(settings.P2P_passphrase, 0);
|
|
13
27
|
const passphrase = passphraseNumbers[0].toString(36) + passphraseNumbers[1].toString(36);
|
|
14
28
|
const relays = splitP2PRelayUrls(settings.P2P_relays);
|
|
15
29
|
const turnServers = splitP2PTurnServerUrls(settings.P2P_turnServers);
|
|
30
|
+
const managedTurnSelected = hasManagedP2PTurnConfiguration(settings);
|
|
31
|
+
const preparedIceServers = settings.P2P_iceServers;
|
|
32
|
+
if (preparedIceServers !== void 0 && preparedIceServers.length === 0) {
|
|
33
|
+
throw new Error("Prepared ICE servers must not be empty.");
|
|
34
|
+
}
|
|
35
|
+
if (managedTurnSelected && preparedIceServers === void 0) {
|
|
36
|
+
throw new Error("The selected managed TURN configuration requires prepared ICE servers.");
|
|
37
|
+
}
|
|
38
|
+
if (managedTurnSelected && preparedIceServers && !containsTurnUrl(preparedIceServers)) {
|
|
39
|
+
throw new Error("The prepared ICE servers do not contain a TURN route.");
|
|
40
|
+
}
|
|
41
|
+
if (preparedIceServers !== void 0 && normaliseP2PConnectionPath(settings.P2P_connectionPath) === P2PConnectionPaths.Relay && !containsTurnUrl(preparedIceServers)) {
|
|
42
|
+
throw new Error("Relay-only P2P requires a prepared TURN route.");
|
|
43
|
+
}
|
|
16
44
|
const relayConfig = {
|
|
17
45
|
manualReconnection: true,
|
|
18
46
|
urls: relays
|
|
@@ -28,7 +56,9 @@ function generateJoinRoomOptions(settings) {
|
|
|
28
56
|
} else if (typeof compatGlobal.RTCPeerConnection !== "undefined") {
|
|
29
57
|
options.rtcPolyfill = compatGlobal.RTCPeerConnection;
|
|
30
58
|
}
|
|
31
|
-
if (
|
|
59
|
+
if (preparedIceServers !== void 0) {
|
|
60
|
+
options.turnConfig = copyIceServers(preparedIceServers);
|
|
61
|
+
} else if (!managedTurnSelected && turnServers.length > 0) {
|
|
32
62
|
options.turnConfig = [
|
|
33
63
|
{
|
|
34
64
|
urls: turnServers,
|
|
@@ -37,7 +67,7 @@ function generateJoinRoomOptions(settings) {
|
|
|
37
67
|
}
|
|
38
68
|
];
|
|
39
69
|
}
|
|
40
|
-
if (normaliseP2PConnectionPath(settings.P2P_connectionPath) === P2PConnectionPaths.Relay && hasValidP2PTurnServerUrl(settings.P2P_turnServers)) {
|
|
70
|
+
if (normaliseP2PConnectionPath(settings.P2P_connectionPath) === P2PConnectionPaths.Relay && (preparedIceServers !== void 0 ? containsTurnUrl(preparedIceServers) : !managedTurnSelected && hasValidP2PTurnServerUrl(settings.P2P_turnServers))) {
|
|
41
71
|
options.rtcConfig = {
|
|
42
72
|
iceTransportPolicy: "relay"
|
|
43
73
|
};
|
|
@@ -46,6 +46,9 @@ function migrateLegacyRemoteConfigurationsInPlace(settings, log) {
|
|
|
46
46
|
}
|
|
47
47
|
const preferredId = settings.remoteType === REMOTE_MINIO ? "legacy-s3" : settings.remoteType === REMOTE_P2P ? "legacy-p2p" : "legacy-couchdb";
|
|
48
48
|
settings.activeConfigurationId = settings.remoteConfigurations[preferredId] ? preferredId : createdIds[0];
|
|
49
|
+
if (settings.remoteConfigurations["legacy-p2p"]) {
|
|
50
|
+
settings.P2P_ActiveRemoteConfigurationId = "legacy-p2p";
|
|
51
|
+
}
|
|
49
52
|
return true;
|
|
50
53
|
}
|
|
51
54
|
function createRemoteConfigurationId() {
|
|
@@ -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) {
|
|
@@ -44,6 +44,7 @@ export declare abstract class ServiceFileHandlerBase extends ServiceModuleBase<S
|
|
|
44
44
|
private setting;
|
|
45
45
|
private vault;
|
|
46
46
|
private fileReflectionProvenance?;
|
|
47
|
+
private readonly deferredConflictChecks;
|
|
47
48
|
constructor(services: ServiceFileHandlerDependencies);
|
|
48
49
|
get db(): DatabaseFileAccess;
|
|
49
50
|
get storage(): StorageAccess;
|
|
@@ -52,31 +53,148 @@ export declare abstract class ServiceFileHandlerBase extends ServiceModuleBase<S
|
|
|
52
53
|
private deleteProvenance;
|
|
53
54
|
private findUniqueContentRevision;
|
|
54
55
|
private getProvenBaseRevision;
|
|
56
|
+
/**
|
|
57
|
+
* Classify a storage read against its displayed database branch.
|
|
58
|
+
*
|
|
59
|
+
* A recorded revision identifies the branch and therefore takes precedence
|
|
60
|
+
* over byte matches. Without a readable base, only current live leaves are
|
|
61
|
+
* considered. Any match avoids a duplicate write, but only a unique match
|
|
62
|
+
* identifies provenance; no match is `unknown`.
|
|
63
|
+
* `preferredPath` selects the provenance key during a rename, and
|
|
64
|
+
* `currentLoaded` supplies an already decoded candidate body when available.
|
|
65
|
+
* Missing chunks do not clear provenance unless their metadata is also
|
|
66
|
+
* missing or deleted.
|
|
67
|
+
*/
|
|
68
|
+
private classifyStorageContent;
|
|
69
|
+
/** Record provenance only when storage matches one unambiguous live leaf. */
|
|
70
|
+
private rememberMatchingLeaf;
|
|
71
|
+
/**
|
|
72
|
+
* Read the raw provenance value used by an optimistic storage snapshot.
|
|
73
|
+
* Unlike `getProvenance`, this does not validate or remove the referenced
|
|
74
|
+
* database revision; an unreadable value is represented as `undefined`.
|
|
75
|
+
*/
|
|
76
|
+
private readProvenanceSnapshot;
|
|
77
|
+
/**
|
|
78
|
+
* Check that storage still has the same path, bytes, timestamp, and
|
|
79
|
+
* provenance as a previously captured snapshot.
|
|
80
|
+
*/
|
|
81
|
+
private currentStorageSnapshotMatches;
|
|
82
|
+
/**
|
|
83
|
+
* Publish provenance after a durable database write if storage is unchanged.
|
|
84
|
+
* A failed revalidation leaves the database revision stored and asks the
|
|
85
|
+
* caller to reread storage before proceeding.
|
|
86
|
+
*/
|
|
87
|
+
private finaliseStoredRevision;
|
|
88
|
+
/**
|
|
89
|
+
* Reread storage after a restored startup snapshot changed and retry a save.
|
|
90
|
+
* The bounded counter prevents an unstable file from causing an endless
|
|
91
|
+
* startup loop; the next event or scan can reconcile a final refusal.
|
|
92
|
+
*/
|
|
93
|
+
private retryCurrentStorage;
|
|
55
94
|
getPath(entry: AnyEntry): FilePathWithPrefix;
|
|
56
95
|
getPathWithoutPrefix(entry: AnyEntry): FilePathWithPrefix;
|
|
57
96
|
readFileFromStub(file: UXFileInfoStub | UXFileInfo): Promise<UXFileInfo>;
|
|
58
97
|
private infoToStub;
|
|
59
98
|
storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean): Promise<boolean>;
|
|
60
99
|
storeFileToDBWithBaseRevision(info: UXFileInfoStub | UXFileInfo | FilePathWithPrefix, baseRevision: string, createIfDifferent?: boolean): Promise<boolean>;
|
|
100
|
+
/**
|
|
101
|
+
* Store current storage content below an exact live revision. A differing
|
|
102
|
+
* body is rejected when `createIfDifferent` is false; callers hold the
|
|
103
|
+
* document lock for this core operation.
|
|
104
|
+
*/
|
|
105
|
+
private storeFileToDBWithBaseRevisionCore;
|
|
106
|
+
/**
|
|
107
|
+
* Save one storage snapshot while the caller holds its document lock.
|
|
108
|
+
* Ordinary saves read once inside the lock and leave retries disabled.
|
|
109
|
+
* Restored startup events retain bounded rereads because file watching has
|
|
110
|
+
* not yet started and cannot supply a new event for a concurrent edit.
|
|
111
|
+
*/
|
|
61
112
|
private storeFileToDBFromRevision;
|
|
62
113
|
deleteFileFromDB(info: UXFileInfoStub | UXInternalFileInfoStub | FilePath): Promise<boolean>;
|
|
114
|
+
/**
|
|
115
|
+
* Record a storage deletion under the caller's document lock, preserving
|
|
116
|
+
* the displayed conflict branch when its provenance is known.
|
|
117
|
+
*/
|
|
118
|
+
private deleteFileFromDBCore;
|
|
63
119
|
renameFileInDB(info: UXFileInfoStub | UXFileInfo, oldPath: FilePath | FilePathWithPrefix): Promise<boolean>;
|
|
120
|
+
/**
|
|
121
|
+
* Record a rename under the caller's source and target locks. Case-only
|
|
122
|
+
* renames retain the source provenance while other renames handle the
|
|
123
|
+
* source conflict before deleting its old path.
|
|
124
|
+
*/
|
|
125
|
+
private renameFileInDBCore;
|
|
64
126
|
deleteRevisionFromDB(info: UXFileInfoStub | FilePath | FilePathWithPrefix, rev: string): Promise<boolean | undefined>;
|
|
127
|
+
/** Delete one selected revision and clear matching provenance under the caller's document lock. */
|
|
128
|
+
private deleteRevisionFromDBCore;
|
|
65
129
|
resolveConflictedByDeletingRevision(info: UXFileInfoStub | FilePath, rev: string): Promise<boolean | undefined>;
|
|
130
|
+
/**
|
|
131
|
+
* Apply the legacy resolve-by-delete sequence while holding one document
|
|
132
|
+
* lock; metadata is captured before the selected revision is removed.
|
|
133
|
+
*/
|
|
134
|
+
private resolveConflictedByDeletingRevisionCore;
|
|
66
135
|
dbToStorageWithSpecificRev(info: UXFileInfoStub | UXFileInfo | FilePath | FilePathWithPrefix | null, rev: string, force?: boolean): Promise<boolean>;
|
|
136
|
+
/**
|
|
137
|
+
* Reflect a selected live revision under the caller's document lock.
|
|
138
|
+
* The public wrapper performs the same lock acquisition before entering here.
|
|
139
|
+
*/
|
|
140
|
+
private dbToStorageWithSpecificRevCore;
|
|
67
141
|
dbToStorage(entryInfo: MetaEntry | FilePathWithPrefix, info: UXFileInfoStub | UXFileInfo | FilePath | null, force?: boolean): Promise<boolean>;
|
|
142
|
+
/** Fetch the current metadata and reflect it while the caller holds its lock. */
|
|
143
|
+
private dbToStorageCore;
|
|
144
|
+
/**
|
|
145
|
+
* Reflect one database revision while protecting concurrent storage edits.
|
|
146
|
+
* Overwrite and deletion paths preserve changed bytes first, then revalidate
|
|
147
|
+
* their snapshot and the current database revision immediately before the
|
|
148
|
+
* destructive operation.
|
|
149
|
+
*/
|
|
68
150
|
private applyDatabaseEntryToStorage;
|
|
151
|
+
/**
|
|
152
|
+
* Preserve storage bytes before an incoming reflection can replace them.
|
|
153
|
+
*
|
|
154
|
+
* The initial read and provenance form one candidate snapshot. Classification
|
|
155
|
+
* gives a recorded branch priority, deduplicates matching live leaves, or
|
|
156
|
+
* creates an independent root for unknown origin. Optional `incomingContent` avoids
|
|
157
|
+
* re-reading an already loaded incoming body. Storage is revalidated before
|
|
158
|
+
* and after the database write; `preserved: true` stops the incoming write,
|
|
159
|
+
* while a returned `snapshot` lets the caller continue only after revalidation.
|
|
160
|
+
*/
|
|
69
161
|
private preserveUnsyncedStorageAsConflict;
|
|
162
|
+
/**
|
|
163
|
+
* Repeat overwrite protection from a fresh storage read after a snapshot
|
|
164
|
+
* becomes stale. Existing conflicts may defer reflection and request an
|
|
165
|
+
* open-only check while leaving the current storage untouched.
|
|
166
|
+
*/
|
|
167
|
+
private recheckIncomingStorage;
|
|
168
|
+
/**
|
|
169
|
+
* Re-read database metadata immediately before reflecting an incoming entry.
|
|
170
|
+
* If the current revision advanced, recurse with fresh storage; force and
|
|
171
|
+
* explicitly allowed conflict writes retain the caller's selected entry.
|
|
172
|
+
* `incomingRetry` bounds repeated database churn.
|
|
173
|
+
*/
|
|
174
|
+
private reconcileAdvancedIncomingRevision;
|
|
70
175
|
private _anyHandlerProcessesFileEvent;
|
|
71
176
|
private canRecordStorageDeletion;
|
|
177
|
+
/**
|
|
178
|
+
* Serialise work by every document ID represented by the supplied paths.
|
|
179
|
+
* IDs are deduplicated and sorted so aliases take one lock and cross-document renames have one order.
|
|
180
|
+
* The callback runs under those locks and must use core methods; conflict
|
|
181
|
+
* hooks requested inside it are deferred until all locks have been released.
|
|
182
|
+
*/
|
|
72
183
|
private serializedByFileEventPaths;
|
|
184
|
+
/**
|
|
185
|
+
* Queue a conflict hook while its document lock is held, or run it directly
|
|
186
|
+
* when no matching lock is active. Preserve `ifOpen` across deferral.
|
|
187
|
+
*/
|
|
188
|
+
private queueConflictCheckFor;
|
|
73
189
|
/**
|
|
74
190
|
* Revalidate a persisted storage operation against the current storage state.
|
|
75
191
|
*
|
|
76
192
|
* Snapshot entries preserve operation intent and ordering only. Their file
|
|
77
193
|
* stub, timestamps, and existence assumptions can be stale after a restart.
|
|
78
194
|
* Current inclusion is therefore read from storage, while destructive work
|
|
79
|
-
* is allowed only after current absence has been observed.
|
|
195
|
+
* is allowed only after current absence has been observed. Store and rename
|
|
196
|
+
* actions use the fresh reread with bounded retry because no later watcher
|
|
197
|
+
* event is guaranteed after startup reconciliation.
|
|
80
198
|
*/
|
|
81
199
|
private _processRestoredFileEvent;
|
|
82
200
|
private _planRestoredFileEvent;
|