@vrtmrz/livesync-commonlib 0.1.14 → 0.1.16
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/replication/couchdb/LiveSyncReplicator.d.ts +35 -12
- package/dist/replication/couchdb/LiveSyncReplicator.js +116 -41
- package/dist/services/base/IService.d.ts +18 -4
- package/dist/services/base/RemoteConnection.d.ts +44 -0
- package/dist/services/base/RemoteConnection.js +0 -0
- package/dist/services/base/RemoteService.d.ts +23 -19
- package/dist/services/base/RemoteService.js +136 -31
- package/dist/string_and_binary/path.js +1 -5
- package/dist/worker/bgWorker.js +1 -1
- package/docs/development.md +4 -0
- package/docs/proven-in-use.md +2 -0
- package/package.json +1 -1
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import { type EntryDoc, type RemoteDBSettings, type EntryLeaf, type SyncParameters, type DatabaseEntry, type NodeData, type RemotePreferredTweakResult } from "../../common/types.js";
|
|
2
2
|
import { LiveSyncAbstractReplicator, type LiveSyncReplicatorEnv, type RemoteDBStatus } from "../LiveSyncAbstractReplicator.js";
|
|
3
3
|
import type { ServiceHub } from "../../services/ServiceHub.js";
|
|
4
|
+
import type { OwnedCouchDBConnection, RemoteConnectionOpenOptions } from "../../services/base/RemoteConnection.js";
|
|
5
|
+
export type { OwnedCouchDBConnection, RemoteConnectionOpenOptions } from "../../services/base/RemoteConnection.js";
|
|
4
6
|
export interface LiveSyncCouchDBReplicatorEnv extends LiveSyncReplicatorEnv {
|
|
5
7
|
services: ServiceHub;
|
|
8
|
+
/** Internal injection point for the bounded one-shot connectivity preflight. */
|
|
9
|
+
oneShotConnectivityTimeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* An owned CouchDB connection which has passed LiveSync's compatibility checks
|
|
13
|
+
* and carries the options required to start replication.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* The caller must transfer ownership or call
|
|
17
|
+
* {@link OwnedCouchDBConnection.close}.
|
|
18
|
+
* The connection retains the same `close()` implementation while compatibility
|
|
19
|
+
* checks add replication options to it.
|
|
20
|
+
*/
|
|
21
|
+
export interface CouchDBReplicationConnection extends OwnedCouchDBConnection<EntryDoc> {
|
|
22
|
+
/** Base options shared by finite and continuous replication. */
|
|
23
|
+
readonly syncOptionBase: PouchDB.Replication.SyncOptions;
|
|
24
|
+
/** Options selected for the requested finite or continuous replication. */
|
|
25
|
+
readonly syncOption: PouchDB.Replication.SyncOptions;
|
|
6
26
|
}
|
|
7
27
|
export declare class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator {
|
|
28
|
+
env: LiveSyncCouchDBReplicatorEnv;
|
|
8
29
|
isMobile(): boolean;
|
|
9
30
|
constructor(env: LiveSyncCouchDBReplicatorEnv);
|
|
10
31
|
getInitialSyncParameters(setting: RemoteDBSettings): Promise<SyncParameters>;
|
|
@@ -32,26 +53,28 @@ export declare class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicato
|
|
|
32
53
|
updateMaxTransferredSeqOnChunks(localDB: PouchDB.Database, remoteID: number, seqStatusMap: Record<number, boolean>): Promise<ReturnType<typeof this.getEmptyMaxEntry>>;
|
|
33
54
|
sendChunks(setting: RemoteDBSettings, remoteDB: PouchDB.Database<EntryDoc> | undefined, showResult: boolean, fromSeq?: number | string): Promise<boolean>;
|
|
34
55
|
openOneShotReplication(setting: RemoteDBSettings, showResult: boolean, retrying: boolean, syncMode: "sync" | "pullOnly" | "pushOnly", ignoreCleanLock?: boolean): Promise<boolean>;
|
|
35
|
-
private
|
|
36
|
-
private
|
|
56
|
+
private checkOneShotReplicationConnectivity;
|
|
57
|
+
private closeRemoteConnection;
|
|
58
|
+
private withRemoteConnection;
|
|
37
59
|
replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise<boolean>;
|
|
38
60
|
replicateAllFromServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise<boolean>;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
info: PouchDB.Core.DatabaseInfo;
|
|
42
|
-
syncOptionBase: PouchDB.Replication.SyncOptions;
|
|
43
|
-
syncOption: PouchDB.Replication.SyncOptions;
|
|
44
|
-
}>;
|
|
61
|
+
private reportConnectivityPreflightTimeout;
|
|
62
|
+
checkReplicationConnectivity(setting: RemoteDBSettings, keepAlive: boolean, skipCheck: boolean, showResult: boolean, ignoreCleanLock?: boolean, connectionOptions?: RemoteConnectionOpenOptions): Promise<false | CouchDBReplicationConnection>;
|
|
45
63
|
openContinuousReplication(setting: RemoteDBSettings, showResult: boolean, retrying: boolean): Promise<boolean>;
|
|
46
64
|
closeReplication(): void;
|
|
47
65
|
tryResetRemoteDatabase(setting: RemoteDBSettings): Promise<void>;
|
|
48
66
|
tryCreateRemoteDatabase(setting: RemoteDBSettings): Promise<void>;
|
|
49
67
|
markRemoteLocked(setting: RemoteDBSettings, locked: boolean, lockByClean: boolean): Promise<void>;
|
|
50
68
|
markRemoteResolved(setting: RemoteDBSettings): Promise<void>;
|
|
51
|
-
connectRemoteCouchDBWithSetting(settings: RemoteDBSettings, isMobile: boolean, performSetup?: boolean, skipInfo?: boolean): Promise<string |
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
connectRemoteCouchDBWithSetting(settings: RemoteDBSettings, isMobile: boolean, performSetup?: boolean, skipInfo?: boolean, connectionOptions?: RemoteConnectionOpenOptions): Promise<string | OwnedCouchDBConnection<EntryDoc>> | "Empty passphrases cannot be used without explicit permission";
|
|
70
|
+
private openOwnedConnection;
|
|
71
|
+
/**
|
|
72
|
+
* Opens a raw PouchDB handle for compatibility with earlier Commonlib
|
|
73
|
+
* consumers.
|
|
74
|
+
*
|
|
75
|
+
* @deprecated Use {@link connectRemoteCouchDBWithSetting} and close the
|
|
76
|
+
* complete owned connection after use.
|
|
77
|
+
*/
|
|
55
78
|
_ensureConnection<T extends DatabaseEntry>(settings: RemoteDBSettings, performSetup?: boolean): Promise<PouchDB.Database<T>>;
|
|
56
79
|
/**
|
|
57
80
|
* Fetch a document from the remote database directly.
|
|
@@ -41,12 +41,20 @@ import {
|
|
|
41
41
|
SyncParamsNotFoundError,
|
|
42
42
|
SyncParamsUpdateError
|
|
43
43
|
} from "../SyncParamsHandler.js";
|
|
44
|
+
import { compatGlobal } from "../../common/coreEnvFunctions.js";
|
|
44
45
|
const currentVersionRange = {
|
|
45
46
|
min: 0,
|
|
46
47
|
max: 2400,
|
|
47
48
|
current: 2
|
|
48
49
|
};
|
|
49
50
|
const selectorOnDemandPull = { selector: { type: { $ne: "leaf" } } };
|
|
51
|
+
const DEFAULT_ONE_SHOT_CONNECTIVITY_TIMEOUT_MS = 6e4;
|
|
52
|
+
class OneShotConnectivityPreflightTimeoutError extends Error {
|
|
53
|
+
constructor(timeoutMs) {
|
|
54
|
+
super(`Remote connectivity preflight exceeded ${timeoutMs} ms.`);
|
|
55
|
+
this.name = "OneShotConnectivityPreflightTimeoutError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
50
58
|
async function* genReplication(s, signal) {
|
|
51
59
|
const inbox = new StreamInbox({ capacity: 1e4 });
|
|
52
60
|
const push = function(e) {
|
|
@@ -379,11 +387,11 @@ class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator {
|
|
|
379
387
|
);
|
|
380
388
|
return false;
|
|
381
389
|
}
|
|
382
|
-
return await this.
|
|
390
|
+
return await this.withRemoteConnection(d, async (db) => this.sendChunks(setting, db, showResult, fromSeq));
|
|
383
391
|
}
|
|
384
392
|
const connectivity = await this.checkReplicationConnectivity(setting, false, false, false, false);
|
|
385
393
|
if (connectivity !== false) {
|
|
386
|
-
await this.
|
|
394
|
+
await this.closeRemoteConnection(connectivity);
|
|
387
395
|
}
|
|
388
396
|
Logger(`Bulk sending chunks to remote database...`, showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "fetch");
|
|
389
397
|
const remoteMilestone = await remoteDB.get(MILESTONE_DOCID);
|
|
@@ -534,7 +542,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
534
542
|
}
|
|
535
543
|
const localDB = this.rawDatabase;
|
|
536
544
|
Logger(this.translate("liveSyncReplicator.oneShotSyncBegin", { syncMode }));
|
|
537
|
-
const ret = await this.
|
|
545
|
+
const ret = await this.checkOneShotReplicationConnectivity(setting, retrying, showResult, ignoreCleanLock);
|
|
538
546
|
if (ret === false) {
|
|
539
547
|
Logger(
|
|
540
548
|
this.translate("liveSyncReplicator.couldNotConnectToServer"),
|
|
@@ -613,7 +621,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
613
621
|
}
|
|
614
622
|
return false;
|
|
615
623
|
} finally {
|
|
616
|
-
await this.
|
|
624
|
+
await this.closeRemoteConnection(ret);
|
|
617
625
|
}
|
|
618
626
|
});
|
|
619
627
|
if (typeof next === "boolean") {
|
|
@@ -621,21 +629,38 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
621
629
|
}
|
|
622
630
|
return await next();
|
|
623
631
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
632
|
+
async checkOneShotReplicationConnectivity(setting, skipCheck, showResult, ignoreCleanLock) {
|
|
633
|
+
if (setting.useRequestAPI) {
|
|
634
|
+
return await this.checkReplicationConnectivity(setting, false, skipCheck, showResult, ignoreCleanLock);
|
|
635
|
+
}
|
|
636
|
+
const timeoutMs = this.env.oneShotConnectivityTimeoutMs ?? DEFAULT_ONE_SHOT_CONNECTIVITY_TIMEOUT_MS;
|
|
637
|
+
const controller = new AbortController();
|
|
638
|
+
const timeout = compatGlobal.setTimeout(
|
|
639
|
+
() => controller.abort(new OneShotConnectivityPreflightTimeoutError(timeoutMs)),
|
|
640
|
+
timeoutMs
|
|
641
|
+
);
|
|
642
|
+
try {
|
|
643
|
+
return await this.checkReplicationConnectivity(setting, false, skipCheck, showResult, ignoreCleanLock, {
|
|
644
|
+
signal: controller.signal,
|
|
645
|
+
allowNativeFallback: false
|
|
646
|
+
});
|
|
647
|
+
} finally {
|
|
648
|
+
compatGlobal.clearTimeout(timeout);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async closeRemoteConnection(connection) {
|
|
627
652
|
try {
|
|
628
|
-
await
|
|
653
|
+
await connection.close();
|
|
629
654
|
} catch (ex) {
|
|
630
655
|
Logger("Failed to close remote database.", LOG_LEVEL_INFO, "sync");
|
|
631
656
|
Logger(ex, LOG_LEVEL_VERBOSE, "sync");
|
|
632
657
|
}
|
|
633
658
|
}
|
|
634
|
-
async
|
|
659
|
+
async withRemoteConnection(connection, task) {
|
|
635
660
|
try {
|
|
636
|
-
return await task(db);
|
|
661
|
+
return await task(connection.db);
|
|
637
662
|
} finally {
|
|
638
|
-
await this.
|
|
663
|
+
await this.closeRemoteConnection(connection);
|
|
639
664
|
}
|
|
640
665
|
}
|
|
641
666
|
replicateAllToServer(setting, showingNotice) {
|
|
@@ -644,7 +669,17 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
644
669
|
replicateAllFromServer(setting, showingNotice) {
|
|
645
670
|
return this.openOneShotReplication(setting, showingNotice ?? false, false, "pullOnly");
|
|
646
671
|
}
|
|
647
|
-
|
|
672
|
+
reportConnectivityPreflightTimeout(connectionOptions, showResult, error) {
|
|
673
|
+
if (!connectionOptions?.signal?.aborted || !(connectionOptions.signal.reason instanceof OneShotConnectivityPreflightTimeoutError)) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
Logger("The remote connectivity check timed out.", showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "sync");
|
|
677
|
+
if (error !== void 0) {
|
|
678
|
+
Logger(error, LOG_LEVEL_VERBOSE, "sync");
|
|
679
|
+
}
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
async checkReplicationConnectivity(setting, keepAlive, skipCheck, showResult, ignoreCleanLock = false, connectionOptions) {
|
|
648
683
|
if (setting.versionUpFlash != "") {
|
|
649
684
|
Logger(this.translate("Replicator.Message.VersionUpFlash"), LOG_LEVEL_NOTICE);
|
|
650
685
|
return false;
|
|
@@ -654,8 +689,25 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
654
689
|
Logger("Another replication running.");
|
|
655
690
|
return false;
|
|
656
691
|
}
|
|
657
|
-
|
|
692
|
+
let dbRet;
|
|
693
|
+
try {
|
|
694
|
+
dbRet = await this.connectRemoteCouchDBWithSetting(
|
|
695
|
+
setting,
|
|
696
|
+
this.isMobile(),
|
|
697
|
+
true,
|
|
698
|
+
false,
|
|
699
|
+
connectionOptions
|
|
700
|
+
);
|
|
701
|
+
} catch (ex) {
|
|
702
|
+
if (this.reportConnectivityPreflightTimeout(connectionOptions, showResult, ex)) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
throw ex;
|
|
706
|
+
}
|
|
658
707
|
if (typeof dbRet === "string") {
|
|
708
|
+
if (this.reportConnectivityPreflightTimeout(connectionOptions, showResult)) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
659
711
|
Logger(
|
|
660
712
|
this.translate("liveSyncReplicator.couldNotConnectToURI", { uri, dbRet }),
|
|
661
713
|
showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
|
|
@@ -734,13 +786,25 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
734
786
|
if (setting.readChunksOnline) {
|
|
735
787
|
syncOptionBase.pull = { ...selectorOnDemandPull };
|
|
736
788
|
}
|
|
789
|
+
if (this.reportConnectivityPreflightTimeout(connectionOptions, showResult)) {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
737
792
|
const syncOption = keepAlive ? { live: true, retry: true, heartbeat: setting.useTimeouts ? false : 3e4, ...syncOptionBase } : { ...syncOptionBase };
|
|
738
|
-
const connection = {
|
|
793
|
+
const connection = {
|
|
794
|
+
...dbRet,
|
|
795
|
+
syncOptionBase,
|
|
796
|
+
syncOption
|
|
797
|
+
};
|
|
739
798
|
ownershipTransferred = true;
|
|
740
799
|
return connection;
|
|
800
|
+
} catch (ex) {
|
|
801
|
+
if (this.reportConnectivityPreflightTimeout(connectionOptions, showResult, ex)) {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
throw ex;
|
|
741
805
|
} finally {
|
|
742
806
|
if (!ownershipTransferred) {
|
|
743
|
-
await this.
|
|
807
|
+
await this.closeRemoteConnection(dbRet);
|
|
744
808
|
}
|
|
745
809
|
}
|
|
746
810
|
}
|
|
@@ -828,7 +892,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
828
892
|
}
|
|
829
893
|
}
|
|
830
894
|
} finally {
|
|
831
|
-
await this.
|
|
895
|
+
await this.closeRemoteConnection(ret);
|
|
832
896
|
}
|
|
833
897
|
}
|
|
834
898
|
return false;
|
|
@@ -862,7 +926,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
862
926
|
Logger(ex, LOG_LEVEL_NOTICE);
|
|
863
927
|
}
|
|
864
928
|
} finally {
|
|
865
|
-
await this.
|
|
929
|
+
await this.closeRemoteConnection(con);
|
|
866
930
|
}
|
|
867
931
|
clearHandlers();
|
|
868
932
|
await this.ensurePBKDF2Salt(setting, true, false);
|
|
@@ -871,7 +935,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
871
935
|
this.closeReplication();
|
|
872
936
|
const con2 = await this.connectRemoteCouchDBWithSetting(setting, this.isMobile(), true);
|
|
873
937
|
if (typeof con2 === "string") return;
|
|
874
|
-
await this.
|
|
938
|
+
await this.closeRemoteConnection(con2);
|
|
875
939
|
clearHandlers();
|
|
876
940
|
await this.ensurePBKDF2Salt(setting, true, false);
|
|
877
941
|
Logger(this.translate("liveSyncReplicator.remoteDbCreatedOrConnected"), LOG_LEVEL_NOTICE);
|
|
@@ -883,7 +947,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
883
947
|
Logger(this.translate("liveSyncReplicator.couldNotConnectToURI", { uri, dbRet }), LOG_LEVEL_NOTICE);
|
|
884
948
|
return;
|
|
885
949
|
}
|
|
886
|
-
await this.
|
|
950
|
+
await this.withRemoteConnection(dbRet, async (db) => {
|
|
887
951
|
if (!await checkRemoteVersion(db, this.migrate.bind(this), VER)) {
|
|
888
952
|
Logger(this.translate("liveSyncReplicator.remoteDbCorrupted"), LOG_LEVEL_NOTICE);
|
|
889
953
|
return;
|
|
@@ -922,7 +986,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
922
986
|
Logger(this.translate("liveSyncReplicator.couldNotConnectToURI", { uri, dbRet }), LOG_LEVEL_NOTICE);
|
|
923
987
|
return;
|
|
924
988
|
}
|
|
925
|
-
await this.
|
|
989
|
+
await this.withRemoteConnection(dbRet, async (db) => {
|
|
926
990
|
if (!await checkRemoteVersion(db, this.migrate.bind(this), VER)) {
|
|
927
991
|
Logger(this.translate("liveSyncReplicator.remoteDbCorrupted"), LOG_LEVEL_NOTICE);
|
|
928
992
|
return;
|
|
@@ -952,7 +1016,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
952
1016
|
}
|
|
953
1017
|
});
|
|
954
1018
|
}
|
|
955
|
-
connectRemoteCouchDBWithSetting(settings, isMobile, performSetup = false, skipInfo = false) {
|
|
1019
|
+
connectRemoteCouchDBWithSetting(settings, isMobile, performSetup = false, skipInfo = false, connectionOptions) {
|
|
956
1020
|
if (settings.encrypt && settings.passphrase == "" && !settings.permitEmptyPassphrase) {
|
|
957
1021
|
return "Empty passphrases cannot be used without explicit permission";
|
|
958
1022
|
}
|
|
@@ -980,15 +1044,26 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
980
1044
|
settings.enableCompression,
|
|
981
1045
|
customHeaders,
|
|
982
1046
|
settings.useRequestAPI,
|
|
983
|
-
async () => await this.getReplicationPBKDF2Salt(settings)
|
|
1047
|
+
async () => await this.getReplicationPBKDF2Salt(settings),
|
|
1048
|
+
connectionOptions
|
|
984
1049
|
);
|
|
985
1050
|
}
|
|
986
|
-
async
|
|
1051
|
+
async openOwnedConnection(settings, performSetup = false) {
|
|
987
1052
|
const ret = await this.connectRemoteCouchDBWithSetting(settings, this.isMobile(), performSetup, true);
|
|
988
1053
|
if (typeof ret === "string") {
|
|
989
1054
|
throw new Error(`${this.translate("liveSyncReplicator.couldNotConnectToServer")}:${ret}`);
|
|
990
1055
|
}
|
|
991
|
-
return ret
|
|
1056
|
+
return ret;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Opens a raw PouchDB handle for compatibility with earlier Commonlib
|
|
1060
|
+
* consumers.
|
|
1061
|
+
*
|
|
1062
|
+
* @deprecated Use {@link connectRemoteCouchDBWithSetting} and close the
|
|
1063
|
+
* complete owned connection after use.
|
|
1064
|
+
*/
|
|
1065
|
+
async _ensureConnection(settings, performSetup = false) {
|
|
1066
|
+
return (await this.openOwnedConnection(settings, performSetup)).db;
|
|
992
1067
|
}
|
|
993
1068
|
/**
|
|
994
1069
|
* Fetch a document from the remote database directly.
|
|
@@ -999,8 +1074,8 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
999
1074
|
* @throws {Error} Other errors that may occur during the fetch operation.
|
|
1000
1075
|
*/
|
|
1001
1076
|
async fetchRemoteDocument(settings, id, db) {
|
|
1002
|
-
const
|
|
1003
|
-
const connDB = db ??
|
|
1077
|
+
const connection = db === void 0 ? await this.openOwnedConnection(settings) : void 0;
|
|
1078
|
+
const connDB = db ?? connection.db;
|
|
1004
1079
|
try {
|
|
1005
1080
|
return await connDB.get(id);
|
|
1006
1081
|
} catch (ex) {
|
|
@@ -1009,8 +1084,8 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1009
1084
|
}
|
|
1010
1085
|
throw ex;
|
|
1011
1086
|
} finally {
|
|
1012
|
-
if (
|
|
1013
|
-
await this.
|
|
1087
|
+
if (connection) {
|
|
1088
|
+
await this.closeRemoteConnection(connection);
|
|
1014
1089
|
}
|
|
1015
1090
|
}
|
|
1016
1091
|
}
|
|
@@ -1023,13 +1098,13 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1023
1098
|
* @throws {Error} If the document could not be put.
|
|
1024
1099
|
*/
|
|
1025
1100
|
async putRemoteDocument(settings, doc, db) {
|
|
1026
|
-
const
|
|
1027
|
-
const connDB = db ??
|
|
1101
|
+
const connection = db === void 0 ? await this.openOwnedConnection(settings, true) : void 0;
|
|
1102
|
+
const connDB = db ?? connection.db;
|
|
1028
1103
|
try {
|
|
1029
1104
|
return await connDB.put(doc);
|
|
1030
1105
|
} finally {
|
|
1031
|
-
if (
|
|
1032
|
-
await this.
|
|
1106
|
+
if (connection) {
|
|
1107
|
+
await this.closeRemoteConnection(connection);
|
|
1033
1108
|
}
|
|
1034
1109
|
}
|
|
1035
1110
|
}
|
|
@@ -1043,7 +1118,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1043
1118
|
);
|
|
1044
1119
|
return false;
|
|
1045
1120
|
}
|
|
1046
|
-
return await this.
|
|
1121
|
+
return await this.withRemoteConnection(ret, async (db) => {
|
|
1047
1122
|
const remoteChunks = await db.allDocs({ keys: missingChunks, include_docs: true });
|
|
1048
1123
|
const errorRows = remoteChunks.rows.filter((e) => "error" in e);
|
|
1049
1124
|
if (errorRows.length > 0) {
|
|
@@ -1074,7 +1149,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1074
1149
|
);
|
|
1075
1150
|
return false;
|
|
1076
1151
|
}
|
|
1077
|
-
return await this.
|
|
1152
|
+
return await this.withRemoteConnection(db, async () => {
|
|
1078
1153
|
Logger(`Connected to ${db.info.db_name} successfully`, LOG_LEVEL_NOTICE);
|
|
1079
1154
|
return true;
|
|
1080
1155
|
});
|
|
@@ -1086,7 +1161,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1086
1161
|
Logger(this.translate("liveSyncReplicator.couldNotConnectToURI", { uri, dbRet }), LOG_LEVEL_NOTICE);
|
|
1087
1162
|
return;
|
|
1088
1163
|
}
|
|
1089
|
-
await this.
|
|
1164
|
+
await this.withRemoteConnection(dbRet, async (db) => {
|
|
1090
1165
|
if (!await checkRemoteVersion(db, this.migrate.bind(this), VER)) {
|
|
1091
1166
|
Logger(this.translate("liveSyncReplicator.remoteDbCorrupted"), LOG_LEVEL_NOTICE);
|
|
1092
1167
|
return;
|
|
@@ -1109,7 +1184,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1109
1184
|
Logger(this.translate("liveSyncReplicator.couldNotConnectToURI", { uri, dbRet }), LOG_LEVEL_NOTICE);
|
|
1110
1185
|
return;
|
|
1111
1186
|
}
|
|
1112
|
-
await this.
|
|
1187
|
+
await this.withRemoteConnection(dbRet, async (db) => {
|
|
1113
1188
|
if (!await checkRemoteVersion(db, this.migrate.bind(this), VER)) {
|
|
1114
1189
|
Logger(this.translate("liveSyncReplicator.remoteDbCorrupted"), LOG_LEVEL_NOTICE);
|
|
1115
1190
|
return;
|
|
@@ -1145,7 +1220,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1145
1220
|
error: new Error(dbRet)
|
|
1146
1221
|
};
|
|
1147
1222
|
}
|
|
1148
|
-
return await this.
|
|
1223
|
+
return await this.withRemoteConnection(dbRet, async (db) => {
|
|
1149
1224
|
try {
|
|
1150
1225
|
if (!await checkRemoteVersion(db, this.migrate.bind(this), VER)) {
|
|
1151
1226
|
const error = new Error("The remote database version is not compatible");
|
|
@@ -1205,7 +1280,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1205
1280
|
Logger(this.translate("liveSyncReplicator.couldNotConnectToURI", { uri, dbRet }), LOG_LEVEL_NOTICE);
|
|
1206
1281
|
return false;
|
|
1207
1282
|
}
|
|
1208
|
-
return await this.
|
|
1283
|
+
return await this.withRemoteConnection(dbRet, async (db) => (await db.compact({ interval: 1e3 })).ok);
|
|
1209
1284
|
}
|
|
1210
1285
|
async getRemoteStatus(setting) {
|
|
1211
1286
|
const dbRet = await this.connectRemoteCouchDBWithSetting(setting, this.isMobile(), true);
|
|
@@ -1214,7 +1289,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1214
1289
|
Logger(this.translate("liveSyncReplicator.couldNotConnectToURI", { uri, dbRet }), LOG_LEVEL_NOTICE);
|
|
1215
1290
|
return false;
|
|
1216
1291
|
}
|
|
1217
|
-
return await this.
|
|
1292
|
+
return await this.withRemoteConnection(dbRet, async (db) => {
|
|
1218
1293
|
const info = await db.info();
|
|
1219
1294
|
return {
|
|
1220
1295
|
...info,
|
|
@@ -1229,7 +1304,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1229
1304
|
Logger(this.translate("liveSyncReplicator.couldNotConnectToURI", { uri, dbRet }), LOG_LEVEL_NOTICE);
|
|
1230
1305
|
return false;
|
|
1231
1306
|
}
|
|
1232
|
-
return await this.
|
|
1307
|
+
return await this.withRemoteConnection(dbRet, countCompromisedChunks);
|
|
1233
1308
|
}
|
|
1234
1309
|
async getConnectedDeviceList(setting = this.currentSettings) {
|
|
1235
1310
|
const dbRet = await this.connectRemoteCouchDBWithSetting(setting, this.isMobile(), true);
|
|
@@ -1238,7 +1313,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1238
1313
|
Logger(this.translate("liveSyncReplicator.couldNotConnectToURI", { uri, dbRet }), LOG_LEVEL_NOTICE);
|
|
1239
1314
|
return false;
|
|
1240
1315
|
}
|
|
1241
|
-
return await this.
|
|
1316
|
+
return await this.withRemoteConnection(dbRet, async (db) => {
|
|
1242
1317
|
const milestoneDoc = await db.get(MILESTONE_DOCID);
|
|
1243
1318
|
if (!milestoneDoc) {
|
|
1244
1319
|
Logger("Could not retrieve remote milestone", LOG_LEVEL_NOTICE);
|
|
@@ -11,6 +11,7 @@ import type { ReplicatorService } from "./ReplicatorService.js";
|
|
|
11
11
|
import type { DatabaseEventService } from "./DatabaseEventService.js";
|
|
12
12
|
import type { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "../../common/models/shared.const.symbols.js";
|
|
13
13
|
import type { AsyncActivityOptions } from "../../interfaces/AsyncActivityRunner.js";
|
|
14
|
+
import type { OwnedCouchDBConnection, RemoteConnectionOpenOptions } from "./RemoteConnection.js";
|
|
14
15
|
declare global {
|
|
15
16
|
interface OPTIONAL_SYNC_FEATURES {
|
|
16
17
|
DISABLE: "DISABLE";
|
|
@@ -135,10 +136,23 @@ export interface IReplicationService {
|
|
|
135
136
|
markResolved(): Promise<void>;
|
|
136
137
|
}
|
|
137
138
|
export interface IRemoteService {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Opens an owned remote CouchDB connection.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* The caller must transfer ownership or call `close()` on the returned
|
|
144
|
+
* connection. Closing it aborts outstanding abort-capable requests before
|
|
145
|
+
* closing the PouchDB handle. Host-native request adapters may not honour
|
|
146
|
+
* `AbortSignal`; callers must not report those requests as cancelled without
|
|
147
|
+
* a host guarantee.
|
|
148
|
+
*
|
|
149
|
+
* @returns An error description, or an owned remote connection. When
|
|
150
|
+
* `skipInfo` is true, the `info` field retains its compatibility placeholder.
|
|
151
|
+
*
|
|
152
|
+
* @throws When PouchDB construction or local connection configuration fails
|
|
153
|
+
* before a connection result can be produced.
|
|
154
|
+
*/
|
|
155
|
+
connect(uri: string, auth: CouchDBCredentials, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean, performSetup: boolean, skipInfo: boolean, compression: boolean, customHeaders: Record<string, string>, useRequestAPI: boolean, getPBKDF2Salt: () => Promise<Uint8Array<ArrayBuffer>>, options?: RemoteConnectionOpenOptions): Promise<string | OwnedCouchDBConnection<EntryDoc>>;
|
|
142
156
|
/**
|
|
143
157
|
* State if the last POST request failed due to payload size.
|
|
144
158
|
*/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options which bind requests made through a remote CouchDB connection to the
|
|
3
|
+
* operation which owns it.
|
|
4
|
+
*/
|
|
5
|
+
export interface RemoteConnectionOpenOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Cancels abort-capable requests made through the connection when the
|
|
8
|
+
* owning operation is cancelled.
|
|
9
|
+
*/
|
|
10
|
+
readonly signal?: AbortSignal;
|
|
11
|
+
/**
|
|
12
|
+
* Allows the diagnostic fallback to a host-native request API after a
|
|
13
|
+
* web-compatible request reports a network `TypeError`.
|
|
14
|
+
*
|
|
15
|
+
* Set this to `false` when the owner must retain an abortable request path.
|
|
16
|
+
* A host-native adapter is not assumed to honour `AbortSignal`.
|
|
17
|
+
*/
|
|
18
|
+
readonly allowNativeFallback?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A remote PouchDB handle together with the lifecycle of requests created
|
|
22
|
+
* through that handle.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* The caller owns this connection and must call
|
|
26
|
+
* {@link OwnedCouchDBConnection.close} when ownership is not transferred.
|
|
27
|
+
* `close()` is idempotent: it first cancels abort-capable requests within this
|
|
28
|
+
* connection, then closes the PouchDB handle. Closing the PouchDB handle retains
|
|
29
|
+
* its normal `closed` event behaviour. This contract does not imply exclusive
|
|
30
|
+
* ownership of a physical HTTP socket.
|
|
31
|
+
*
|
|
32
|
+
* @typeParam T - The document type stored in the remote database.
|
|
33
|
+
*/
|
|
34
|
+
export interface OwnedCouchDBConnection<T extends object> {
|
|
35
|
+
/** The remote PouchDB database handle. */
|
|
36
|
+
readonly db: PouchDB.Database<T>;
|
|
37
|
+
/**
|
|
38
|
+
* The database information read while opening the connection. When the
|
|
39
|
+
* caller skips that read, compatibility placeholders are returned.
|
|
40
|
+
*/
|
|
41
|
+
readonly info: PouchDB.Core.DatabaseInfo;
|
|
42
|
+
/** Cancels scoped requests and closes the PouchDB handle exactly once. */
|
|
43
|
+
close(): Promise<void>;
|
|
44
|
+
}
|
|
File without changes
|
|
@@ -9,6 +9,7 @@ import type { SettingService } from "./SettingService.js";
|
|
|
9
9
|
import { UnresolvedErrorManager } from "./UnresolvedErrorManager.js";
|
|
10
10
|
import { type LogFunction } from "../lib/logUtils.js";
|
|
11
11
|
import type { PouchDBConstructor } from "../../pouchdb/PouchDBConstructor.js";
|
|
12
|
+
import type { OwnedCouchDBConnection, RemoteConnectionOpenOptions } from "./RemoteConnection.js";
|
|
12
13
|
export interface RemoteServiceDependencies {
|
|
13
14
|
/** PouchDB with the HTTP adapter required by the host runtime already registered. */
|
|
14
15
|
pouchDB: PouchDBConstructor;
|
|
@@ -25,21 +26,6 @@ type FetchMethod = (typeof FetchMethod)[keyof typeof FetchMethod];
|
|
|
25
26
|
* The RemoteService provides methods for interacting with the remote database.
|
|
26
27
|
*/
|
|
27
28
|
export declare abstract class RemoteService<T extends ServiceContext = ServiceContext> extends ServiceBase<T> implements IRemoteService {
|
|
28
|
-
/**
|
|
29
|
-
* Connect to the remote database with the provided settings.
|
|
30
|
-
* @param uri The URI of the remote database.
|
|
31
|
-
* @param auth The authentication credentials for the remote database.
|
|
32
|
-
* @param disableRequestURI Whether to disable the request URI.
|
|
33
|
-
* @param passphrase The passphrase for the remote database.
|
|
34
|
-
* @param useDynamicIterationCount Whether to use dynamic iteration count.
|
|
35
|
-
* @param performSetup Whether to perform setup.
|
|
36
|
-
* @param skipInfo Whether to skip information retrieval.
|
|
37
|
-
* @param compression Whether to enable compression.
|
|
38
|
-
* @param customHeaders Custom headers to include in the request.
|
|
39
|
-
* @param useRequestAPI Whether to use the request API.
|
|
40
|
-
* @param getPBKDF2Salt Function to retrieve the PBKDF2 salt.
|
|
41
|
-
* Note that this function is used for CouchDB and compatible only.
|
|
42
|
-
*/
|
|
43
29
|
protected _log: LogFunction;
|
|
44
30
|
protected _authHeader: AuthorizationHeaderGenerator;
|
|
45
31
|
protected _APIService: APIService;
|
|
@@ -53,9 +39,27 @@ export declare abstract class RemoteService<T extends ServiceContext = ServiceCo
|
|
|
53
39
|
showError(msg: string, max_log_level?: LOG_LEVEL): void;
|
|
54
40
|
clearErrors(): void;
|
|
55
41
|
performFetch(req: string | Request, opts?: RequestInit, fetchMethod?: FetchMethod): Promise<Response>;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Opens a remote CouchDB connection with request cancellation bound to its
|
|
44
|
+
* owner.
|
|
45
|
+
*
|
|
46
|
+
* @param uri - The URI of the remote database.
|
|
47
|
+
* @param auth - Authentication credentials for the remote database.
|
|
48
|
+
* @param disableRequestURI - Whether request URI handling is disabled.
|
|
49
|
+
* @param passphrase - The database encryption passphrase, or `false` when
|
|
50
|
+
* encryption is disabled.
|
|
51
|
+
* @param useDynamicIterationCount - Whether legacy dynamic iteration counts
|
|
52
|
+
* are enabled.
|
|
53
|
+
* @param performSetup - Whether PouchDB may set up a missing database.
|
|
54
|
+
* @param skipInfo - Whether to omit the opening database information request.
|
|
55
|
+
* @param compression - Whether chunk compression is enabled.
|
|
56
|
+
* @param customHeaders - Additional HTTP headers for every request.
|
|
57
|
+
* @param useRequestAPI - Whether to use the host-native request adapter.
|
|
58
|
+
* @param getPBKDF2Salt - Resolves the PBKDF2 salt when encryption needs it.
|
|
59
|
+
* @param connectionOptions - Ownership and request fallback options.
|
|
60
|
+
* @returns An error description, or the owned connection. The caller must
|
|
61
|
+
* transfer ownership or call `close()`.
|
|
62
|
+
*/
|
|
63
|
+
connect(uri: string, auth: CouchDBCredentials, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean, performSetup: boolean, skipInfo: boolean, compression: boolean, customHeaders: Record<string, string>, useRequestAPI: boolean, getPBKDF2Salt: () => Promise<Uint8Array<ArrayBuffer>>, connectionOptions?: RemoteConnectionOpenOptions): Promise<string | OwnedCouchDBConnection<EntryDoc>>;
|
|
60
64
|
}
|
|
61
65
|
export {};
|