@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
307
|
+
return connection;
|
|
198
308
|
}
|
|
199
309
|
try {
|
|
200
310
|
const info = await db.info();
|
|
201
|
-
return {
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|