@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.
@@ -13,26 +13,84 @@ import { UnresolvedErrorManager } from "./UnresolvedErrorManager.js";
13
13
  import { createInstanceLogFunction, MARK_LOG_NETWORK_ERROR } from "../lib/logUtils.js";
14
14
  import { runWithTrackedPhysicalRequest } from "../lib/remoteActivity.js";
15
15
  import { LiveSyncError } from "../../common/LSError.js";
16
+ function createRemoteConnectionScope(ownerSignal) {
17
+ const controller = new AbortController();
18
+ const combinedSignals = /* @__PURE__ */ new WeakMap();
19
+ let detachOwner = () => {
20
+ };
21
+ const abort = (reason) => {
22
+ detachOwner();
23
+ if (!controller.signal.aborted) {
24
+ controller.abort(reason);
25
+ }
26
+ };
27
+ if (ownerSignal?.aborted) {
28
+ abort(ownerSignal.reason);
29
+ } else if (ownerSignal) {
30
+ const onOwnerAbort = () => abort(ownerSignal.reason);
31
+ ownerSignal.addEventListener("abort", onOwnerAbort, { once: true });
32
+ detachOwner = () => ownerSignal.removeEventListener("abort", onOwnerAbort);
33
+ }
34
+ const combine = (signal) => {
35
+ if (!signal || signal === controller.signal) return controller.signal;
36
+ const cached = combinedSignals.get(signal);
37
+ if (cached) return cached;
38
+ const combined = combineAbortSignals(signal, controller.signal);
39
+ combinedSignals.set(signal, combined);
40
+ return combined;
41
+ };
42
+ return { signal: controller.signal, combine, abort };
43
+ }
44
+ function combineAbortSignals(first, second) {
45
+ if (first === second) return first;
46
+ const abortSignalWithAny = AbortSignal;
47
+ if (typeof abortSignalWithAny.any === "function") {
48
+ return abortSignalWithAny.any([first, second]);
49
+ }
50
+ const controller = new AbortController();
51
+ const listeners = /* @__PURE__ */ new Map();
52
+ const detach = () => {
53
+ for (const [signal, listener] of listeners) {
54
+ signal.removeEventListener("abort", listener);
55
+ }
56
+ listeners.clear();
57
+ };
58
+ const abortFrom = (signal) => {
59
+ detach();
60
+ if (!controller.signal.aborted) {
61
+ controller.abort(signal.reason);
62
+ }
63
+ };
64
+ for (const signal of [first, second]) {
65
+ if (signal.aborted) {
66
+ abortFrom(signal);
67
+ break;
68
+ }
69
+ const listener = () => abortFrom(signal);
70
+ listeners.set(signal, listener);
71
+ signal.addEventListener("abort", listener, { once: true });
72
+ }
73
+ return controller.signal;
74
+ }
75
+ function createCouchDBConnection(db, info, scope) {
76
+ let closePromise;
77
+ return {
78
+ db,
79
+ info,
80
+ close: () => {
81
+ closePromise ??= (async () => {
82
+ scope.abort(new Error("Remote connection closed."));
83
+ await db.close();
84
+ })();
85
+ return closePromise;
86
+ }
87
+ };
88
+ }
16
89
  const FetchMethod = {
17
90
  webCompat: 0,
18
91
  native: 1
19
92
  };
20
93
  class RemoteService extends ServiceBase {
21
- /**
22
- * Connect to the remote database with the provided settings.
23
- * @param uri The URI of the remote database.
24
- * @param auth The authentication credentials for the remote database.
25
- * @param disableRequestURI Whether to disable the request URI.
26
- * @param passphrase The passphrase for the remote database.
27
- * @param useDynamicIterationCount Whether to use dynamic iteration count.
28
- * @param performSetup Whether to perform setup.
29
- * @param skipInfo Whether to skip information retrieval.
30
- * @param compression Whether to enable compression.
31
- * @param customHeaders Custom headers to include in the request.
32
- * @param useRequestAPI Whether to use the request API.
33
- * @param getPBKDF2Salt Function to retrieve the PBKDF2 salt.
34
- * Note that this function is used for CouchDB and compatible only.
35
- */
36
94
  _log;
37
95
  _authHeader = new AuthorizationHeaderGenerator();
38
96
  _APIService;
@@ -101,13 +159,35 @@ class RemoteService extends ServiceBase {
101
159
  return response;
102
160
  });
103
161
  }
104
- async connect(uri, auth, disableRequestURI, passphrase, useDynamicIterationCount, performSetup, skipInfo, compression, customHeaders, useRequestAPI, getPBKDF2Salt) {
162
+ /**
163
+ * Opens a remote CouchDB connection with request cancellation bound to its
164
+ * owner.
165
+ *
166
+ * @param uri - The URI of the remote database.
167
+ * @param auth - Authentication credentials for the remote database.
168
+ * @param disableRequestURI - Whether request URI handling is disabled.
169
+ * @param passphrase - The database encryption passphrase, or `false` when
170
+ * encryption is disabled.
171
+ * @param useDynamicIterationCount - Whether legacy dynamic iteration counts
172
+ * are enabled.
173
+ * @param performSetup - Whether PouchDB may set up a missing database.
174
+ * @param skipInfo - Whether to omit the opening database information request.
175
+ * @param compression - Whether chunk compression is enabled.
176
+ * @param customHeaders - Additional HTTP headers for every request.
177
+ * @param useRequestAPI - Whether to use the host-native request adapter.
178
+ * @param getPBKDF2Salt - Resolves the PBKDF2 salt when encryption needs it.
179
+ * @param connectionOptions - Ownership and request fallback options.
180
+ * @returns An error description, or the owned connection. The caller must
181
+ * transfer ownership or call `close()`.
182
+ */
183
+ async connect(uri, auth, disableRequestURI, passphrase, useDynamicIterationCount, performSetup, skipInfo, compression, customHeaders, useRequestAPI, getPBKDF2Salt, connectionOptions = {}) {
105
184
  if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
106
185
  if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters.";
107
186
  if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
108
187
  if (!this._APIService.isOnline) {
109
188
  return "Network is offline";
110
189
  }
190
+ const connectionScope = createRemoteConnectionScope(connectionOptions.signal);
111
191
  const conf = {
112
192
  adapter: "http",
113
193
  auth: "username" in auth ? auth : void 0,
@@ -143,14 +223,18 @@ class RemoteService extends ServiceBase {
143
223
  headers.append("authorization", authHeader);
144
224
  }
145
225
  try {
226
+ const signal = connectionScope.combine(opts?.signal);
146
227
  const response = await this.performFetch(
147
228
  requestSrc,
148
- { ...opts, headers },
229
+ { ...opts, headers, signal },
149
230
  useRequestAPI ? FetchMethod.native : FetchMethod.webCompat
150
231
  );
151
232
  return response;
152
233
  } catch (ex) {
153
234
  if (ex instanceof TypeError) {
235
+ if (connectionScope.signal.aborted || connectionOptions.allowNativeFallback === false) {
236
+ throw ex;
237
+ }
154
238
  if (useRequestAPI) {
155
239
  this._log("Failed to request by API.");
156
240
  throw ex;
@@ -176,7 +260,12 @@ class RemoteService extends ServiceBase {
176
260
  } catch (ex) {
177
261
  this._log(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL_VERBOSE);
178
262
  const msg = ex instanceof Error ? `${ex?.name}:${ex?.message}` : ex?.toString();
179
- this.showError(`${MARK_LOG_NETWORK_ERROR}Network Error: Failed to fetch: ${msg}`, LOG_LEVEL_INFO);
263
+ if (!connectionScope.signal.aborted) {
264
+ this.showError(
265
+ `${MARK_LOG_NETWORK_ERROR}Network Error: Failed to fetch: ${msg}`,
266
+ LOG_LEVEL_INFO
267
+ );
268
+ }
180
269
  this._log(ex, LOG_LEVEL_VERBOSE);
181
270
  if (url.toString().indexOf("_bulk_docs") !== -1) {
182
271
  this.last_successful_post = false;
@@ -187,28 +276,44 @@ class RemoteService extends ServiceBase {
187
276
  }
188
277
  };
189
278
  const setting = this._settingService.currentSettings();
190
- const db = new this._pouchDB(uri, conf);
191
- replicationFilter(db, compression);
192
- disableEncryption();
193
- if (passphrase !== "false" && typeof passphrase === "string") {
194
- enableEncryption(db, passphrase, useDynamicIterationCount, false, getPBKDF2Salt, setting.E2EEAlgorithm);
279
+ let db;
280
+ try {
281
+ db = new this._pouchDB(uri, conf);
282
+ } catch (ex) {
283
+ connectionScope.abort(ex);
284
+ throw ex;
285
+ }
286
+ const compatibilityInfo = { db_name: "", doc_count: 0, update_seq: "" };
287
+ const connection = createCouchDBConnection(db, compatibilityInfo, connectionScope);
288
+ const closeAfterConnectionFailure = async () => {
289
+ try {
290
+ await connection.close();
291
+ } catch (closeError) {
292
+ this._log("Failed to close remote database after connection failure.", LOG_LEVEL_INFO);
293
+ this._log(closeError, LOG_LEVEL_VERBOSE);
294
+ }
295
+ };
296
+ try {
297
+ replicationFilter(db, compression);
298
+ disableEncryption();
299
+ if (passphrase !== "false" && typeof passphrase === "string") {
300
+ enableEncryption(db, passphrase, useDynamicIterationCount, false, getPBKDF2Salt, setting.E2EEAlgorithm);
301
+ }
302
+ } catch (ex) {
303
+ await closeAfterConnectionFailure();
304
+ throw ex;
195
305
  }
196
306
  if (skipInfo) {
197
- return { db, info: { db_name: "", doc_count: 0, update_seq: "" } };
307
+ return connection;
198
308
  }
199
309
  try {
200
310
  const info = await db.info();
201
- return { db, info };
311
+ return { ...connection, info };
202
312
  } catch (ex) {
203
313
  const exMsg = ex instanceof Error ? ex : LiveSyncError.fromError(ex);
204
314
  const msg = `${exMsg.name}:${exMsg.message}`;
205
315
  this._log(ex, LOG_LEVEL_VERBOSE);
206
- try {
207
- await db.close();
208
- } catch (closeError) {
209
- this._log("Failed to close remote database after connection failure.", LOG_LEVEL_INFO);
210
- this._log(closeError, LOG_LEVEL_VERBOSE);
211
- }
316
+ await closeAfterConnectionFailure();
212
317
  return msg;
213
318
  }
214
319
  }
@@ -68,11 +68,7 @@ function expandDocumentIDPrefix(id) {
68
68
  const _hashString = memorizeFuncWithLRUCache(async (key) => {
69
69
  const buff = writeString(key);
70
70
  const webcrypto = await getWebCrypto();
71
- let digest = await webcrypto.subtle.digest("SHA-256", buff);
72
- const len = key.length;
73
- for (let i = 0; i < len; i++) {
74
- digest = await webcrypto.subtle.digest("SHA-256", buff);
75
- }
71
+ const digest = await webcrypto.subtle.digest("SHA-256", buff);
76
72
  return uint8ArrayToHexString(new Uint8Array(digest));
77
73
  });
78
74
  function hashString(key) {