@vrtmrz/livesync-commonlib 0.1.19 → 0.1.20

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 +249 -0
  25. package/dist/replication/ReplicatorProvider.js +145 -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 +70 -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 +72 -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 +248 -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 +83 -0
  90. package/dist/services/base/ReplicationService.typedReplication.js +191 -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
@@ -3,8 +3,7 @@ import {
3
3
  RemotePreferredTweakStatuses,
4
4
  LOG_LEVEL_NOTICE,
5
5
  LOG_LEVEL_INFO,
6
- LOG_LEVEL_VERBOSE,
7
- SETTING_KEY_P2P_DEVICE_NAME
6
+ LOG_LEVEL_VERBOSE
8
7
  } from "../../common/types.js";
9
8
  import {
10
9
  LiveSyncAbstractReplicator
@@ -12,175 +11,75 @@ import {
12
11
  import { TrysteroReplicator } from "./TrysteroReplicator.js";
13
12
  import {
14
13
  EVENT_ADVERTISEMENT_RECEIVED,
15
- EVENT_P2P_CONNECTED,
16
- P2PHost
14
+ EVENT_P2P_CONNECTED
17
15
  } from "./TrysteroReplicatorP2PServer.js";
18
16
  import { delay } from "octagonal-wheels/promises";
19
- import {
20
- hasValidP2PTurnServerUrl,
21
- normaliseP2PConnectionPath,
22
- normaliseP2PMaxWirePayloadBytes
23
- } from "../../common/models/setting.p2p.js";
24
- import { P2PConnectionPaths } from "../../common/models/setting.const.js";
17
+ import { P2PRoomSessionOwner } from "./P2PRoomSessionOwner.js";
25
18
  class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
26
- _p2pHost;
27
- _replicator;
28
- _lifecycleOperation = Promise.resolve();
29
- _shouldBeOpen = false;
30
- _activeTransportCompatibilitySignature;
19
+ constructor(env, sessionOwner = new P2PRoomSessionOwner(env)) {
20
+ super(env);
21
+ this.sessionOwner = sessionOwner;
22
+ this.env = env;
23
+ }
24
+ sessionOwner;
25
+ /** Peer lifecycle is fenced inside the room session rather than this facade. */
26
+ handlesPeerEventsWithinSession = true;
31
27
  get openReplicationUI() {
32
28
  return this.env.openReplicationUI;
33
29
  }
34
30
  get rawReplicator() {
35
- return this._replicator;
31
+ return this.sessionOwner.currentSession?.replicator;
36
32
  }
37
33
  get rawHost() {
38
- return this._p2pHost;
34
+ return this.sessionOwner.currentSession?.host;
39
35
  }
40
36
  getReplicationPBKDF2Salt(_setting, _refresh) {
41
37
  return Promise.resolve(new Uint8Array(32));
42
38
  }
43
39
  terminateSync() {
44
- }
45
- _buildEnv() {
46
- const services = this.env.services;
47
- return {
48
- events: services.context.events,
49
- translate: services.context.translate,
50
- get settings() {
51
- return services.setting.currentSettings();
52
- },
53
- get db() {
54
- return services.database.localDatabase.localDatabase;
55
- },
56
- get simpleStore() {
57
- return services.keyValueDB.openSimpleStore("p2p-sync");
58
- },
59
- get deviceName() {
60
- return services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) || services.vault.getVaultName();
61
- },
62
- get platform() {
63
- return services.API.getPlatform();
64
- },
65
- get confirm() {
66
- return services.API.confirm;
67
- },
68
- runFiniteReplicationActivity: (task, options) => services.replicator.runFiniteReplicationActivity(task, options),
69
- canStartOrdinaryReplication: (showMessage = false) => services.replication.onCheckReplicationReady(showMessage),
70
- processReplicatedDocs: async (docs) => {
71
- const settings = services.setting.currentSettings();
72
- if (settings.suspendParseReplicationResult) {
73
- const docLength = docs.length;
74
- if (docLength > 0) {
75
- Logger(
76
- `P2P sync, but parseReplicationResult is suspended. Ignoring ${docLength} documents.`,
77
- LOG_LEVEL_VERBOSE
78
- );
79
- }
80
- return;
81
- }
82
- await services.replication.parseSynchroniseResult(docs);
83
- }
84
- };
85
- }
86
- _enqueueLifecycleOperation(operation) {
87
- const queued = this._lifecycleOperation.catch(() => void 0).then(operation);
88
- this._lifecycleOperation = queued.catch(() => void 0);
89
- return queued;
90
- }
91
- /**
92
- * Capture settings which are fixed when the active P2P transport joins its room.
93
- * A later open request can remain idempotent only while these effective values match.
94
- */
95
- _getTransportCompatibilitySignature() {
96
- const settings = this.env.services.setting.currentSettings();
97
- const configuredPath = normaliseP2PConnectionPath(settings.P2P_connectionPath);
98
- const effectivePath = configuredPath === P2PConnectionPaths.Relay && hasValidP2PTurnServerUrl(settings.P2P_turnServers ?? "") ? P2PConnectionPaths.Relay : P2PConnectionPaths.Automatic;
99
- return `${normaliseP2PMaxWirePayloadBytes(settings.P2P_maxWirePayloadBytes)}:${effectivePath}`;
100
- }
101
- /** Close and forget the transport without changing the requested lifecycle state. */
102
- async _closeTransport() {
103
- if (this._replicator) {
104
- this._replicator.disableBroadcastChanges();
105
- await this._replicator.close();
106
- this._replicator = void 0;
107
- }
108
- this._p2pHost = void 0;
109
- this._activeTransportCompatibilitySignature = void 0;
40
+ this.sessionOwner.cancelActiveTransfers();
110
41
  }
111
42
  async open() {
112
- if (!this.env.services.setting.currentSettings().P2P_Enabled) {
113
- Logger(this.translate("P2P.NotEnabled"), LOG_LEVEL_NOTICE);
114
- return;
115
- }
116
- this._shouldBeOpen = true;
117
- await this._enqueueLifecycleOperation(async () => {
118
- if (!this._shouldBeOpen) return;
119
- const compatibilitySignature = this._getTransportCompatibilitySignature();
120
- if (this._replicator && this._p2pHost?.isServing) {
121
- if (this._activeTransportCompatibilitySignature === compatibilitySignature) {
122
- Logger("P2P replicator is already open.");
123
- return;
124
- }
125
- await this._closeTransport();
126
- }
127
- try {
128
- const env = this._buildEnv();
129
- const host = new P2PHost(env);
130
- const replicator = new TrysteroReplicator(env, host);
131
- this._p2pHost = host;
132
- this._replicator = replicator;
133
- await replicator.open();
134
- this._activeTransportCompatibilitySignature = compatibilitySignature;
135
- } catch (e) {
136
- Logger(e instanceof Error ? e.message : "Error while opening P2P connection", LOG_LEVEL_NOTICE);
137
- Logger(e, LOG_LEVEL_VERBOSE);
138
- this._p2pHost = void 0;
139
- this._replicator = void 0;
140
- this._activeTransportCompatibilitySignature = void 0;
141
- }
142
- });
43
+ await this.sessionOwner.open();
143
44
  }
144
45
  async close() {
145
- this._shouldBeOpen = false;
146
- await this._enqueueLifecycleOperation(async () => {
147
- await this._closeTransport();
148
- });
46
+ await this.sessionOwner.close();
149
47
  }
150
48
  closeReplication() {
151
- this._replicator?.disconnectFromServer();
49
+ this.sessionOwner.currentSession?.replicator.disconnectFromServer();
152
50
  }
153
51
  get server() {
154
- return this._replicator?.server;
52
+ return this.sessionOwner.currentSession?.replicator.server;
155
53
  }
156
54
  get knownAdvertisements() {
157
- return this._replicator?.knownAdvertisements ?? [];
55
+ return this.sessionOwner.currentSession?.replicator.knownAdvertisements ?? [];
158
56
  }
159
57
  enableBroadcastChanges() {
160
- this._replicator?.enableBroadcastChanges();
58
+ this.sessionOwner.currentSession?.replicator.enableBroadcastChanges();
161
59
  }
162
60
  disableBroadcastChanges() {
163
- this._replicator?.disableBroadcastChanges();
61
+ this.sessionOwner.currentSession?.replicator.disableBroadcastChanges();
164
62
  }
165
63
  requestStatus() {
166
- this._replicator?.requestStatus();
64
+ this.sessionOwner.currentSession?.replicator.requestStatus();
167
65
  }
168
66
  onNewPeer(peer) {
169
- return this._replicator?.onNewPeer(peer);
67
+ return this.sessionOwner.currentSession?.replicator.onNewPeer(peer);
170
68
  }
171
69
  onPeerLeaved(peerId) {
172
- this._replicator?.onPeerLeaved(peerId);
70
+ this.sessionOwner.currentSession?.replicator.onPeerLeaved(peerId);
173
71
  }
174
72
  async replicateFromCommand(showResult = false) {
175
- const replicator = this._replicator;
176
- if (!replicator) return;
177
- await this.env.services.replicator.runFiniteReplicationActivity(
73
+ const replicator = this.sessionOwner.currentSession?.replicator;
74
+ if (!replicator) return false;
75
+ const result = await this.env.services.replicator.runFiniteReplicationActivity(
178
76
  () => replicator.replicateFromCommand(showResult),
179
77
  { label: "replication" }
180
78
  );
79
+ return result.status === "completed";
181
80
  }
182
81
  async replicateFrom(peerId, showNotice = false, skipOrdinaryReplicationPolicy = false) {
183
- const replicator = this._replicator;
82
+ const replicator = this.sessionOwner.currentSession?.replicator;
184
83
  if (!replicator) throw new Error("P2P replicator is not open");
185
84
  return await this.env.services.replicator.runFiniteReplicationActivity(
186
85
  () => skipOrdinaryReplicationPolicy ? replicator.replicateFrom(peerId, showNotice, false, true) : replicator.replicateFrom(peerId, showNotice),
@@ -188,7 +87,7 @@ class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
188
87
  );
189
88
  }
190
89
  async requestSynchroniseToPeer(peerId) {
191
- const replicator = this._replicator;
90
+ const replicator = this.sessionOwner.currentSession?.replicator;
192
91
  if (!replicator) throw new Error("P2P replicator is not open");
193
92
  return await this.env.services.replicator.runBoundedRemoteActivity(
194
93
  () => replicator.requestSynchroniseToPeer(peerId),
@@ -196,33 +95,35 @@ class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
196
95
  );
197
96
  }
198
97
  async getRemoteConfig(peerId) {
199
- if (!this._replicator) throw new Error("P2P replicator is not open");
200
- return await this._replicator.getRemoteConfig(peerId);
98
+ const replicator = this.sessionOwner.currentSession?.replicator;
99
+ if (!replicator) throw new Error("P2P replicator is not open");
100
+ return await replicator.getRemoteConfig(peerId);
201
101
  }
202
102
  watchPeer(peerId) {
203
- this._replicator?.watchPeer(peerId);
103
+ this.sessionOwner.currentSession?.replicator.watchPeer(peerId);
204
104
  }
205
105
  unwatchPeer(peerId) {
206
- this._replicator?.unwatchPeer(peerId);
106
+ this.sessionOwner.currentSession?.replicator.unwatchPeer(peerId);
207
107
  }
208
108
  async sync(peerId, showNotice = false) {
209
- if (!this._replicator) throw new Error("P2P replicator is not open");
210
- return await this._replicator.sync(peerId, showNotice);
109
+ const replicator = this.sessionOwner.currentSession?.replicator;
110
+ if (!replicator) throw new Error("P2P replicator is not open");
111
+ return await replicator.sync(peerId, showNotice);
211
112
  }
212
113
  setOnSetup() {
213
- this._replicator?.setOnSetup();
114
+ this.sessionOwner.currentSession?.replicator.setOnSetup();
214
115
  }
215
116
  clearOnSetup() {
216
- this._replicator?.clearOnSetup();
117
+ this.sessionOwner.currentSession?.replicator.clearOnSetup();
217
118
  }
218
119
  async makeDecision(decision) {
219
- await this._replicator?.server?.makeDecision(decision);
120
+ await this.sessionOwner.currentSession?.replicator.server?.makeDecision(decision);
220
121
  }
221
122
  async revokeDecision(decision) {
222
- await this._replicator?.server?.revokeDecision(decision);
123
+ await this.sessionOwner.currentSession?.replicator.server?.revokeDecision(decision);
223
124
  }
224
125
  async makeSureOpened() {
225
- if (!this._replicator || !this._p2pHost?.isServing) {
126
+ if (!this.sessionOwner.isConnected) {
226
127
  await this.open();
227
128
  }
228
129
  }
@@ -232,11 +133,13 @@ class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
232
133
  }
233
134
  const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
234
135
  await this.makeSureOpened();
235
- if (!this._replicator) {
136
+ const replicator = this.sessionOwner.currentSession?.replicator;
137
+ if (!replicator) {
236
138
  Logger(this.translate("P2P.ReplicatorInstanceMissing"), logLevel);
237
139
  return false;
238
140
  }
239
- await this._replicator.replicateFromCommand(showResult);
141
+ const result = await replicator.replicateFromCommand(showResult);
142
+ return result.status === "completed";
240
143
  }
241
144
  tryConnectRemote(_setting, _showResult) {
242
145
  return Promise.resolve(false);
@@ -314,7 +217,8 @@ class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
314
217
  return this.replicateAllFromServer(setting, showingNotice);
315
218
  }
316
219
  await this.open();
317
- if (!this._replicator) {
220
+ const replicator = this.sessionOwner.currentSession?.replicator;
221
+ if (!replicator) {
318
222
  Logger("Failed to get replicator instance.", logLevel);
319
223
  return false;
320
224
  }
@@ -323,12 +227,12 @@ class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
323
227
  }
324
228
  await this.env.services.context.events.waitFor(EVENT_P2P_CONNECTED);
325
229
  const peerFrom = setting.P2P_RebuildFrom;
326
- this._replicator.setOnSetup();
230
+ replicator.setOnSetup();
327
231
  try {
328
232
  const r = await this.tryUntilSuccess(
329
233
  async () => {
330
234
  await this.makeSureOpened();
331
- return this._replicator ?? false;
235
+ return this.sessionOwner.currentSession?.replicator ?? false;
332
236
  },
333
237
  10,
334
238
  logLevel
@@ -354,7 +258,7 @@ class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
354
258
  return false;
355
259
  }
356
260
  } finally {
357
- this._replicator?.clearOnSetup();
261
+ this.sessionOwner.currentSession?.replicator.clearOnSetup();
358
262
  }
359
263
  }
360
264
  tryResetRemoteDatabase(_setting) {
@@ -411,10 +315,6 @@ class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
411
315
  );
412
316
  return Promise.resolve(false);
413
317
  }
414
- constructor(env) {
415
- super(env);
416
- this.env = env;
417
- }
418
318
  }
419
319
  export {
420
320
  LiveSyncTrysteroReplicator
@@ -0,0 +1,20 @@
1
+ import type { P2PReplicationResult } from "./TrysteroReplicator.js";
2
+ /**
3
+ * Stable de-duplication state for automatic baseline transfers.
4
+ *
5
+ * The service owner keeps this object across transport-only room replacement.
6
+ * Completed baselines are reset only for a new application lifecycle or a
7
+ * changed logical peer namespace/local database identity. In-flight work from
8
+ * an older generation may still settle, but cannot publish completion into the
9
+ * current generation.
10
+ */
11
+ export declare class P2PAutomationCoordinator {
12
+ private generation;
13
+ private identity;
14
+ private readonly completedPeers;
15
+ private readonly inFlightPeers;
16
+ beginLifecycle(): void;
17
+ reconcileIdentity(namespace: string, database: object): void;
18
+ runBaseline(peerName: string, task: () => Promise<P2PReplicationResult>): Promise<P2PReplicationResult>;
19
+ private normalisePeerName;
20
+ }
@@ -0,0 +1,43 @@
1
+ class P2PAutomationCoordinator {
2
+ generation = 0;
3
+ identity;
4
+ completedPeers = /* @__PURE__ */ new Set();
5
+ inFlightPeers = /* @__PURE__ */ new Map();
6
+ beginLifecycle() {
7
+ this.generation += 1;
8
+ this.completedPeers.clear();
9
+ }
10
+ reconcileIdentity(namespace, database) {
11
+ if (this.identity?.namespace === namespace && this.identity.database === database) return;
12
+ this.identity = { namespace, database };
13
+ this.generation += 1;
14
+ this.completedPeers.clear();
15
+ }
16
+ runBaseline(peerName, task) {
17
+ const key = this.normalisePeerName(peerName);
18
+ if (this.completedPeers.has(key)) {
19
+ return Promise.resolve({ status: "completed", ok: true });
20
+ }
21
+ const current = this.inFlightPeers.get(key);
22
+ if (current) return current;
23
+ const generation = this.generation;
24
+ const operation = task().then((result) => {
25
+ if (result.status === "completed" && generation === this.generation) {
26
+ this.completedPeers.add(key);
27
+ }
28
+ return result;
29
+ }).finally(() => {
30
+ if (this.inFlightPeers.get(key) === operation) {
31
+ this.inFlightPeers.delete(key);
32
+ }
33
+ });
34
+ this.inFlightPeers.set(key, operation);
35
+ return operation;
36
+ }
37
+ normalisePeerName(peerName) {
38
+ return peerName.trim().toLocaleLowerCase("en-US");
39
+ }
40
+ }
41
+ export {
42
+ P2PAutomationCoordinator
43
+ };
@@ -0,0 +1,32 @@
1
+ import type { ObsidianLiveSyncSettings } from "../../common/types.js";
2
+ /** Stable decision code for a trial which would extend an active relay binding. */
3
+ export declare const ACTIVE_P2P_RELAY_BINDING_CONFLICT: "active-p2p-relay-binding-conflict";
4
+ /** Settings needed to arbitrate a signalling-only P2P connection probe. */
5
+ export type P2PConnectionProbeSettings = Pick<ObsidianLiveSyncSettings, "P2P_relays">;
6
+ /** Owner settlement for one P2P connection-probe request. */
7
+ export type P2PConnectionProbeAdmissionResult<T> = {
8
+ readonly status: "observed-active";
9
+ } | {
10
+ readonly status: "trial";
11
+ readonly result: T;
12
+ } | {
13
+ readonly status: "blocked";
14
+ readonly reason: typeof ACTIVE_P2P_RELAY_BINDING_CONFLICT;
15
+ };
16
+ /**
17
+ * Arbitrates a complete short-lived probe against the stable P2P room owner.
18
+ *
19
+ * The continuation owns construction and disposal of its trial resources. It
20
+ * must not await another lifecycle operation on the same P2P service because
21
+ * the owner holds that lifecycle serialisation boundary until it settles.
22
+ */
23
+ export interface P2PConnectionProbeAdmission {
24
+ run<T>(trialSettings: P2PConnectionProbeSettings, runOwnedTrial: () => Promise<T>): Promise<P2PConnectionProbeAdmissionResult<T>>;
25
+ }
26
+ /**
27
+ * Return whether an active relay set already covers every requested relay.
28
+ *
29
+ * Relay strings are split, trimmed, and de-duplicated. Broader URI
30
+ * canonicalisation would diverge from the key used by the Trystero transport.
31
+ */
32
+ export declare function activeP2PRelayBindingCovers(activeSettings: P2PConnectionProbeSettings, trialSettings: P2PConnectionProbeSettings): boolean;
@@ -0,0 +1,10 @@
1
+ import { splitP2PRelayUrls } from "../../common/models/setting.p2p.js";
2
+ const ACTIVE_P2P_RELAY_BINDING_CONFLICT = "active-p2p-relay-binding-conflict";
3
+ function activeP2PRelayBindingCovers(activeSettings, trialSettings) {
4
+ const activeRelays = new Set(splitP2PRelayUrls(activeSettings.P2P_relays));
5
+ return splitP2PRelayUrls(trialSettings.P2P_relays).every((relay) => activeRelays.has(relay));
6
+ }
7
+ export {
8
+ ACTIVE_P2P_RELAY_BINDING_CONFLICT,
9
+ activeP2PRelayBindingCovers
10
+ };
@@ -0,0 +1,31 @@
1
+ import { type RpcErrorShape } from "../../rpc/index.js";
2
+ import type { P2PReplicationResult } from "./TrysteroReplicator.js";
3
+ /** JSON-safe representation used only while a replication result crosses RPC. */
4
+ export type P2PReplicationWireResult = {
5
+ readonly status: "completed";
6
+ readonly ok: true;
7
+ readonly error?: never;
8
+ } | {
9
+ readonly status: "cancelled";
10
+ readonly ok?: false;
11
+ readonly error?: never;
12
+ } | {
13
+ readonly status: "failed";
14
+ readonly ok?: false;
15
+ readonly error: RpcErrorShape;
16
+ };
17
+ /**
18
+ * Decompose a local result before it enters an `RpcRoom` success envelope.
19
+ *
20
+ * `Error` properties are not enumerable, so JSON would otherwise reduce the
21
+ * failure to `{}` even though the surrounding result reaches the caller.
22
+ */
23
+ export declare function toP2PReplicationWireResult(result: P2PReplicationResult): P2PReplicationWireResult;
24
+ /**
25
+ * Restore the local `Error` contract after a JSON-safe result leaves RPC.
26
+ * Malformed results from an older or incompatible peer remain explicit errors
27
+ * instead of leaking an untyped object into local result handling.
28
+ */
29
+ export declare function fromP2PReplicationWireResult(result: P2PReplicationWireResult): P2PReplicationResult;
30
+ /** Decompose an auxiliary command failure which remains inside successful RPC data. */
31
+ export declare function toP2PWireError(error: unknown): RpcErrorShape;
@@ -0,0 +1,50 @@
1
+ import { RpcError } from "../../rpc/index.js";
2
+ import { asRpcErrorShape } from "../../rpc/errors.js";
3
+ const RPC_ERROR_CODES = /* @__PURE__ */ new Set([
4
+ "TIMEOUT",
5
+ "NOT_CONNECTED",
6
+ "REMOTE_ERROR",
7
+ "CANCELLED",
8
+ "PROTOCOL_ERROR"
9
+ ]);
10
+ function isRpcErrorCode(value) {
11
+ return typeof value === "string" && RPC_ERROR_CODES.has(value);
12
+ }
13
+ function toP2PReplicationWireResult(result) {
14
+ if (result.status !== "failed") return result;
15
+ return { status: "failed", error: asRpcErrorShape(result.error) };
16
+ }
17
+ function fromP2PReplicationWireResult(result) {
18
+ if (result.status !== "failed") return result;
19
+ const wireError = result.error;
20
+ if (!wireError || typeof wireError !== "object" || !("message" in wireError)) {
21
+ return {
22
+ status: "failed",
23
+ error: new RpcError(
24
+ "REMOTE_ERROR",
25
+ "The remote peer reported a replication failure without a usable reason."
26
+ )
27
+ };
28
+ }
29
+ const record = wireError;
30
+ if (typeof record.message !== "string") {
31
+ return {
32
+ status: "failed",
33
+ error: new RpcError(
34
+ "REMOTE_ERROR",
35
+ "The remote peer reported a replication failure without a usable reason."
36
+ )
37
+ };
38
+ }
39
+ const code = isRpcErrorCode(record.code) ? record.code : "REMOTE_ERROR";
40
+ const { message, details } = record;
41
+ return { status: "failed", error: new RpcError(code, message, details) };
42
+ }
43
+ function toP2PWireError(error) {
44
+ return asRpcErrorShape(error);
45
+ }
46
+ export {
47
+ fromP2PReplicationWireResult,
48
+ toP2PReplicationWireResult,
49
+ toP2PWireError
50
+ };
@@ -0,0 +1,32 @@
1
+ import type { ReplicatorHostEnv } from "./types.js";
2
+ import { TrysteroReplicator } from "./TrysteroReplicator.js";
3
+ import { P2PHost } from "./TrysteroReplicatorP2PServer.js";
4
+ /**
5
+ * Narrow operation boundary owned by one P2P room session.
6
+ *
7
+ * A manual stop aborts only current finite operations. Retiring the session
8
+ * aborts the session lifetime, waits for cooperative operations to settle,
9
+ * and then releases the room transport.
10
+ */
11
+ export interface P2PFiniteOperationOwner {
12
+ runFiniteOperation<T>(task: (signal: AbortSignal) => T | PromiseLike<T>, callerSignal?: AbortSignal): Promise<T>;
13
+ }
14
+ /** Owns one P2P room membership and every finite operation bound to it. */
15
+ export declare class P2PRoomSession implements P2PFiniteOperationOwner {
16
+ readonly host: P2PHost;
17
+ readonly replicator: TrysteroReplicator;
18
+ private readonly sessionController;
19
+ private readonly activeFiniteOperations;
20
+ private readonly detachPeerHandlers;
21
+ private acceptingOperations;
22
+ private retirement?;
23
+ constructor(env: ReplicatorHostEnv);
24
+ /** Open the room owned by this session. */
25
+ open(): Promise<void>;
26
+ runFiniteOperation<T>(task: (signal: AbortSignal) => T | PromiseLike<T>, callerSignal?: AbortSignal): Promise<T>;
27
+ /** Request cooperative cancellation of current transfers without leaving the room. */
28
+ cancelActiveTransfers(reason?: unknown): void;
29
+ /** Fence this session, settle its current operations, and release its room. */
30
+ retire(reason?: unknown): Promise<void>;
31
+ private linkOperationController;
32
+ }
@@ -0,0 +1,101 @@
1
+ import { TrysteroReplicator } from "./TrysteroReplicator.js";
2
+ import { P2PHost } from "./TrysteroReplicatorP2PServer.js";
3
+ class P2PRoomSession {
4
+ host;
5
+ replicator;
6
+ sessionController = new AbortController();
7
+ activeFiniteOperations = /* @__PURE__ */ new Set();
8
+ detachPeerHandlers;
9
+ acceptingOperations = true;
10
+ retirement;
11
+ constructor(env) {
12
+ this.host = new P2PHost(env);
13
+ this.replicator = new TrysteroReplicator(env, this.host, this);
14
+ this.detachPeerHandlers = this.host.setSessionPeerHandlers({
15
+ onAdvertisement: async (peer) => {
16
+ if (!this.acceptingOperations) return;
17
+ await this.replicator.onNewPeer(peer);
18
+ },
19
+ onPeerLeft: (peerId) => {
20
+ if (!this.acceptingOperations) return;
21
+ this.replicator.onPeerLeaved(peerId);
22
+ }
23
+ });
24
+ }
25
+ /** Open the room owned by this session. */
26
+ async open() {
27
+ if (!this.acceptingOperations) {
28
+ throw new Error("The P2P room session is retiring.");
29
+ }
30
+ await this.replicator.open();
31
+ }
32
+ async runFiniteOperation(task, callerSignal) {
33
+ if (!this.acceptingOperations) {
34
+ throw new Error("The P2P room session is retiring.");
35
+ }
36
+ const controller = new AbortController();
37
+ const detachSignals = this.linkOperationController(controller, callerSignal);
38
+ const operation = { controller, detachSignals };
39
+ this.activeFiniteOperations.add(operation);
40
+ const settlement = Promise.resolve().then(() => task(controller.signal)).finally(() => {
41
+ detachSignals();
42
+ this.activeFiniteOperations.delete(operation);
43
+ });
44
+ operation.settlement = settlement;
45
+ return await settlement;
46
+ }
47
+ /** Request cooperative cancellation of current transfers without leaving the room. */
48
+ cancelActiveTransfers(reason = new Error("P2P transfer cancelled.")) {
49
+ for (const operation of this.activeFiniteOperations) {
50
+ if (!operation.controller.signal.aborted) {
51
+ operation.controller.abort(reason);
52
+ }
53
+ }
54
+ }
55
+ /** Fence this session, settle its current operations, and release its room. */
56
+ retire(reason = new Error("P2P room session retired.")) {
57
+ this.retirement ??= (async () => {
58
+ this.acceptingOperations = false;
59
+ this.detachPeerHandlers();
60
+ if (!this.sessionController.signal.aborted) {
61
+ this.sessionController.abort(reason);
62
+ }
63
+ this.cancelActiveTransfers(reason);
64
+ await Promise.allSettled(
65
+ [...this.activeFiniteOperations].map((operation) => operation.settlement).filter((settlement) => settlement !== void 0)
66
+ );
67
+ this.replicator.disableBroadcastChanges();
68
+ await this.replicator.dispose();
69
+ })();
70
+ return this.retirement;
71
+ }
72
+ linkOperationController(controller, callerSignal) {
73
+ const listeners = /* @__PURE__ */ new Map();
74
+ const detach = () => {
75
+ for (const [signal, listener] of listeners) {
76
+ signal.removeEventListener("abort", listener);
77
+ }
78
+ listeners.clear();
79
+ };
80
+ const abortFrom = (signal) => {
81
+ detach();
82
+ if (!controller.signal.aborted) {
83
+ controller.abort(signal.reason);
84
+ }
85
+ };
86
+ for (const signal of [this.sessionController.signal, callerSignal]) {
87
+ if (!signal) continue;
88
+ if (signal.aborted) {
89
+ abortFrom(signal);
90
+ break;
91
+ }
92
+ const listener = () => abortFrom(signal);
93
+ listeners.set(signal, listener);
94
+ signal.addEventListener("abort", listener, { once: true });
95
+ }
96
+ return detach;
97
+ }
98
+ }
99
+ export {
100
+ P2PRoomSession
101
+ };