@vrtmrz/livesync-commonlib 0.1.23 → 0.1.25
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.compatibility.d.ts +34 -0
- package/dist/common/models/tweak.compatibility.js +110 -0
- package/dist/common/models/tweak.definition.d.ts +110 -19
- package/dist/common/models/tweak.definition.js +62 -29
- package/dist/common/settingConstants.d.ts +5 -0
- package/dist/common/types.d.ts +6 -3
- package/dist/common/types.js +6 -0
- package/dist/common/utils.js +4 -1
- 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/LiveSyncDBFunctions.d.ts +3 -3
- package/dist/pouchdb/LiveSyncDBFunctions.js +9 -15
- package/dist/replication/CentralCompatibility.d.ts +4 -2
- package/dist/replication/CentralCompatibility.js +5 -3
- package/dist/replication/couchdb/LiveSyncReplicator.js +10 -2
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +2 -2
- package/dist/replication/journal/LiveSyncJournalReplicator.js +18 -4
- package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +22 -1
- package/dist/replication/trystero/P2PRoomSessionOwner.js +193 -13
- package/dist/replication/trystero/TrysteroReplicator.js +12 -7
- 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/services/base/IService.d.ts +3 -3
- package/dist/services/base/ReplicationService.d.ts +2 -2
- package/dist/services/base/ReplicationService.js +14 -11
- package/dist/services/base/SettingService.d.ts +1 -0
- package/dist/services/base/SettingService.js +37 -9
- package/dist/services/base/TweakValueService.d.ts +5 -3
- package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +5 -5
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +2 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/development.md +3 -3
- package/docs/p2p-transport-lifecycle.md +86 -4
- package/docs/proven-in-use.md +32 -0
- package/docs/settings-lifecycle.md +20 -1
- package/package.json +1 -1
|
@@ -2,8 +2,12 @@ import { Logger } from "../../common/logger.js";
|
|
|
2
2
|
import {
|
|
3
3
|
LOG_LEVEL_NOTICE,
|
|
4
4
|
LOG_LEVEL_VERBOSE,
|
|
5
|
-
SETTING_KEY_P2P_DEVICE_NAME
|
|
5
|
+
SETTING_KEY_P2P_DEVICE_NAME,
|
|
6
|
+
hasManagedP2PTurnConfiguration,
|
|
7
|
+
omitP2PRuntimeSettings
|
|
6
8
|
} from "../../common/types.js";
|
|
9
|
+
import { P2PConnectionPaths } from "../../common/models/setting.const.js";
|
|
10
|
+
import { normaliseP2PConnectionPath } from "../../common/models/setting.p2p.js";
|
|
7
11
|
import { P2PRoomSession } from "./P2PRoomSession.js";
|
|
8
12
|
import { P2PAutomationCoordinator } from "./P2PAutomationCoordinator.js";
|
|
9
13
|
import { PEER_REPLICATION_READINESS } from "../ReplicatorProvider.js";
|
|
@@ -12,19 +16,28 @@ import {
|
|
|
12
16
|
ACTIVE_P2P_RELAY_BINDING_CONFLICT,
|
|
13
17
|
activeP2PRelayBindingCovers
|
|
14
18
|
} from "./P2PConnectionProbeAdmission.js";
|
|
19
|
+
class StaleRoomReconciliationError extends Error {
|
|
20
|
+
}
|
|
21
|
+
const P2P_ROOM_OPEN_TIMEOUT_MS = 3e4;
|
|
22
|
+
const P2P_SETTINGS_PREPARATION_TIMEOUT_MS = 3e4;
|
|
23
|
+
const P2P_ICE_SERVER_EXPIRY_MARGIN_MS = 3e4;
|
|
15
24
|
class P2PRoomSessionOwner {
|
|
16
|
-
constructor(env, createSession = (sessionEnv) => new P2PRoomSession(sessionEnv)) {
|
|
25
|
+
constructor(env, createSession = (sessionEnv) => new P2PRoomSession(sessionEnv), options = {}) {
|
|
17
26
|
this.env = env;
|
|
18
27
|
this.createSession = createSession;
|
|
28
|
+
this.options = options;
|
|
19
29
|
}
|
|
20
30
|
env;
|
|
21
31
|
createSession;
|
|
32
|
+
options;
|
|
22
33
|
current;
|
|
23
34
|
lifecycleOperation = Promise.resolve();
|
|
24
35
|
persistentDemands = /* @__PURE__ */ new Set();
|
|
25
36
|
finiteDemands = /* @__PURE__ */ new Set();
|
|
26
37
|
automationCoordinator = new P2PAutomationCoordinator();
|
|
27
38
|
activeBinding;
|
|
39
|
+
pendingPreparation;
|
|
40
|
+
runtimeGeneration = 0;
|
|
28
41
|
get currentSession() {
|
|
29
42
|
return this.current;
|
|
30
43
|
}
|
|
@@ -52,6 +65,7 @@ class P2PRoomSessionOwner {
|
|
|
52
65
|
if (active && !enabled) {
|
|
53
66
|
Logger(this.env.services.context.translate("P2P.NotEnabled"), LOG_LEVEL_NOTICE);
|
|
54
67
|
}
|
|
68
|
+
this.fencePreparationForCurrentSettings(enabled);
|
|
55
69
|
await this.reconcileTransport();
|
|
56
70
|
}
|
|
57
71
|
/**
|
|
@@ -66,6 +80,7 @@ class P2PRoomSessionOwner {
|
|
|
66
80
|
}
|
|
67
81
|
const demand = /* @__PURE__ */ Symbol("p2p-finite-room-demand");
|
|
68
82
|
this.finiteDemands.add(demand);
|
|
83
|
+
this.fencePreparationForCurrentSettings(true);
|
|
69
84
|
try {
|
|
70
85
|
await this.reconcileTransport();
|
|
71
86
|
if (!this.finiteDemands.has(demand)) {
|
|
@@ -116,7 +131,7 @@ class P2PRoomSessionOwner {
|
|
|
116
131
|
return;
|
|
117
132
|
}
|
|
118
133
|
const binding = this.getEffectiveBinding();
|
|
119
|
-
if (this.current?.host.isServing && this.bindingsMatch(this.activeBinding, binding)) {
|
|
134
|
+
if (this.current?.host.isServing && this.bindingsMatch(this.activeBinding, binding) && this.connectionSettingsRemainUsable(this.activeBinding?.settings)) {
|
|
120
135
|
this.reconcileCurrentSessionPolicy(this.current);
|
|
121
136
|
Logger("P2P replicator is already open.");
|
|
122
137
|
return;
|
|
@@ -125,34 +140,55 @@ class P2PRoomSessionOwner {
|
|
|
125
140
|
await this.closeTransport();
|
|
126
141
|
}
|
|
127
142
|
let candidate;
|
|
143
|
+
const operationGeneration = this.runtimeGeneration;
|
|
128
144
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
const connectionSettings = await this.prepareConnectionSettings(binding, operationGeneration);
|
|
146
|
+
this.assertReconciliationCurrent(binding, operationGeneration, connectionSettings);
|
|
147
|
+
const connectionBinding = { ...binding, settings: connectionSettings };
|
|
148
|
+
candidate = this.createSession(this.buildSessionEnv(connectionBinding));
|
|
149
|
+
if (hasManagedP2PTurnConfiguration(binding.settings)) {
|
|
150
|
+
await this.withRoomOpenDeadline(candidate.open());
|
|
151
|
+
} else {
|
|
152
|
+
await candidate.open();
|
|
153
|
+
}
|
|
131
154
|
if (!candidate.host.isServing) {
|
|
132
155
|
throw new Error("The P2P room did not start serving.");
|
|
133
156
|
}
|
|
134
157
|
const currentBinding = this.getEffectiveBinding();
|
|
135
|
-
if (!currentBinding.enabled || !this.hasRoomDemand() || !this.bindingsMatch(binding, currentBinding)) {
|
|
158
|
+
if (!currentBinding.enabled || !this.hasRoomDemand() || !this.bindingsMatch(binding, currentBinding) || operationGeneration !== this.runtimeGeneration || !this.connectionSettingsRemainUsable(connectionSettings)) {
|
|
136
159
|
await candidate.retire();
|
|
137
160
|
return;
|
|
138
161
|
}
|
|
139
162
|
this.current = candidate;
|
|
140
|
-
this.activeBinding =
|
|
163
|
+
this.activeBinding = connectionBinding;
|
|
141
164
|
this.reconcileCurrentSessionPolicy(candidate);
|
|
142
165
|
} catch (error) {
|
|
143
166
|
await candidate?.retire(error).catch((retirementError) => {
|
|
144
|
-
|
|
167
|
+
if (hasManagedP2PTurnConfiguration(binding.settings)) {
|
|
168
|
+
Logger("The managed P2P room could not be retired cleanly.", LOG_LEVEL_VERBOSE);
|
|
169
|
+
} else {
|
|
170
|
+
Logger(retirementError, LOG_LEVEL_VERBOSE);
|
|
171
|
+
}
|
|
145
172
|
});
|
|
146
|
-
Logger(error instanceof Error ? error.message : "Error while opening P2P connection", LOG_LEVEL_NOTICE);
|
|
147
|
-
Logger(error, LOG_LEVEL_VERBOSE);
|
|
148
173
|
this.current = void 0;
|
|
149
174
|
this.activeBinding = void 0;
|
|
175
|
+
if (error instanceof StaleRoomReconciliationError || operationGeneration !== this.runtimeGeneration) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (hasManagedP2PTurnConfiguration(binding.settings)) {
|
|
179
|
+
Logger("The managed P2P room could not be prepared or opened.", LOG_LEVEL_NOTICE);
|
|
180
|
+
Logger("Managed P2P room failure details were omitted.", LOG_LEVEL_VERBOSE);
|
|
181
|
+
throw new Error("The managed P2P room could not be prepared or opened.");
|
|
182
|
+
}
|
|
183
|
+
Logger(error instanceof Error ? error.message : "Error while opening P2P connection", LOG_LEVEL_NOTICE);
|
|
184
|
+
Logger(error, LOG_LEVEL_VERBOSE);
|
|
150
185
|
}
|
|
151
186
|
});
|
|
152
187
|
}
|
|
153
188
|
async close() {
|
|
154
189
|
this.persistentDemands.clear();
|
|
155
190
|
this.finiteDemands.clear();
|
|
191
|
+
this.invalidatePreparation();
|
|
156
192
|
await this.enqueueLifecycleOperation(async () => {
|
|
157
193
|
await this.closeTransport();
|
|
158
194
|
});
|
|
@@ -179,11 +215,153 @@ class P2PRoomSessionOwner {
|
|
|
179
215
|
bindingsMatch(current, candidate) {
|
|
180
216
|
return current?.database === candidate.database && current.signature === candidate.signature;
|
|
181
217
|
}
|
|
218
|
+
connectionSettingsRemainUsable(settings) {
|
|
219
|
+
if (!settings) return false;
|
|
220
|
+
const managed = hasManagedP2PTurnConfiguration(settings);
|
|
221
|
+
const iceServers = settings.P2P_iceServers;
|
|
222
|
+
if (iceServers === void 0) return !managed;
|
|
223
|
+
if (!this.hasUsableIceServer(iceServers)) return false;
|
|
224
|
+
const hasTurnServer = this.hasUsableTurnServer(iceServers);
|
|
225
|
+
if (managed && !hasTurnServer) return false;
|
|
226
|
+
if (normaliseP2PConnectionPath(settings.P2P_connectionPath) === P2PConnectionPaths.Relay && !hasTurnServer) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const expiresAt = settings.P2P_iceServersExpiresAt;
|
|
230
|
+
if (expiresAt === void 0) return !managed;
|
|
231
|
+
return Number.isFinite(expiresAt) && expiresAt > Date.now() + P2P_ICE_SERVER_EXPIRY_MARGIN_MS;
|
|
232
|
+
}
|
|
233
|
+
fencePreparationForCurrentSettings(enabled) {
|
|
234
|
+
if (!enabled) {
|
|
235
|
+
this.invalidatePreparation();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const pendingChanged = this.pendingPreparation !== void 0 && this.pendingPreparation.signature !== this.getEffectiveBinding().signature;
|
|
239
|
+
if (pendingChanged) this.invalidatePreparation();
|
|
240
|
+
}
|
|
241
|
+
invalidatePreparation() {
|
|
242
|
+
this.runtimeGeneration += 1;
|
|
243
|
+
const pending = this.pendingPreparation;
|
|
244
|
+
this.pendingPreparation = void 0;
|
|
245
|
+
if (pending && !pending.controller.signal.aborted) {
|
|
246
|
+
pending.controller.abort(new Error("The P2P settings preparation was cancelled."));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async prepareConnectionSettings(binding, operationGeneration) {
|
|
250
|
+
const managed = hasManagedP2PTurnConfiguration(binding.settings);
|
|
251
|
+
const prepare = this.options.prepareP2PSettings;
|
|
252
|
+
if (!prepare) {
|
|
253
|
+
if (managed) throw new Error("The selected managed TURN configuration is not supported by this host.");
|
|
254
|
+
return binding.settings;
|
|
255
|
+
}
|
|
256
|
+
const controller = new AbortController();
|
|
257
|
+
const pending = {
|
|
258
|
+
signature: binding.signature,
|
|
259
|
+
controller
|
|
260
|
+
};
|
|
261
|
+
this.pendingPreparation = pending;
|
|
262
|
+
try {
|
|
263
|
+
const prepared = await this.prepareWithDeadline(prepare, binding.settings, controller);
|
|
264
|
+
if (prepared.P2P_iceServers === void 0 && prepared.P2P_iceServersExpiresAt === void 0) {
|
|
265
|
+
if (managed) throw new Error("The prepared managed TURN settings do not include ICE servers.");
|
|
266
|
+
return binding.settings;
|
|
267
|
+
}
|
|
268
|
+
const iceServers = this.copyPreparedIceServers(prepared.P2P_iceServers);
|
|
269
|
+
const expiresAt = prepared.P2P_iceServersExpiresAt;
|
|
270
|
+
const connectionSettings = {
|
|
271
|
+
...binding.settings,
|
|
272
|
+
P2P_iceServers: iceServers,
|
|
273
|
+
P2P_iceServersExpiresAt: expiresAt
|
|
274
|
+
};
|
|
275
|
+
if (!this.connectionSettingsRemainUsable(connectionSettings)) {
|
|
276
|
+
throw new Error("The prepared managed TURN settings are missing usable ICE servers or expiry.");
|
|
277
|
+
}
|
|
278
|
+
this.assertReconciliationCurrent(binding, operationGeneration, connectionSettings);
|
|
279
|
+
return connectionSettings;
|
|
280
|
+
} finally {
|
|
281
|
+
if (this.pendingPreparation === pending) {
|
|
282
|
+
this.pendingPreparation = void 0;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
prepareWithDeadline(prepare, settings, controller) {
|
|
287
|
+
const preparation = Promise.resolve().then(() => prepare(Object.freeze({ ...settings }), controller.signal));
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
let settled = false;
|
|
290
|
+
const finish = (operation) => {
|
|
291
|
+
if (settled) return;
|
|
292
|
+
settled = true;
|
|
293
|
+
globalThis.clearTimeout(timeout);
|
|
294
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
295
|
+
operation();
|
|
296
|
+
};
|
|
297
|
+
const onAbort = () => finish(
|
|
298
|
+
() => reject(
|
|
299
|
+
controller.signal.reason instanceof Error ? controller.signal.reason : new Error("The P2P settings preparation was cancelled.")
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
const timeout = globalThis.setTimeout(() => {
|
|
303
|
+
controller.abort(new Error("The P2P settings preparation timed out."));
|
|
304
|
+
}, P2P_SETTINGS_PREPARATION_TIMEOUT_MS);
|
|
305
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
306
|
+
if (controller.signal.aborted) onAbort();
|
|
307
|
+
preparation.then(
|
|
308
|
+
(prepared) => finish(() => resolve(prepared)),
|
|
309
|
+
() => finish(() => reject(new Error("The P2P settings preparation failed.")))
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
copyPreparedIceServers(iceServers) {
|
|
314
|
+
if (!Array.isArray(iceServers) || iceServers.length === 0) return [];
|
|
315
|
+
return iceServers.map((server) => ({
|
|
316
|
+
urls: typeof server.urls === "string" ? server.urls : [...server.urls],
|
|
317
|
+
...server.username === void 0 ? {} : { username: server.username },
|
|
318
|
+
...server.credential === void 0 ? {} : { credential: server.credential }
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
hasUsableIceServer(iceServers) {
|
|
322
|
+
return Array.isArray(iceServers) && iceServers.some((server) => {
|
|
323
|
+
const urls = typeof server?.urls === "string" ? [server.urls] : server?.urls;
|
|
324
|
+
return Array.isArray(urls) && urls.some((url) => typeof url === "string" && url.trim().length > 0);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
hasUsableTurnServer(iceServers) {
|
|
328
|
+
return Array.isArray(iceServers) && iceServers.some((server) => {
|
|
329
|
+
const urls = typeof server?.urls === "string" ? [server.urls] : server?.urls;
|
|
330
|
+
return Array.isArray(urls) && urls.some((url) => typeof url === "string" && /^turns?:/iu.test(url));
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async withRoomOpenDeadline(opening) {
|
|
334
|
+
let timeout;
|
|
335
|
+
try {
|
|
336
|
+
await Promise.race([
|
|
337
|
+
opening,
|
|
338
|
+
new Promise((_resolve, reject) => {
|
|
339
|
+
timeout = globalThis.setTimeout(
|
|
340
|
+
() => reject(new Error("The P2P room opening timed out.")),
|
|
341
|
+
P2P_ROOM_OPEN_TIMEOUT_MS
|
|
342
|
+
);
|
|
343
|
+
})
|
|
344
|
+
]);
|
|
345
|
+
} finally {
|
|
346
|
+
if (timeout !== void 0) globalThis.clearTimeout(timeout);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
assertReconciliationCurrent(binding, operationGeneration, connectionSettings) {
|
|
350
|
+
if (operationGeneration !== this.runtimeGeneration || !this.hasRoomDemand()) {
|
|
351
|
+
throw new StaleRoomReconciliationError();
|
|
352
|
+
}
|
|
353
|
+
const currentBinding = this.getEffectiveBinding();
|
|
354
|
+
if (!currentBinding.enabled) throw new StaleRoomReconciliationError();
|
|
355
|
+
if (binding && !this.bindingsMatch(binding, currentBinding)) throw new StaleRoomReconciliationError();
|
|
356
|
+
if (connectionSettings && !this.connectionSettingsRemainUsable(connectionSettings)) {
|
|
357
|
+
throw new StaleRoomReconciliationError();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
182
360
|
/** Capture immutable session inputs while keeping policy live. */
|
|
183
361
|
getEffectiveBinding() {
|
|
184
|
-
const settings = {
|
|
362
|
+
const settings = omitP2PRuntimeSettings({
|
|
185
363
|
...this.env.services.setting.currentSettings()
|
|
186
|
-
};
|
|
364
|
+
});
|
|
187
365
|
const deviceName = this.env.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) || this.env.services.vault.getVaultName();
|
|
188
366
|
const database = this.env.services.database.localDatabase.localDatabase;
|
|
189
367
|
this.automationCoordinator.reconcileIdentity(
|
|
@@ -245,5 +423,7 @@ class P2PRoomSessionOwner {
|
|
|
245
423
|
}
|
|
246
424
|
}
|
|
247
425
|
export {
|
|
248
|
-
P2PRoomSessionOwner
|
|
426
|
+
P2PRoomSessionOwner,
|
|
427
|
+
P2P_ICE_SERVER_EXPIRY_MARGIN_MS,
|
|
428
|
+
P2P_SETTINGS_PREPARATION_TIMEOUT_MS
|
|
249
429
|
};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
TweakValuesShouldMatchedTemplate,
|
|
3
|
+
omitP2PRuntimeSettings
|
|
4
|
+
} from "../../common/types.js";
|
|
5
|
+
import { assessTweakCompatibility } from "../../common/models/tweak.compatibility.js";
|
|
2
6
|
import {
|
|
3
7
|
LOG_LEVEL_INFO,
|
|
4
8
|
LOG_LEVEL_NOTICE,
|
|
@@ -17,7 +21,6 @@ import {
|
|
|
17
21
|
} from "./TrysteroReplicatorP2PServer.js";
|
|
18
22
|
import { encryptWithEphemeralSalt, decryptWithEphemeralSalt } from "octagonal-wheels/encryption/hkdf";
|
|
19
23
|
import { sha1 } from "octagonal-wheels/hash/purejs";
|
|
20
|
-
import { isObjectDifferent } from "octagonal-wheels/object";
|
|
21
24
|
import { getRelaySockets, pauseRelayReconnection, resumeRelayReconnection } from "@trystero-p2p/nostr";
|
|
22
25
|
import { P2PAutomationCoordinator } from "./P2PAutomationCoordinator.js";
|
|
23
26
|
import { compatGlobal } from "../../common/coreEnvFunctions.js";
|
|
@@ -253,12 +256,15 @@ class TrysteroReplicator {
|
|
|
253
256
|
);
|
|
254
257
|
});
|
|
255
258
|
const setting = {
|
|
256
|
-
...this.settings,
|
|
259
|
+
...omitP2PRuntimeSettings(this.settings),
|
|
257
260
|
configPassphraseStore: "",
|
|
258
261
|
encryptedCouchDBConnection: "",
|
|
259
262
|
encryptedPassphrase: "",
|
|
260
263
|
pluginSyncExtendedSetting: {}
|
|
261
264
|
};
|
|
265
|
+
delete setting.P2P_managedType;
|
|
266
|
+
delete setting.P2P_managedId;
|
|
267
|
+
delete setting.P2P_managedToken;
|
|
262
268
|
if (!passphrase || passphrase.trim() == "") {
|
|
263
269
|
Logger(
|
|
264
270
|
"Passphrase is required to transfer the configuration. The peer cannot be decrypt the config\nIf you repeatedly receive unintended configuration-sharing requests, change the RPC channel immediately. It allows you to leave the connection and disappear, while they are trying brute force attack for the decoy on their local.",
|
|
@@ -700,7 +706,7 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
|
|
|
700
706
|
const decryptedConfig = JSON.parse(
|
|
701
707
|
await decrypt(encryptedConfig, passphrase)
|
|
702
708
|
);
|
|
703
|
-
return decryptedConfig;
|
|
709
|
+
return omitP2PRuntimeSettings(decryptedConfig);
|
|
704
710
|
} catch (e) {
|
|
705
711
|
Logger("Error while decrypting the configuration", LOG_LEVEL_NOTICE);
|
|
706
712
|
Logger(e, LOG_LEVEL_VERBOSE);
|
|
@@ -728,9 +734,6 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
|
|
|
728
734
|
const tweakValues = await connection.invokeRemoteObjectFunction("getTweakSettings", [this.server.serverPeerId], 5e3, signal);
|
|
729
735
|
if (signal?.aborted) return false;
|
|
730
736
|
const thisTweakValues = await this.getTweakSettings("");
|
|
731
|
-
if (!isObjectDifferent(thisTweakValues, tweakValues)) {
|
|
732
|
-
return true;
|
|
733
|
-
}
|
|
734
737
|
if (thisTweakValues.passphrase !== tweakValues.passphrase) {
|
|
735
738
|
Logger(
|
|
736
739
|
"Replication cancelled: Passphrase is not matched\nCannot replicate to a remote database until the problem is resolved.",
|
|
@@ -738,6 +741,8 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
|
|
|
738
741
|
);
|
|
739
742
|
return false;
|
|
740
743
|
}
|
|
744
|
+
const tweakAssessment = assessTweakCompatibility(thisTweakValues, tweakValues);
|
|
745
|
+
if (!tweakAssessment.representationDiffers) return true;
|
|
741
746
|
Logger(
|
|
742
747
|
"Some mismatched configuration have been detected... Please check settings for efficient replication.",
|
|
743
748
|
logLevel
|
|
@@ -121,6 +121,7 @@ export declare class TrysteroReplicatorP2PServer {
|
|
|
121
121
|
onAfterJoinRoom(): void;
|
|
122
122
|
startService(bindings?: BindableObject[], beforeAdvertisement?: () => void): Promise<void>;
|
|
123
123
|
start(bindings?: BindableObject[], beforeAdvertisement?: () => void): Promise<void>;
|
|
124
|
+
private logTransportError;
|
|
124
125
|
/**
|
|
125
126
|
* @deprecated Use serveFunction or serveObject instead. This is only for backward compatibility and may be removed in the future.
|
|
126
127
|
* @param type
|
|
@@ -79,7 +79,7 @@ class TrysteroReplicatorP2PServer {
|
|
|
79
79
|
`Some error has been occurred while leaving the room, but possibly can be ignored`,
|
|
80
80
|
LOG_LEVEL_VERBOSE
|
|
81
81
|
);
|
|
82
|
-
|
|
82
|
+
this.logTransportError(ex);
|
|
83
83
|
}
|
|
84
84
|
this._room = void 0;
|
|
85
85
|
this._env.events.emitEvent(EVENT_P2P_DISCONNECTED);
|
|
@@ -97,7 +97,7 @@ class TrysteroReplicatorP2PServer {
|
|
|
97
97
|
this._rpcRoom = void 0;
|
|
98
98
|
} catch (ex) {
|
|
99
99
|
Logger(`Some error has been occurred while shutting down the server`, LOG_LEVEL_INFO);
|
|
100
|
-
|
|
100
|
+
this.logTransportError(ex);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
/** Release subscriptions whose lifetime is the host object rather than one transport join. */
|
|
@@ -459,7 +459,7 @@ You can chose as follows:
|
|
|
459
459
|
handshakeTimeoutMs: 3e4,
|
|
460
460
|
onJoinError: (error) => {
|
|
461
461
|
Logger("Some peer Failed to join Trystero room");
|
|
462
|
-
|
|
462
|
+
this.logTransportError(error);
|
|
463
463
|
}
|
|
464
464
|
});
|
|
465
465
|
await this.setRoom(room);
|
|
@@ -468,6 +468,13 @@ You can chose as follows:
|
|
|
468
468
|
void this.dispatchConnectionStatus();
|
|
469
469
|
await this.startService(bindings, beforeAdvertisement);
|
|
470
470
|
}
|
|
471
|
+
logTransportError(error) {
|
|
472
|
+
if (this.settings.P2P_iceServers !== void 0) {
|
|
473
|
+
Logger("Managed P2P transport error details were omitted.", LOG_LEVEL_VERBOSE);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
Logger(error, LOG_LEVEL_VERBOSE);
|
|
477
|
+
}
|
|
471
478
|
/**
|
|
472
479
|
* @deprecated Use serveFunction or serveObject instead. This is only for backward compatibility and may be removed in the future.
|
|
473
480
|
* @param type
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import { P2PConnectionPaths } from "../../common/models/setting.const.js";
|
|
2
2
|
import {
|
|
3
3
|
hasValidP2PTurnServerUrl,
|
|
4
|
+
hasManagedP2PTurnConfiguration,
|
|
4
5
|
normaliseP2PConnectionPath,
|
|
5
6
|
normaliseP2PMaxWirePayloadBytes,
|
|
6
7
|
splitP2PRelayUrls,
|
|
7
8
|
splitP2PTurnServerUrls
|
|
8
9
|
} from "../../common/models/setting.p2p.js";
|
|
9
10
|
function getP2PReplicatorConfigurationIdentity(settings) {
|
|
11
|
+
const managedTurnSelected = hasManagedP2PTurnConfiguration(settings);
|
|
10
12
|
const turnServerValue = settings.P2P_turnServers ?? "";
|
|
11
13
|
const turnServers = splitP2PTurnServerUrls(turnServerValue);
|
|
12
14
|
const configuredPath = normaliseP2PConnectionPath(settings.P2P_connectionPath);
|
|
13
|
-
const effectivePath = configuredPath === P2PConnectionPaths.Relay && hasValidP2PTurnServerUrl(turnServerValue) ? P2PConnectionPaths.Relay : P2PConnectionPaths.Automatic;
|
|
14
|
-
const turnAuthentication = turnServers.length > 0 ? [settings.P2P_turnUsername, settings.P2P_turnCredential] : void 0;
|
|
15
|
+
const effectivePath = configuredPath === P2PConnectionPaths.Relay && (managedTurnSelected || hasValidP2PTurnServerUrl(turnServerValue)) ? P2PConnectionPaths.Relay : P2PConnectionPaths.Automatic;
|
|
16
|
+
const turnAuthentication = !managedTurnSelected && turnServers.length > 0 ? [settings.P2P_turnUsername, settings.P2P_turnCredential] : void 0;
|
|
15
17
|
return JSON.stringify([
|
|
16
18
|
"p2p",
|
|
17
19
|
settings.P2P_AppID || "self-hosted-livesync",
|
|
18
20
|
settings.P2P_roomID,
|
|
19
21
|
settings.P2P_passphrase,
|
|
20
22
|
splitP2PRelayUrls(settings.P2P_relays),
|
|
21
|
-
turnServers,
|
|
23
|
+
managedTurnSelected ? void 0 : turnServers,
|
|
22
24
|
turnAuthentication,
|
|
25
|
+
managedTurnSelected ? [settings.P2P_managedType, settings.P2P_managedId, settings.P2P_managedToken] : void 0,
|
|
23
26
|
normaliseP2PMaxWirePayloadBytes(settings.P2P_maxWirePayloadBytes),
|
|
24
27
|
effectivePath,
|
|
25
28
|
settings.P2P_useDiagRTC ?? false
|
|
@@ -2,6 +2,7 @@ import type { NecessaryServices } from "../../interfaces/ServiceModule.js";
|
|
|
2
2
|
import type { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator.js";
|
|
3
3
|
import { type UseP2PReplicatorResult } from "./UseP2PReplicatorResult.js";
|
|
4
4
|
import { type P2PServiceViews } from "../../p2p/P2PService.js";
|
|
5
|
+
import type { PrepareP2PSettings } from "../../p2p/P2PService.js";
|
|
5
6
|
/**
|
|
6
7
|
* Factory type: given the compatibility Replicator and the stable service
|
|
7
8
|
* views, returns the openReplicationUI callback for that instance.
|
|
@@ -14,6 +15,10 @@ import { type P2PServiceViews } from "../../p2p/P2PService.js";
|
|
|
14
15
|
export type OpenReplicationUIFactory = (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) => (showResult: boolean) => Promise<boolean | void>;
|
|
15
16
|
/** Same shape as OpenReplicationUIFactory, used for the rebuild/replicateAllFromServer flow. */
|
|
16
17
|
export type OpenRebuildUIFactory = OpenReplicationUIFactory;
|
|
18
|
+
/** Optional host integrations for the P2P room lifecycle. */
|
|
19
|
+
export interface P2PReplicatorFeatureOptions {
|
|
20
|
+
readonly prepareP2PSettings?: PrepareP2PSettings;
|
|
21
|
+
}
|
|
17
22
|
/**
|
|
18
23
|
* Compose one private P2P service context and register non-owning active
|
|
19
24
|
* provider adapters over it.
|
|
@@ -23,4 +28,4 @@ export type OpenRebuildUIFactory = OpenReplicationUIFactory;
|
|
|
23
28
|
* migrated to those views.
|
|
24
29
|
* @param host
|
|
25
30
|
*/
|
|
26
|
-
export declare function useP2PReplicatorFeature(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator" | "remote", never>, openReplicationUIFactory?: OpenReplicationUIFactory, openRebuildUIFactory?: OpenRebuildUIFactory): UseP2PReplicatorResult;
|
|
31
|
+
export declare function useP2PReplicatorFeature(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator" | "remote", never>, openReplicationUIFactory?: OpenReplicationUIFactory, openRebuildUIFactory?: OpenRebuildUIFactory, options?: P2PReplicatorFeatureOptions): UseP2PReplicatorResult;
|
|
@@ -15,10 +15,13 @@ import {
|
|
|
15
15
|
supportedStopActiveTransfer
|
|
16
16
|
} from "../index.js";
|
|
17
17
|
import { getP2PReplicatorConfigurationIdentity } from "./p2pReplicatorConfigurationIdentity.js";
|
|
18
|
-
function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFactory) {
|
|
19
|
-
const service = createP2PService(
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFactory, options = {}) {
|
|
19
|
+
const service = createP2PService(
|
|
20
|
+
{
|
|
21
|
+
services: host.services
|
|
22
|
+
},
|
|
23
|
+
options
|
|
24
|
+
);
|
|
22
25
|
const replicator = service.compatibilityReplicator;
|
|
23
26
|
const { views, lifecycle } = service;
|
|
24
27
|
const configureReplicator = (instance) => {
|
|
@@ -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() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FetchHttpHandler } from "@smithy/fetch-http-handler";
|
|
2
2
|
import { type LOG_LEVEL } from "octagonal-wheels/common/logger";
|
|
3
|
-
import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, RemotePreferredTweakResult, SettingsMigrationState, TweakValues, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
|
|
3
|
+
import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, RemotePreferredTweakResult, SettingsMigrationState, TweakAssessment, TweakValues, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
|
|
4
4
|
import type { LiveSyncLocalDB } from "../../pouchdb/LiveSyncLocalDB.js";
|
|
5
5
|
import type { ReplicatorInstance } from "../../replication/ReplicatorInstance.js";
|
|
6
6
|
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
|
@@ -335,8 +335,8 @@ export interface ISettingService {
|
|
|
335
335
|
}
|
|
336
336
|
export interface ITweakValueService {
|
|
337
337
|
fetchRemotePreferred(trialSetting: RemoteDBSettings): Promise<RemotePreferredTweakResult>;
|
|
338
|
-
checkAndAskResolvingMismatched(preferred: Partial<TweakValues
|
|
339
|
-
askResolvingMismatched(preferredSource: TweakValues, updatePreferredRemote?: (setting: ObsidianLiveSyncSettings) => Promise<boolean
|
|
338
|
+
checkAndAskResolvingMismatched(preferred: Partial<TweakValues>, assessment?: TweakAssessment): Promise<[TweakValues | boolean, boolean]>;
|
|
339
|
+
askResolvingMismatched(preferredSource: TweakValues, updatePreferredRemote?: (setting: ObsidianLiveSyncSettings) => Promise<boolean>, assessment?: TweakAssessment): Promise<"OK" | "CHECKAGAIN" | "IGNORE">;
|
|
340
340
|
checkAndAskUseRemoteConfiguration(settings: RemoteDBSettings): Promise<{
|
|
341
341
|
result: false | TweakValues;
|
|
342
342
|
requireFetch: boolean;
|
|
@@ -117,8 +117,8 @@ export declare abstract class ReplicationService<T extends ServiceContext = Serv
|
|
|
117
117
|
*
|
|
118
118
|
* Each attempt owns a separate reservation. Compatibility recovery runs
|
|
119
119
|
* between those reservations, and a retry is admitted only when the exact
|
|
120
|
-
* first-attempt context remains active.
|
|
121
|
-
*
|
|
120
|
+
* first-attempt context remains active. Each attempt uses a detached settings
|
|
121
|
+
* snapshot, and a compatibility retry captures its snapshot after recovery.
|
|
122
122
|
*/
|
|
123
123
|
private performDirectionalReplication;
|
|
124
124
|
replicateAllToRemote(showingNotice?: boolean): Promise<boolean>;
|
|
@@ -329,40 +329,43 @@ class ReplicationService extends ServiceBase {
|
|
|
329
329
|
*
|
|
330
330
|
* Each attempt owns a separate reservation. Compatibility recovery runs
|
|
331
331
|
* between those reservations, and a retry is admitted only when the exact
|
|
332
|
-
* first-attempt context remains active.
|
|
333
|
-
*
|
|
332
|
+
* first-attempt context remains active. Each attempt uses a detached settings
|
|
333
|
+
* snapshot, and a compatibility retry captures its snapshot after recovery.
|
|
334
334
|
*/
|
|
335
335
|
async performDirectionalReplication(direction, showingNotice, admission = DIRECTIONAL_REPLICATION_ADMISSION.ORDINARY) {
|
|
336
336
|
if (direction === DIRECTIONAL_REPLICATION.UPLOAD && !await this.onBeforeReplicate(showingNotice)) {
|
|
337
337
|
this._log(this.context.translate("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
|
|
338
338
|
return false;
|
|
339
339
|
}
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
340
|
+
const captureSetting = () => {
|
|
341
|
+
const detachedSetting = asCopy(this.settingService.currentSettings());
|
|
342
|
+
if (admission === DIRECTIONAL_REPLICATION_ADMISSION.EXPLICIT_REBUILD) {
|
|
343
|
+
detachedSetting.versionUpFlash = "";
|
|
344
|
+
}
|
|
345
|
+
return Object.freeze(detachedSetting);
|
|
346
|
+
};
|
|
347
|
+
const setting = captureSetting();
|
|
345
348
|
let expectedContext;
|
|
346
|
-
const run = async () => {
|
|
349
|
+
const run = async (attemptSetting) => {
|
|
347
350
|
const admitted = await this.replicatorService.runWithActiveReplicatorContext((context) => {
|
|
348
351
|
if (expectedContext && context !== expectedContext) {
|
|
349
352
|
return replicationBlocked("not-ready");
|
|
350
353
|
}
|
|
351
354
|
expectedContext ??= context;
|
|
352
|
-
return runDirectionalReplication(context,
|
|
355
|
+
return runDirectionalReplication(context, attemptSetting, direction, showingNotice);
|
|
353
356
|
});
|
|
354
357
|
if (admitted) return admitted;
|
|
355
358
|
this._log(`Active replicator not found during directional ${direction}`, LOG_LEVEL_NOTICE);
|
|
356
359
|
return replicationBlocked("no-active-replicator");
|
|
357
360
|
};
|
|
358
|
-
const outcome = await run();
|
|
361
|
+
const outcome = await run(setting);
|
|
359
362
|
if (isReplicationCompleted(outcome)) return true;
|
|
360
363
|
if (outcome.status !== "failed") return false;
|
|
361
364
|
const failedContext = expectedContext;
|
|
362
365
|
if (!failedContext) return false;
|
|
363
366
|
const failure = Object.freeze({ context: failedContext, setting, outcome });
|
|
364
367
|
const checkResult = await this.checkConnectionFailure(failure);
|
|
365
|
-
return checkResult === "CHECKAGAIN" && isReplicationCompleted(await run());
|
|
368
|
+
return checkResult === "CHECKAGAIN" && isReplicationCompleted(await run(captureSetting()));
|
|
366
369
|
}
|
|
367
370
|
async replicateAllToRemote(showingNotice = false) {
|
|
368
371
|
if (!this.appLifecycleService.isReady()) return false;
|