@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.
@@ -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 closeRemoteDatabase;
36
- private withRemoteDatabase;
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
- checkReplicationConnectivity(setting: RemoteDBSettings, keepAlive: boolean, skipCheck: boolean, showResult: boolean, ignoreCleanLock?: boolean): Promise<false | {
40
- db: PouchDB.Database<EntryDoc>;
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
- db: PouchDB.Database<EntryDoc>;
53
- info: PouchDB.Core.DatabaseInfo;
54
- }> | "Empty passphrases cannot be used without explicit permission";
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.withRemoteDatabase(d.db, async (db) => this.sendChunks(setting, db, showResult, fromSeq));
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.closeRemoteDatabase(connectivity.db);
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.checkReplicationConnectivity(setting, false, retrying, showResult, ignoreCleanLock);
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.closeRemoteDatabase(db);
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
- // One-shot attempts own their temporary remote handle. Continuous replication
625
- // keeps its handle until the replication controller terminates it.
626
- async closeRemoteDatabase(db) {
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 db.close();
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 withRemoteDatabase(db, task) {
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.closeRemoteDatabase(db);
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
- async checkReplicationConnectivity(setting, keepAlive, skipCheck, showResult, ignoreCleanLock = false) {
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
- const dbRet = await this.connectRemoteCouchDBWithSetting(setting, this.isMobile(), true);
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 = { db: dbRet.db, info: dbRet.info, syncOptionBase, syncOption };
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.closeRemoteDatabase(dbRet.db);
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.closeRemoteDatabase(db);
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.closeRemoteDatabase(con.db);
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.closeRemoteDatabase(con2.db);
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.withRemoteDatabase(dbRet.db, async (db) => {
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.withRemoteDatabase(dbRet.db, async (db) => {
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 _ensureConnection(settings, performSetup = false) {
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.db;
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 ownsConnection = db === void 0;
1003
- const connDB = db ?? await this._ensureConnection(settings);
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 (ownsConnection) {
1013
- await this.closeRemoteDatabase(connDB);
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 ownsConnection = db === void 0;
1027
- const connDB = db ?? await this._ensureConnection(settings, true);
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 (ownsConnection) {
1032
- await this.closeRemoteDatabase(connDB);
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.withRemoteDatabase(ret.db, async (db) => {
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.withRemoteDatabase(db.db, async () => {
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.withRemoteDatabase(dbRet.db, async (db) => {
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.withRemoteDatabase(dbRet.db, async (db) => {
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.withRemoteDatabase(dbRet.db, async (db) => {
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.withRemoteDatabase(dbRet.db, async (db) => (await db.compact({ interval: 1e3 })).ok);
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.withRemoteDatabase(dbRet.db, async (db) => {
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.withRemoteDatabase(dbRet.db, countCompromisedChunks);
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.withRemoteDatabase(dbRet.db, async (db) => {
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
- 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>>): Promise<string | {
139
- db: PouchDB.Database<EntryDoc>;
140
- info: PouchDB.Core.DatabaseInfo;
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
- 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>>): Promise<string | {
57
- db: PouchDB.Database<EntryDoc>;
58
- info: PouchDB.Core.DatabaseInfo;
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 {};