@vrtmrz/livesync-commonlib 0.1.19 → 0.1.21

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.
Files changed (114) hide show
  1. package/README.md +5 -1
  2. package/dist/common/models/setting.p2p.d.ts +2 -0
  3. package/dist/common/models/setting.p2p.js +5 -0
  4. package/dist/interfaces/ServiceModule.d.ts +13 -4
  5. package/dist/managers/ChunkFetcher.js +10 -0
  6. package/dist/p2p/P2PService.d.ts +123 -0
  7. package/dist/p2p/P2PService.js +271 -0
  8. package/dist/p2p/index.d.ts +8 -0
  9. package/dist/p2p/index.js +8 -0
  10. package/dist/pouchdb/LiveSyncLocalDB.d.ts +3 -0
  11. package/dist/pouchdb/LiveSyncLocalDB.js +23 -3
  12. package/dist/pouchdb/ReplicatorShim.d.ts +43 -4
  13. package/dist/pouchdb/ReplicatorShim.js +78 -5
  14. package/dist/replication/CentralCompatibility.d.ts +38 -0
  15. package/dist/replication/CentralCompatibility.js +39 -0
  16. package/dist/replication/CentralRemoteAdministration.d.ts +84 -0
  17. package/dist/replication/CentralRemoteAdministration.js +71 -0
  18. package/dist/replication/LiveSyncAbstractReplicator.d.ts +19 -6
  19. package/dist/replication/ProviderCapability.d.ts +32 -0
  20. package/dist/replication/ProviderCapability.js +27 -0
  21. package/dist/replication/RemoteResource.d.ts +85 -0
  22. package/dist/replication/RemoteResource.js +17 -0
  23. package/dist/replication/ReplicatorInstance.d.ts +23 -0
  24. package/dist/replication/ReplicatorProvider.d.ts +262 -0
  25. package/dist/replication/ReplicatorProvider.js +150 -0
  26. package/dist/replication/couchdb/LiveSyncReplicator.d.ts +54 -8
  27. package/dist/replication/couchdb/LiveSyncReplicator.js +411 -158
  28. package/dist/replication/index.d.ts +10 -0
  29. package/dist/replication/index.js +72 -0
  30. package/dist/replication/journal/JournalSyncCore.d.ts +2 -0
  31. package/dist/replication/journal/JournalSyncCore.js +18 -5
  32. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +38 -10
  33. package/dist/replication/journal/LiveSyncJournalReplicator.js +271 -63
  34. package/dist/replication/journal/LiveSyncJournalReplicatorEnv.d.ts +8 -0
  35. package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +2 -0
  36. package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -0
  37. package/dist/replication/journal/objectstore/MinioStorageAdapter.js +10 -1
  38. package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +12 -40
  39. package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +52 -152
  40. package/dist/replication/trystero/P2PAutomationCoordinator.d.ts +20 -0
  41. package/dist/replication/trystero/P2PAutomationCoordinator.js +43 -0
  42. package/dist/replication/trystero/P2PConnectionProbeAdmission.d.ts +32 -0
  43. package/dist/replication/trystero/P2PConnectionProbeAdmission.js +10 -0
  44. package/dist/replication/trystero/P2PReplicationWire.d.ts +31 -0
  45. package/dist/replication/trystero/P2PReplicationWire.js +50 -0
  46. package/dist/replication/trystero/P2PRoomSession.d.ts +32 -0
  47. package/dist/replication/trystero/P2PRoomSession.js +101 -0
  48. package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +61 -0
  49. package/dist/replication/trystero/P2PRoomSessionOwner.js +249 -0
  50. package/dist/replication/trystero/TrysteroReplicator.d.ts +88 -36
  51. package/dist/replication/trystero/TrysteroReplicator.js +273 -113
  52. package/dist/replication/trystero/TrysteroReplicatorP2PClient.d.ts +7 -4
  53. package/dist/replication/trystero/TrysteroReplicatorP2PClient.js +21 -14
  54. package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +26 -3
  55. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +84 -39
  56. package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -6
  57. package/dist/replication/trystero/addP2PEventHandlers.d.ts +8 -12
  58. package/dist/replication/trystero/addP2PEventHandlers.js +19 -19
  59. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.d.ts +9 -0
  60. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +30 -0
  61. package/dist/replication/trystero/types.d.ts +18 -0
  62. package/dist/replication/trystero/types.js +5 -1
  63. package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +3 -3
  64. package/dist/replication/trystero/useP2PReplicatorCommands.js +6 -8
  65. package/dist/replication/trystero/useP2PReplicatorFeature.d.ts +16 -7
  66. package/dist/replication/trystero/useP2PReplicatorFeature.js +73 -55
  67. package/dist/rpc/RpcRoom.d.ts +15 -1
  68. package/dist/rpc/RpcRoom.js +176 -41
  69. package/dist/rpc/RpcSession.d.ts +3 -1
  70. package/dist/rpc/RpcSession.js +2 -2
  71. package/dist/rpc/index.d.ts +1 -1
  72. package/dist/rpc/transports/trysteroUtils.js +2 -1
  73. package/dist/rpc/types.d.ts +26 -1
  74. package/dist/serviceFeatures/checkRemoteSize.js +5 -2
  75. package/dist/serviceModules/Rebuilder.js +66 -56
  76. package/dist/services/base/AppLifecycleService.d.ts +4 -0
  77. package/dist/services/base/AppLifecycleService.js +4 -0
  78. package/dist/services/base/ControlService.js +0 -4
  79. package/dist/services/base/DatabaseEventService.js +1 -1
  80. package/dist/services/base/IService.d.ts +105 -8
  81. package/dist/services/base/ReadinessEvaluator.d.ts +40 -0
  82. package/dist/services/base/ReadinessEvaluator.js +16 -0
  83. package/dist/services/base/RemoteConnection.d.ts +3 -0
  84. package/dist/services/base/RemoteService.js +2 -2
  85. package/dist/services/base/ReplicationService.d.ts +45 -10
  86. package/dist/services/base/ReplicationService.js +250 -91
  87. package/dist/services/base/ReplicationService.readiness.d.ts +52 -0
  88. package/dist/services/base/ReplicationService.readiness.js +87 -0
  89. package/dist/services/base/ReplicationService.typedReplication.d.ts +93 -0
  90. package/dist/services/base/ReplicationService.typedReplication.js +214 -0
  91. package/dist/services/base/ReplicatorService.activeReplicatorState.d.ts +47 -0
  92. package/dist/services/base/ReplicatorService.activeReplicatorState.js +107 -0
  93. package/dist/services/base/ReplicatorService.centralRemoteAdministration.d.ts +5 -0
  94. package/dist/services/base/ReplicatorService.centralRemoteAdministration.js +44 -0
  95. package/dist/services/base/ReplicatorService.d.ts +75 -14
  96. package/dist/services/base/ReplicatorService.js +232 -53
  97. package/dist/services/base/ReplicatorService.remoteResourceResolver.d.ts +4 -0
  98. package/dist/services/base/ReplicatorService.remoteResourceResolver.js +11 -0
  99. package/dist/services/base/TweakValueService.d.ts +3 -2
  100. package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +1 -1
  101. package/dist/services/lib/HandlerUtils.d.ts +24 -0
  102. package/dist/services/lib/HandlerUtils.js +40 -0
  103. package/docs/development.md +40 -7
  104. package/docs/p2p-transport-lifecycle.md +135 -11
  105. package/docs/proven-in-use.md +5 -1
  106. package/docs/remote-configurations.md +4 -0
  107. package/docs/service-feature-composition.md +250 -0
  108. package/package.json +17 -1
  109. package/dist/replication/trystero/P2PReplicatorBase.d.ts +0 -19
  110. package/dist/replication/trystero/P2PReplicatorCore.d.ts +0 -8
  111. package/dist/replication/trystero/P2PReplicatorCore.js +0 -56
  112. package/dist/replication/trystero/TrysteroReplicatorP2PConnection.d.ts +0 -2
  113. package/dist/replication/trystero/TrysteroReplicatorP2PConnection.js +0 -4
  114. /package/dist/replication/{trystero/P2PReplicatorBase.js → ReplicatorInstance.js} +0 -0
@@ -1,21 +1,45 @@
1
1
  import { TweakValuesShouldMatchedTemplate } from "../../common/types.js";
2
- import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
2
+ import {
3
+ LOG_LEVEL_INFO,
4
+ LOG_LEVEL_NOTICE,
5
+ LOG_LEVEL_VERBOSE,
6
+ Logger
7
+ } from "octagonal-wheels/common/logger";
3
8
  import { replicateShim } from "../../pouchdb/ReplicatorShim.js";
4
- import {} from "./types.js";
5
- import { TrysteroConnection } from "./TrysteroReplicatorP2PConnection.js";
9
+ import { resolveCurrentP2PSettings } from "./types.js";
6
10
  import { scheduleOnceIfDuplicated, serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock_v2";
7
11
  import { delay, fireAndForget } from "octagonal-wheels/promises";
8
- import { EVENT_P2P_REPLICATOR_PROGRESS, EVENT_P2P_REPLICATOR_STATUS, P2PHost } from "./TrysteroReplicatorP2PServer.js";
12
+ import {
13
+ EVENT_ADVERTISEMENT_RECEIVED,
14
+ EVENT_P2P_REPLICATOR_PROGRESS,
15
+ EVENT_P2P_REPLICATOR_STATUS,
16
+ P2PHost
17
+ } from "./TrysteroReplicatorP2PServer.js";
9
18
  import { encryptWithEphemeralSalt, decryptWithEphemeralSalt } from "octagonal-wheels/encryption/hkdf";
10
19
  import { sha1 } from "octagonal-wheels/hash/purejs";
11
20
  import { isObjectDifferent } from "octagonal-wheels/object";
12
21
  import { getRelaySockets, pauseRelayReconnection, resumeRelayReconnection } from "@trystero-p2p/nostr";
22
+ import { P2PAutomationCoordinator } from "./P2PAutomationCoordinator.js";
23
+ import { compatGlobal } from "../../common/coreEnvFunctions.js";
24
+ import {
25
+ fromP2PReplicationWireResult,
26
+ toP2PReplicationWireResult,
27
+ toP2PWireError
28
+ } from "./P2PReplicationWire.js";
13
29
  async function encrypt(data, passphrase) {
14
30
  return await encryptWithEphemeralSalt(data, passphrase, true);
15
31
  }
16
32
  async function decrypt(encryptedData, passphrase) {
17
33
  return await decryptWithEphemeralSalt(encryptedData, passphrase);
18
34
  }
35
+ const P2P_REPLICATION_COMPLETED = Object.freeze({ status: "completed", ok: true });
36
+ const P2P_REPLICATION_CANCELLED = Object.freeze({ status: "cancelled" });
37
+ function p2pReplicationFailed(error) {
38
+ return { status: "failed", error };
39
+ }
40
+ function isP2PReplicationCancelled(result) {
41
+ return result.status === "cancelled";
42
+ }
19
43
  async function getHashedStringWithCurrentTime(source) {
20
44
  const salt = (~~((/* @__PURE__ */ new Date()).getTime() / 1e3 / 180)).toString(36);
21
45
  const salt2 = await sha1(salt);
@@ -23,6 +47,9 @@ async function getHashedStringWithCurrentTime(source) {
23
47
  }
24
48
  class TrysteroReplicator {
25
49
  _env;
50
+ finiteOperationOwner;
51
+ disposed = false;
52
+ fallbackAutomationCoordinator = new P2PAutomationCoordinator();
26
53
  server;
27
54
  replicationStatus() {
28
55
  return {};
@@ -30,6 +57,10 @@ class TrysteroReplicator {
30
57
  get settings() {
31
58
  return this._env.settings;
32
59
  }
60
+ /** Latest room-local policy settings supplied by the owning host. */
61
+ get currentSettings() {
62
+ return resolveCurrentP2PSettings(this._env);
63
+ }
33
64
  get db() {
34
65
  return this._env.db;
35
66
  }
@@ -45,6 +76,9 @@ class TrysteroReplicator {
45
76
  get translate() {
46
77
  return this._env.translate;
47
78
  }
79
+ get automationCoordinator() {
80
+ return this._env.automationCoordinator ?? this.fallbackAutomationCoordinator;
81
+ }
48
82
  async runFiniteReplicationActivity(task) {
49
83
  if (this._env.runFiniteReplicationActivity) {
50
84
  return await this._env.runFiniteReplicationActivity(task, { label: "replication" });
@@ -54,8 +88,9 @@ class TrysteroReplicator {
54
88
  async canStartOrdinaryReplication(showMessage = false) {
55
89
  return this._env.canStartOrdinaryReplication ? await this._env.canStartOrdinaryReplication(showMessage) : true;
56
90
  }
57
- constructor(env, server) {
91
+ constructor(env, server, finiteOperationOwner) {
58
92
  this._env = env;
93
+ this.finiteOperationOwner = finiteOperationOwner;
59
94
  if (server) {
60
95
  this.server = server;
61
96
  return;
@@ -76,13 +111,19 @@ class TrysteroReplicator {
76
111
  if (!this.settings.P2P_relays || this.settings.P2P_relays.length === 0) {
77
112
  throw new Error("No relay URIs provided. We need them to establish the P2P connection");
78
113
  }
79
- this.server = new TrysteroConnection(env);
114
+ this.server = new P2PHost(env);
80
115
  } catch (e) {
81
116
  Logger(e instanceof Error ? e.message : "Error while creating TrysteroReplicator", LOG_LEVEL_NOTICE);
82
117
  Logger(e, LOG_LEVEL_VERBOSE);
83
118
  throw e;
84
119
  }
85
120
  }
121
+ async runSessionFiniteOperation(task, callerSignal) {
122
+ if (this.finiteOperationOwner) {
123
+ return await this.finiteOperationOwner.runFiniteOperation(task, callerSignal);
124
+ }
125
+ return await task(callerSignal);
126
+ }
86
127
  async close() {
87
128
  this.requestStatus();
88
129
  await this.server?.shutdown();
@@ -92,13 +133,31 @@ class TrysteroReplicator {
92
133
  this.requestStatus();
93
134
  this.disconnectFromServer();
94
135
  }
136
+ /** Close the transport and release resources owned for this object's lifetime. */
137
+ async dispose() {
138
+ if (this.disposed) return;
139
+ this.disposed = true;
140
+ try {
141
+ await this.close();
142
+ } finally {
143
+ this.server?.dispose();
144
+ }
145
+ }
95
146
  async open() {
147
+ if (this.disposed) {
148
+ throw new Error("The P2P Replicator has been disposed.");
149
+ }
96
150
  this.allowReconnection();
97
- await this.server?.start([this.getCommands()]);
151
+ const commands = this.getCommands();
152
+ await this.server?.start(
153
+ [commands],
154
+ () => this.server?.serveCancellationAwareFunction(
155
+ "reqSync",
156
+ async ({ signal }, _peerId, fromPeerId) => toP2PReplicationWireResult(await this.handleSynchronisationRequest(fromPeerId, signal))
157
+ )
158
+ );
98
159
  this.dispatchStatus();
99
- if (this.settings.P2P_AutoBroadcast) {
100
- this.enableBroadcastChanges();
101
- }
160
+ this.reconcileAutoBroadcast(this.currentSettings.P2P_AutoBroadcast);
102
161
  }
103
162
  async makeSureOpened() {
104
163
  if (!this.server?.isServing) {
@@ -106,19 +165,27 @@ class TrysteroReplicator {
106
165
  }
107
166
  }
108
167
  get autoSyncPeers() {
109
- const peers = this.settings.P2P_AutoSyncPeers.split(",").map((e) => e.trim()).filter((e) => e.length > 0).map((e) => e.startsWith("~") ? new RegExp(e.substring(1), "i") : new RegExp(`^${e}$`, "i"));
168
+ const peers = this.currentSettings.P2P_AutoSyncPeers.split(",").map((e) => e.trim()).filter((e) => e.length > 0).map((e) => e.startsWith("~") ? new RegExp(e.substring(1), "i") : new RegExp(`^${e}$`, "i"));
110
169
  return peers;
111
170
  }
112
171
  get autoWatchPeers() {
113
- const peers = this.settings.P2P_AutoWatchPeers.split(",").map((e) => e.trim()).filter((e) => e.length > 0).map((e) => e.startsWith("~") ? new RegExp(e.substring(1), "i") : new RegExp(`^${e}$`, "i"));
172
+ const peers = this.currentSettings.P2P_AutoWatchPeers.split(",").map((e) => e.trim()).filter((e) => e.length > 0).map((e) => e.startsWith("~") ? new RegExp(e.substring(1), "i") : new RegExp(`^${e}$`, "i"));
114
173
  return peers;
115
174
  }
116
175
  async onNewPeer(peer) {
117
176
  const peerName = peer.name;
118
- if (this.autoSyncPeers.some((e) => e.test(peerName))) {
119
- await this.runFiniteReplicationActivity(() => this.sync(peer.peerId));
177
+ const shouldAutoSync = this.autoSyncPeers.some((e) => e.test(peerName));
178
+ const shouldAutoWatch = this.autoWatchPeers.some((e) => e.test(peerName));
179
+ if (!shouldAutoSync && !shouldAutoWatch) return;
180
+ const acceptance = await this.server?.evaluatePeerAcceptance(peer.peerId);
181
+ if (acceptance !== "accepted") return;
182
+ if (shouldAutoSync) {
183
+ await this.automationCoordinator.runBaseline(
184
+ peerName,
185
+ () => this.runFiniteReplicationActivity(() => this.sync(peer.peerId))
186
+ );
120
187
  }
121
- if (this.autoWatchPeers.some((e) => e.test(peerName))) {
188
+ if (shouldAutoWatch && await this.getRemoteIsBroadcasting(peer.peerId) === true) {
122
189
  this.watchPeer(peer.peerId);
123
190
  }
124
191
  }
@@ -148,15 +215,19 @@ class TrysteroReplicator {
148
215
  }
149
216
  return allSettings;
150
217
  }
218
+ async handleSynchronisationRequest(fromPeerId, signal) {
219
+ if (this._onSetup) {
220
+ return p2pReplicationFailed(new Error("The setup is in progress"));
221
+ }
222
+ return await this.runFiniteReplicationActivity(
223
+ () => signal ? this.replicateFrom(fromPeerId, false, false, false, signal) : this.replicateFrom(fromPeerId)
224
+ );
225
+ }
151
226
  getCommands() {
152
227
  return {
153
- reqSync: async (fromPeerId) => {
154
- if (this._onSetup) {
155
- return { error: new Error("The setup is in progress") };
156
- }
157
- const result = await this.runFiniteReplicationActivity(() => this.replicateFrom(fromPeerId));
158
- return result;
159
- },
228
+ // The wire-visible command deliberately accepts only serialisable
229
+ // arguments. RpcRoom supplies cancellation context separately.
230
+ reqSync: async (fromPeerId) => toP2PReplicationWireResult(await this.handleSynchronisationRequest(fromPeerId)),
160
231
  "!reqAuth": async (fromPeerId) => {
161
232
  return await this.server?.isAcceptablePeer(fromPeerId);
162
233
  },
@@ -165,13 +236,13 @@ class TrysteroReplicator {
165
236
  },
166
237
  onProgress: async (fromPeerId) => {
167
238
  if (this._onSetup) {
168
- return { error: new Error("The setup is in progress") };
239
+ return { error: toP2PWireError(new Error("The setup is in progress")) };
169
240
  }
170
241
  await this.onUpdateDatabase(fromPeerId);
171
242
  },
172
243
  getAllConfig: async (fromPeerId) => {
173
244
  if (this._onSetup) {
174
- return { error: new Error("The setup is in progress") };
245
+ return { error: toP2PWireError(new Error("The setup is in progress")) };
175
246
  }
176
247
  const passphrase = await skipIfDuplicated(`getAllConfig-${fromPeerId}`, async () => {
177
248
  return await this.confirm.askString(
@@ -218,7 +289,7 @@ class TrysteroReplicator {
218
289
  },
219
290
  requestBroadcasting: async (peerId) => {
220
291
  if (this._onSetup) {
221
- return { error: new Error("The setup is in progress") };
292
+ return { error: toP2PWireError(new Error("The setup is in progress")) };
222
293
  }
223
294
  if (this._isBroadcasting) {
224
295
  return true;
@@ -235,7 +306,7 @@ class TrysteroReplicator {
235
306
  }
236
307
  };
237
308
  }
238
- async requestAuthenticate(peerId) {
309
+ async requestAuthenticate(peerId, signal) {
239
310
  if (!this.server) return false;
240
311
  try {
241
312
  const connection = this.server.getConnection(peerId);
@@ -243,7 +314,8 @@ class TrysteroReplicator {
243
314
  const r = await connection.invokeRemoteObjectFunction(
244
315
  "!reqAuth",
245
316
  [selfPeerId],
246
- 2e4
317
+ 2e4,
318
+ signal
247
319
  );
248
320
  return r;
249
321
  } catch (e) {
@@ -252,30 +324,34 @@ class TrysteroReplicator {
252
324
  return false;
253
325
  }
254
326
  }
255
- // async selectPeer() {
256
- // if (!this.server) return false;
257
- // const knownPeers = this.server.knownAdvertisements;
258
- // if (knownPeers.length === 0) {
259
- // Logger("No known peers", LOG_LEVEL_VERBOSE);
260
- // return false;
261
- // }
262
- // const peers = [...Object.entries(knownPeers)].map(([peerId, info]) => {
263
- // return `${info.peerId}\u2001: (${info.name})`;
264
- // });
265
- // const selectedPeer = await this.confirm.askSelectString("Select a peer to replicate", peers);
266
- // if (selectedPeer) return selectedPeer.split("\u2001")[0];
267
- // return false;
268
- // }
269
327
  lastSeq = "";
270
- async requestSynchroniseToPeer(peerId) {
328
+ async requestSynchroniseToPeer(peerId, callerSignal) {
329
+ return await this.runSessionFiniteOperation(
330
+ async (signal) => await this.requestSynchroniseToPeerWithinSession(peerId, signal),
331
+ callerSignal
332
+ );
333
+ }
334
+ async requestSynchroniseToPeerWithinSession(peerId, signal) {
271
335
  if (!await this.canStartOrdinaryReplication(false)) {
272
- return { error: new Error("Replication is not ready") };
336
+ return p2pReplicationFailed(new Error("Replication is not ready"));
273
337
  }
338
+ if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
274
339
  await delay(25);
340
+ if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
275
341
  if (!this.server) throw new Error("Server is not available");
276
342
  const conn = this.server.getConnection(peerId);
277
- const result = await conn.invokeRemoteFunction("reqSync", [this.server.serverPeerId], 0);
278
- return result;
343
+ try {
344
+ const result = await conn.invokeRemoteFunction(
345
+ "reqSync",
346
+ [this.server.serverPeerId],
347
+ 0,
348
+ signal
349
+ );
350
+ return fromP2PReplicationWireResult(result);
351
+ } catch (error) {
352
+ if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
353
+ throw error;
354
+ }
279
355
  }
280
356
  async requestSynchroniseToAllAvailablePeers() {
281
357
  await scheduleOnceIfDuplicated("requestSynchroniseToAllAvailablePeers", async () => {
@@ -287,6 +363,7 @@ class TrysteroReplicator {
287
363
  });
288
364
  }
289
365
  dispatchStatus() {
366
+ if (this.disposed) return;
290
367
  this._env.events.emitEvent(EVENT_P2P_REPLICATOR_STATUS, {
291
368
  isBroadcasting: this._isBroadcasting,
292
369
  replicatingTo: [...this._replicateToPeers],
@@ -347,20 +424,37 @@ class TrysteroReplicator {
347
424
  void this.changes.on("complete", closeChanges);
348
425
  fireAndForget(async () => await this.notifyProgress());
349
426
  }
427
+ /** Apply automatic broadcast policy without restarting this room. */
428
+ reconcileAutoBroadcast(enabled) {
429
+ if (enabled === this._isBroadcasting) return;
430
+ if (enabled) {
431
+ this.enableBroadcastChanges();
432
+ } else {
433
+ this.disableBroadcastChanges();
434
+ }
435
+ }
350
436
  get knownAdvertisements() {
351
437
  return this.server?.knownAdvertisements ?? [];
352
438
  }
353
439
  availableReplicationPairs = /* @__PURE__ */ new Set();
354
- async sync(remotePeer, showNotice = false) {
355
- const from = await this.replicateFrom(remotePeer, showNotice);
356
- if (!from || from.error) {
440
+ async sync(remotePeer, showNotice = false, callerSignal) {
441
+ return await this.runSessionFiniteOperation(
442
+ async (signal) => await this.syncWithinSession(remotePeer, showNotice, signal),
443
+ callerSignal
444
+ );
445
+ }
446
+ async syncWithinSession(remotePeer, showNotice, signal) {
447
+ const from = await this.replicateFrom(remotePeer, showNotice, false, false, signal);
448
+ if (isP2PReplicationCancelled(from)) return from;
449
+ if (from.error) {
357
450
  Logger("Error while replicating from the remote", LOG_LEVEL_VERBOSE);
358
451
  Logger(from.error, LOG_LEVEL_VERBOSE);
359
452
  return from;
360
453
  }
454
+ if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
361
455
  const logLevel = showNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
362
456
  Logger(`P2P Replication has been requested to ${remotePeer}`, logLevel, "p2p-replicator");
363
- const res = await this.requestSynchroniseToPeer(remotePeer);
457
+ const res = await this.requestSynchroniseToPeer(remotePeer, signal);
364
458
  if (res.ok) {
365
459
  Logger("P2P Replication has been done", logLevel, "p2p-replicator");
366
460
  }
@@ -368,29 +462,9 @@ class TrysteroReplicator {
368
462
  Logger("Error while syncing from the remote", logLevel, "p2p-replicator");
369
463
  Logger(res.error, LOG_LEVEL_VERBOSE);
370
464
  }
465
+ return res;
371
466
  }
372
467
  _replicateToPeers = /* @__PURE__ */ new Set();
373
- // async replicateTo() {
374
- // await this.makeSureOpened();
375
- // const remotePeer = await this.selectPeer();
376
- // if (!remotePeer) {
377
- // Logger("No peer selected", LOG_LEVEL_VERBOSE);
378
- // return;
379
- // }
380
- // Logger(`P2P Replicating to ${remotePeer}`, LOG_LEVEL_INFO);
381
- // try {
382
- // if (this._replicateToPeers.has(remotePeer)) {
383
- // Logger(`Replication to ${remotePeer} is already in progress`, LOG_LEVEL_VERBOSE);
384
- // return;
385
- // }
386
- // this._replicateToPeers.add(remotePeer);
387
- // this.dispatchStatus();
388
- // return await this.requestSynchroniseToPeer(remotePeer);
389
- // } finally {
390
- // this._replicateToPeers.delete(remotePeer);
391
- // this.dispatchStatus();
392
- // }
393
- // }
394
468
  _replicateFromPeers = /* @__PURE__ */ new Set();
395
469
  dispatchReplicationProgress(peerId, info) {
396
470
  this.onReplicationProgress(peerId, info);
@@ -446,24 +520,47 @@ class TrysteroReplicator {
446
520
  Logger(ex, LOG_LEVEL_VERBOSE);
447
521
  });
448
522
  }
449
- async replicateFrom(remotePeer, showNotice = false, fromStart = false, skipOrdinaryReplicationPolicy = false) {
523
+ async replicateFrom(remotePeer, showNotice = false, fromStart = false, skipOrdinaryReplicationPolicy = false, callerSignal) {
524
+ return await this.runSessionFiniteOperation(
525
+ async (signal) => await this.replicateFromWithinSession(
526
+ remotePeer,
527
+ showNotice,
528
+ fromStart,
529
+ skipOrdinaryReplicationPolicy,
530
+ signal
531
+ ),
532
+ callerSignal
533
+ );
534
+ }
535
+ async replicateFromWithinSession(remotePeer, showNotice, fromStart, skipOrdinaryReplicationPolicy, signal) {
450
536
  if (!skipOrdinaryReplicationPolicy && !await this.canStartOrdinaryReplication(showNotice)) {
451
- return { error: new Error("Replication is not ready") };
537
+ return p2pReplicationFailed(new Error("Replication is not ready"));
452
538
  }
539
+ if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
453
540
  const logLevel = showNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
454
541
  Logger(`P2P Requesting Authentication to ${remotePeer}`, logLevel, "p2p-replicator");
455
- if (await this.requestAuthenticate(remotePeer) !== true) {
456
- Logger("Peer rejected the connection", LOG_LEVEL_NOTICE, "p2p-replicator");
457
- return { error: new Error("Peer rejected the connection") };
542
+ const authenticated = signal ? await this.requestAuthenticate(remotePeer, signal) : await this.requestAuthenticate(remotePeer);
543
+ if (authenticated !== true) {
544
+ if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
545
+ Logger("Peer rejected the connection", logLevel, "p2p-replicator");
546
+ return p2pReplicationFailed(new Error("Peer rejected the connection"));
547
+ }
548
+ let tweaksMatched;
549
+ try {
550
+ tweaksMatched = signal ? await this.checkTweakValues(remotePeer, signal, logLevel) : await this.checkTweakValues(remotePeer, void 0, logLevel);
551
+ } catch (error) {
552
+ if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
553
+ throw error;
458
554
  }
459
- if (await this.checkTweakValues(remotePeer) !== true) {
460
- Logger("Tweak values are not matched", LOG_LEVEL_NOTICE, "p2p-replicator");
461
- return { error: new Error("Tweak values are not matched") };
555
+ if (tweaksMatched !== true) {
556
+ if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
557
+ Logger("Tweak values are not matched", logLevel, "p2p-replicator");
558
+ return p2pReplicationFailed(new Error("Tweak values are not matched"));
462
559
  }
463
560
  Logger(`P2P Replicating from ${remotePeer}`, logLevel, "p2p-replicator");
464
561
  if (this._replicateFromPeers.has(remotePeer)) {
465
- Logger(`Replication from ${remotePeer} is already in progress`, LOG_LEVEL_NOTICE, "p2p-replicator");
466
- return { error: new Error("Replication from this peer is already in progress") };
562
+ Logger(`Replication from ${remotePeer} is already in progress`, logLevel, "p2p-replicator");
563
+ return p2pReplicationFailed(new Error("Replication from this peer is already in progress"));
467
564
  }
468
565
  this._replicateFromPeers.add(remotePeer);
469
566
  this.dispatchStatus();
@@ -472,7 +569,7 @@ class TrysteroReplicator {
472
569
  throw new Error("Server is not available");
473
570
  }
474
571
  const connection = this.server.getConnection(remotePeer);
475
- const remoteDB = connection.remoteDB;
572
+ const remoteDB = connection.getRemoteDB(signal);
476
573
  Logger(`P2P replicateFrom preparing remote DB info for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
477
574
  const remoteDBInfo = await remoteDB.info();
478
575
  Logger(
@@ -487,7 +584,7 @@ class TrysteroReplicator {
487
584
  "p2p-replicator"
488
585
  );
489
586
  Logger(`P2P replicateFrom entering replicateShim for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
490
- await replicateShim(
587
+ const outcome = await replicateShim(
491
588
  this.db,
492
589
  remoteDB,
493
590
  async (docs, info) => {
@@ -504,22 +601,28 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
504
601
  },
505
602
  {
506
603
  live: false,
507
- rewind: fromStart
604
+ rewind: fromStart,
605
+ signal
508
606
  /*, batch_size: batchSize */
509
607
  }
510
608
  );
511
609
  Logger(`P2P replicateFrom replicateShim returned for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
512
610
  void this.acknowledgeProgress(remotePeer, void 0);
611
+ if (outcome.status === "cancelled") {
612
+ Logger(`P2P Replication from ${remotePeer} has been cancelled`, logLevel, "p2p-replicator");
613
+ return P2P_REPLICATION_CANCELLED;
614
+ }
513
615
  Logger(`P2P Replication from ${remotePeer} has been completed`, logLevel, "p2p-replicator");
514
616
  } catch (e) {
617
+ if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
515
618
  Logger("Error while P2P replicating", logLevel, "p2p-replicator");
516
619
  Logger(e, LOG_LEVEL_VERBOSE);
517
- return { error: e };
620
+ return p2pReplicationFailed(e);
518
621
  } finally {
519
622
  this._replicateFromPeers.delete(remotePeer);
520
623
  this.dispatchStatus();
521
624
  }
522
- return { ok: true };
625
+ return P2P_REPLICATION_COMPLETED;
523
626
  }
524
627
  notifyProgress(excludePeerId) {
525
628
  if (!this._isBroadcasting) return;
@@ -604,14 +707,15 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
604
707
  return false;
605
708
  }
606
709
  }
607
- async checkTweakValues(peerId) {
710
+ /** Compare peer settings without exceeding the presentation authority of the initiating call. */
711
+ async checkTweakValues(peerId, signal, logLevel = LOG_LEVEL_NOTICE) {
608
712
  if (!this.server) {
609
- Logger("Server is not available", LOG_LEVEL_NOTICE);
713
+ Logger("Server is not available", logLevel);
610
714
  return false;
611
715
  }
612
716
  const peerPlatform = this.server.knownAdvertisements.find((e) => e.peerId == peerId)?.platform;
613
717
  if (peerPlatform == null) {
614
- Logger("Peer is not found", LOG_LEVEL_NOTICE);
718
+ Logger("Peer is not found", logLevel);
615
719
  return false;
616
720
  }
617
721
  if (this.platform === "pseudo-replicator") {
@@ -621,7 +725,8 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
621
725
  return true;
622
726
  }
623
727
  const connection = this.server.getConnection(peerId);
624
- const tweakValues = await connection.invokeRemoteObjectFunction("getTweakSettings", [this.server.serverPeerId], 5e3);
728
+ const tweakValues = await connection.invokeRemoteObjectFunction("getTweakSettings", [this.server.serverPeerId], 5e3, signal);
729
+ if (signal?.aborted) return false;
625
730
  const thisTweakValues = await this.getTweakSettings("");
626
731
  if (!isObjectDifferent(thisTweakValues, tweakValues)) {
627
732
  return true;
@@ -629,43 +734,98 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
629
734
  if (thisTweakValues.passphrase !== tweakValues.passphrase) {
630
735
  Logger(
631
736
  "Replication cancelled: Passphrase is not matched\nCannot replicate to a remote database until the problem is resolved.",
632
- LOG_LEVEL_NOTICE
737
+ logLevel
633
738
  );
634
739
  return false;
635
740
  }
636
741
  Logger(
637
742
  "Some mismatched configuration have been detected... Please check settings for efficient replication.",
638
- LOG_LEVEL_NOTICE
743
+ logLevel
639
744
  );
640
745
  return true;
641
746
  }
642
- async replicateFromCommand(showResult = false) {
643
- const r = await skipIfDuplicated("replicateFromCommand", async () => {
644
- const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
645
- if (!this._env.settings.P2P_Enabled) {
646
- Logger(this.translate("P2P.NotEnabled"), logLevel);
647
- return Promise.resolve(false);
747
+ configuredPeerNames() {
748
+ return [...new Set(this.currentSettings.P2P_SyncOnReplication.split(",").map((name) => name.trim()))].filter(
749
+ Boolean
750
+ );
751
+ }
752
+ findAdvertisement(peerName) {
753
+ return this.knownAdvertisements.find((advertisement) => advertisement.name === peerName);
754
+ }
755
+ async waitForConfiguredAdvertisements(peerNames, timeoutMs, signal) {
756
+ const allPresent = () => peerNames.every((name) => this.findAdvertisement(name) !== void 0);
757
+ if (allPresent() || timeoutMs <= 0 || signal?.aborted) return;
758
+ await new Promise((resolve) => {
759
+ let timeout;
760
+ let detachAdvertisement = () => void 0;
761
+ const finish = () => {
762
+ detachAdvertisement();
763
+ signal?.removeEventListener("abort", finish);
764
+ if (timeout !== void 0) compatGlobal.clearTimeout(timeout);
765
+ resolve();
766
+ };
767
+ detachAdvertisement = this._env.events.onEvent(EVENT_ADVERTISEMENT_RECEIVED, () => {
768
+ if (allPresent()) finish();
769
+ });
770
+ signal?.addEventListener("abort", finish, { once: true });
771
+ timeout = compatGlobal.setTimeout(finish, timeoutMs);
772
+ if (allPresent() || signal?.aborted) finish();
773
+ });
774
+ }
775
+ configuredTargetResult(name, peerId, result) {
776
+ if (result.status === "completed") return { name, peerId, status: "completed" };
777
+ if (result.status === "cancelled") return { name, peerId, status: "cancelled" };
778
+ return { name, peerId, status: "failed", error: result.error };
779
+ }
780
+ /** Synchronise configured peer targets without opening peer-selection UI. */
781
+ async replicateFromCommand(showResult = false, discoveryTimeoutMs = 5e3, callerSignal) {
782
+ const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
783
+ if (!this.currentSettings.P2P_Enabled) {
784
+ Logger(this.translate("P2P.NotEnabled"), logLevel);
785
+ return { status: "blocked", reason: "no-targets", targets: [] };
786
+ }
787
+ const peerNames = this.configuredPeerNames();
788
+ if (peerNames.length === 0) {
789
+ Logger(this.translate("P2P.NoAutoSyncPeers"), logLevel);
790
+ return { status: "blocked", reason: "no-targets", targets: [] };
791
+ }
792
+ await this.waitForConfiguredAdvertisements(peerNames, discoveryTimeoutMs, callerSignal);
793
+ const targets = [];
794
+ for (const peerName of peerNames) {
795
+ if (callerSignal?.aborted) {
796
+ targets.push({ name: peerName, status: "cancelled" });
797
+ continue;
648
798
  }
649
- const peers = this._env.settings.P2P_SyncOnReplication.split(",").map((e) => e.trim()).filter((e) => e);
650
- if (peers.length == 0) {
651
- Logger(this.translate("P2P.NoAutoSyncPeers"), LOG_LEVEL_NOTICE);
652
- return Promise.resolve(false);
799
+ const advertisement = this.findAdvertisement(peerName);
800
+ if (!advertisement) {
801
+ Logger(this.translate(`P2P.SeemsOffline`, { name: peerName }), logLevel);
802
+ targets.push({ name: peerName, status: "missing" });
803
+ continue;
653
804
  }
654
- for (const peer of peers) {
655
- const peerId = this.knownAdvertisements.find((e) => e.name == peer)?.peerId;
656
- if (!peerId) {
657
- Logger(this.translate(`P2P.SeemsOffline`, { name: peer }), logLevel);
658
- } else {
659
- Logger(this.translate(`P2P.SyncStartedWith`, { name: peer }), logLevel);
660
- await this.sync(peerId, showResult);
661
- }
805
+ const acceptance = await this.server?.evaluatePeerAcceptance(advertisement.peerId) ?? "unknown";
806
+ if (acceptance !== "accepted") {
807
+ targets.push({
808
+ name: peerName,
809
+ peerId: advertisement.peerId,
810
+ status: acceptance === "rejected" ? "rejected" : acceptance === "undecided" ? "undecided" : "missing"
811
+ });
812
+ continue;
662
813
  }
814
+ Logger(this.translate(`P2P.SyncStartedWith`, { name: peerName }), logLevel);
815
+ const result = await this.automationCoordinator.runBaseline(
816
+ peerName,
817
+ () => this.sync(advertisement.peerId, showResult, callerSignal)
818
+ );
819
+ targets.push(this.configuredTargetResult(peerName, advertisement.peerId, result));
820
+ }
821
+ if (targets.every((target) => target.status === "completed")) {
663
822
  Logger(this.translate("P2P.SyncCompleted"), logLevel);
664
- return Promise.resolve(true);
665
- });
666
- if (r === null) {
667
- Logger(this.translate("P2P.SyncAlreadyRunning"), LOG_LEVEL_NOTICE);
823
+ return { status: "completed", targets };
824
+ }
825
+ if (targets.every((target) => target.status === "cancelled")) {
826
+ return { status: "cancelled", targets };
668
827
  }
828
+ return { status: "partial", targets };
669
829
  }
670
830
  disconnectFromServer() {
671
831
  const connections = getRelaySockets();
@@ -7,13 +7,16 @@ export declare class TrysteroReplicatorP2PClient {
7
7
  _connectedPeerId: string;
8
8
  _remoteDB: PouchDBShim<SomeDocument<object>>;
9
9
  get remoteDB(): PouchDBShim<SomeDocument<object>>;
10
+ /** Bind the reverse database RPC calls to one finite-operation signal. */
11
+ getRemoteDB(signal?: AbortSignal): PouchDBShim<SomeDocument<object>>;
10
12
  constructor(server: TrysteroReplicatorP2PServer, connectedPeerId: string);
11
- _bindRemoteDB(): PouchDBShim<SomeDocument<object>>;
13
+ _bindRemoteDB(signal?: AbortSignal): PouchDBShim<SomeDocument<object>>;
14
+ private getCallOptions;
12
15
  _sendRPC(type: string, args: JsonLike[], timeout?: number): Promise<JsonLike>;
13
16
  __onResponse(_data: Response): void;
14
- bindRemoteFunction<T extends unknown[], U>(type: string, timeout?: number): (...args: T) => Promise<U>;
15
- invokeRemoteFunction<T extends unknown[], U>(type: string, args: T, timeout?: number): Promise<U>;
17
+ bindRemoteFunction<T extends unknown[], U>(type: string, timeout?: number, signal?: AbortSignal): (...args: T) => Promise<U>;
18
+ invokeRemoteFunction<T extends unknown[], U>(type: string, args: T, timeout?: number, signal?: AbortSignal): Promise<U>;
16
19
  bindRemoteObjectFunctions<T extends BindableObject<any>, U extends keyof T>(key: U, timeout?: number): (...args: Parameters<T[U]>) => Promise<Awaited<ReturnType<T[U]>>>;
17
- invokeRemoteObjectFunction<T extends BindableObject<any>, U extends NonPrivateMethodKeys<T>>(key: U, args: Parameters<T[U]>, timeout?: number): Promise<Awaited<ReturnType<T[U]>>>;
20
+ invokeRemoteObjectFunction<T extends BindableObject<any>, U extends NonPrivateMethodKeys<T>>(key: U, args: Parameters<T[U]>, timeout?: number, signal?: AbortSignal): Promise<Awaited<ReturnType<T[U]>>>;
18
21
  close(): void;
19
22
  }