@vrtmrz/livesync-commonlib 0.1.24 → 0.1.26

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 (47) hide show
  1. package/dist/API/processSetting.d.ts +1 -1
  2. package/dist/API/processSetting.js +10 -6
  3. package/dist/common/ConnectionString.js +15 -8
  4. package/dist/common/models/setting.const.qr.js +5 -0
  5. package/dist/common/models/setting.lifecycle.js +5 -3
  6. package/dist/common/models/setting.p2p.d.ts +10 -0
  7. package/dist/common/models/setting.p2p.js +13 -0
  8. package/dist/common/models/setting.type.d.ts +10 -0
  9. package/dist/common/models/tweak.definition.d.ts +5 -0
  10. package/dist/common/settingConstants.d.ts +5 -0
  11. package/dist/common/types.d.ts +3 -3
  12. package/dist/common/types.js +6 -0
  13. package/dist/common/utils.js +4 -1
  14. package/dist/interfaces/DatabaseFileAccess.d.ts +31 -3
  15. package/dist/interfaces/FileReflectionProvenance.d.ts +2 -2
  16. package/dist/managers/EntryManager/EntryManager.d.ts +4 -2
  17. package/dist/managers/EntryManager/EntryManager.js +9 -4
  18. package/dist/managers/EntryManager/EntryManagerImpls.d.ts +12 -4
  19. package/dist/managers/EntryManager/EntryManagerImpls.js +27 -9
  20. package/dist/p2p/P2PService.d.ts +7 -1
  21. package/dist/p2p/P2PService.js +13 -7
  22. package/dist/p2p/index.d.ts +3 -1
  23. package/dist/p2p/index.js +6 -0
  24. package/dist/pouchdb/LiveSyncLocalDB.d.ts +4 -2
  25. package/dist/pouchdb/LiveSyncLocalDB.js +8 -4
  26. package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +22 -1
  27. package/dist/replication/trystero/P2PRoomSessionOwner.js +193 -13
  28. package/dist/replication/trystero/TrysteroReplicator.js +9 -3
  29. package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +1 -0
  30. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +10 -3
  31. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +6 -3
  32. package/dist/replication/trystero/useP2PReplicatorFeature.d.ts +6 -1
  33. package/dist/replication/trystero/useP2PReplicatorFeature.js +7 -4
  34. package/dist/rpc/transports/trysteroUtils.d.ts +2 -2
  35. package/dist/rpc/transports/trysteroUtils.js +32 -2
  36. package/dist/serviceFeatures/remoteConfig.js +3 -0
  37. package/dist/serviceModules/ServiceDatabaseFileAccessBase.d.ts +26 -2
  38. package/dist/serviceModules/ServiceDatabaseFileAccessBase.js +57 -5
  39. package/dist/serviceModules/ServiceFileHandlerBase.d.ts +119 -1
  40. package/dist/serviceModules/ServiceFileHandlerBase.js +450 -70
  41. package/dist/services/base/SettingService.d.ts +1 -0
  42. package/dist/services/base/SettingService.js +37 -9
  43. package/docs/conflict-resolution.md +28 -15
  44. package/docs/development.md +3 -3
  45. package/docs/p2p-transport-lifecycle.md +86 -4
  46. package/docs/proven-in-use.md +31 -1
  47. package/package.json +1 -1
@@ -5,6 +5,7 @@ import { LiveSyncTrysteroReplicator, type LiveSyncTrysteroReplicatorEnv } from "
5
5
  import type { P2PReplicationResult } from "../replication/trystero/TrysteroReplicator.js";
6
6
  import type { AcceptanceDecision, RevokeAcceptanceDecision } from "../replication/trystero/TrysteroReplicatorP2PServer.js";
7
7
  import type { Advertisement } from "../replication/trystero/types.js";
8
+ import { type PrepareP2PSettings } from "../replication/trystero/P2PRoomSessionOwner.js";
8
9
  import { type ReplicationOutcome } from "../replication/ReplicatorProvider.js";
9
10
  import type { P2PConnectionProbeAdmission } from "../replication/trystero/P2PConnectionProbeAdmission.js";
10
11
  export { ACTIVE_P2P_RELAY_BINDING_CONFLICT } from "../replication/trystero/P2PConnectionProbeAdmission.js";
@@ -114,10 +115,15 @@ export interface P2PServiceComposition {
114
115
  /** Create a non-owning adapter for the active Replicator selection. */
115
116
  createActiveReplicator(): ReplicatorInstance;
116
117
  }
118
+ /** Optional host dependencies used by the private P2P service composition. */
119
+ export interface P2PServiceOptions {
120
+ readonly prepareP2PSettings?: PrepareP2PSettings;
121
+ }
122
+ export type { PrepareP2PSettings };
117
123
  /**
118
124
  * Compose stable capability views over one private P2P service context.
119
125
  *
120
126
  * `P2PRoomSessionOwner` remains the resource owner. The returned composition
121
127
  * object is host wiring, not a capability façade for ordinary consumers.
122
128
  */
123
- export declare function createP2PService(env: LiveSyncTrysteroReplicatorEnv): P2PServiceComposition;
129
+ export declare function createP2PService(env: LiveSyncTrysteroReplicatorEnv, options?: P2PServiceOptions): P2PServiceComposition;
@@ -2,7 +2,9 @@ import { getPeerConnectionStats } from "../rpc/transports/DiagRTCPeerConnections
2
2
  import {
3
3
  LiveSyncTrysteroReplicator
4
4
  } from "../replication/trystero/LiveSyncTrysteroReplicator.js";
5
- import { P2PRoomSessionOwner } from "../replication/trystero/P2PRoomSessionOwner.js";
5
+ import {
6
+ P2PRoomSessionOwner
7
+ } from "../replication/trystero/P2PRoomSessionOwner.js";
6
8
  import { compatGlobal } from "../common/coreEnvFunctions.js";
7
9
  import {
8
10
  REPLICATION_CANCELLED,
@@ -75,12 +77,16 @@ function closeForLifecycle(owner, state) {
75
77
  cancelDelayedAutomation(owner, state);
76
78
  return owner.close();
77
79
  }
78
- function reconcileAutoStart(context, settings) {
80
+ async function reconcileAutoStart(context, settings) {
79
81
  if (!settings.P2P_Enabled || !settings.P2P_AutoStart) {
80
- return context.roomSessionOwner.setPersistentDemand("automatic", false);
82
+ await context.roomSessionOwner.setPersistentDemand("automatic", false);
83
+ return;
84
+ }
85
+ if (context.state.explicitDisconnectVeto || context.state.lifecycleClosed) return;
86
+ try {
87
+ await context.roomSessionOwner.setPersistentDemand("automatic", true);
88
+ } catch {
81
89
  }
82
- if (context.state.explicitDisconnectVeto || context.state.lifecycleClosed) return Promise.resolve();
83
- return context.roomSessionOwner.setPersistentDemand("automatic", true);
84
90
  }
85
91
  function scheduleAutoStart(context, delayMs = 100) {
86
92
  clearDelayedAutoStart(context.state);
@@ -224,8 +230,8 @@ function createServiceLifecycle(context) {
224
230
  scheduleAutoStart: (delayMs) => scheduleAutoStart(context, delayMs)
225
231
  };
226
232
  }
227
- function createP2PService(env) {
228
- const roomSessionOwner = new P2PRoomSessionOwner(env);
233
+ function createP2PService(env, options = {}) {
234
+ const roomSessionOwner = new P2PRoomSessionOwner(env, void 0, options);
229
235
  const state = {
230
236
  explicitDisconnectVeto: false,
231
237
  lifecycleClosed: false,
@@ -1,4 +1,4 @@
1
- export type { P2PCandidateSummary, P2PChangeRelay, P2PConfigurationExchange, P2PDiagnostics, P2PPeerConnectionMetrics, P2PPeerAdmission, P2PPeerDirectory, P2PServiceViews, P2PTargetedTransfer, P2PTransportLifecycle, P2PConnectionProbeAdmission, P2PConnectionProbeAdmissionResult, P2PConnectionProbeSettings, } from "./P2PService.js";
1
+ export type { P2PCandidateSummary, P2PChangeRelay, P2PConfigurationExchange, P2PDiagnostics, P2PPeerConnectionMetrics, P2PPeerAdmission, P2PPeerDirectory, P2PServiceViews, P2PTargetedTransfer, P2PTransportLifecycle, P2PConnectionProbeAdmission, P2PConnectionProbeAdmissionResult, P2PConnectionProbeSettings, P2PServiceOptions, PrepareP2PSettings, } from "./P2PService.js";
2
2
  export { ACTIVE_P2P_RELAY_BINDING_CONFLICT } from "./P2PService.js";
3
3
  export { useP2PReplicatorCommands } from "../replication/trystero/useP2PReplicatorCommands.js";
4
4
  export { useP2PReplicatorFeature } from "../replication/trystero/useP2PReplicatorFeature.js";
@@ -6,3 +6,5 @@ export type { OpenRebuildUIFactory, OpenReplicationUIFactory, } from "../replica
6
6
  export type { UseP2PReplicatorResult } from "../replication/trystero/UseP2PReplicatorResult.js";
7
7
  export type { P2PReplicationResult } from "../replication/trystero/TrysteroReplicator.js";
8
8
  export type { Advertisement } from "../replication/trystero/types.js";
9
+ export { P2P_ICE_SERVER_EXPIRY_MARGIN_MS, P2P_SETTINGS_PREPARATION_TIMEOUT_MS, } from "../replication/trystero/P2PRoomSessionOwner.js";
10
+ export type { P2PReplicatorFeatureOptions } from "../replication/trystero/useP2PReplicatorFeature.js";
package/dist/p2p/index.js CHANGED
@@ -1,8 +1,14 @@
1
1
  import { ACTIVE_P2P_RELAY_BINDING_CONFLICT } from "./P2PService.js";
2
2
  import { useP2PReplicatorCommands } from "../replication/trystero/useP2PReplicatorCommands.js";
3
3
  import { useP2PReplicatorFeature } from "../replication/trystero/useP2PReplicatorFeature.js";
4
+ import {
5
+ P2P_ICE_SERVER_EXPIRY_MARGIN_MS,
6
+ P2P_SETTINGS_PREPARATION_TIMEOUT_MS
7
+ } from "../replication/trystero/P2PRoomSessionOwner.js";
4
8
  export {
5
9
  ACTIVE_P2P_RELAY_BINDING_CONFLICT,
10
+ P2P_ICE_SERVER_EXPIRY_MARGIN_MS,
11
+ P2P_SETTINGS_PREPARATION_TIMEOUT_MS,
6
12
  useP2PReplicatorCommands,
7
13
  useP2PReplicatorFeature
8
14
  };
@@ -196,12 +196,14 @@ export declare class LiveSyncLocalDB {
196
196
  bulkDocsRaw<T extends EntryDoc>(docs: Array<PouchDB.Core.PutDocument<T>>, options?: PouchDB.Core.BulkDocsOptions): Promise<Array<PouchDB.Core.Response | PouchDB.Core.Error>>;
197
197
  isTargetFile(filenameSrc: string): boolean;
198
198
  getDBEntryMeta(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, includeDeleted?: boolean): Promise<false | LoadedEntry>;
199
- getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise<false | LoadedEntry>;
200
- getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise<false | LoadedEntry>;
199
+ getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean, localOnly?: boolean): Promise<false | LoadedEntry>;
200
+ getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean, localOnly?: boolean): Promise<false | LoadedEntry>;
201
201
  deleteDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise<boolean>;
202
202
  storeDeletionAtRevision(path: FilePathWithPrefix | FilePath, baseRevision: string): Promise<false | PouchDB.Core.Response>;
203
203
  putDBEntry(note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise<false | PouchDB.Core.Response>;
204
204
  putDBEntryWithLiveBaseRevision(note: SavingEntry, baseRevision: string, onlyChunks?: boolean): Promise<false | PouchDB.Core.Response>;
205
+ /** Preserve existing branches and delegate a fresh root write to the entry manager. */
206
+ putDBEntryAsIndependentRoot(note: SavingEntry): Promise<false | PouchDB.Core.Response>;
205
207
  getConflictedDoc(path: FilePathWithPrefix, rev: string): Promise<false | diff_result_leaf>;
206
208
  tryAutoMerge(path: FilePathWithPrefix, enableMarkdownAutoMerge: boolean): AutoMergeResult;
207
209
  }
@@ -457,11 +457,11 @@ class LiveSyncLocalDB {
457
457
  async getDBEntryMeta(path, opt, includeDeleted = false) {
458
458
  return await this.managers.entryManager.getDBEntryMeta(path, opt, includeDeleted);
459
459
  }
460
- async getDBEntry(path, opt, dump = false, waitForReady = true, includeDeleted = false) {
461
- return await this.managers.entryManager.getDBEntry(path, opt, dump, waitForReady, includeDeleted);
460
+ async getDBEntry(path, opt, dump = false, waitForReady = true, includeDeleted = false, localOnly = false) {
461
+ return await this.managers.entryManager.getDBEntry(path, opt, dump, waitForReady, includeDeleted, localOnly);
462
462
  }
463
- async getDBEntryFromMeta(meta, dump = false, waitForReady = true) {
464
- return await this.managers.entryManager.getDBEntryFromMeta(meta, dump, waitForReady);
463
+ async getDBEntryFromMeta(meta, dump = false, waitForReady = true, localOnly = false) {
464
+ return await this.managers.entryManager.getDBEntryFromMeta(meta, dump, waitForReady, localOnly);
465
465
  }
466
466
  async deleteDBEntry(path, opt) {
467
467
  return await this.managers.entryManager.deleteDBEntry(path, opt);
@@ -475,6 +475,10 @@ class LiveSyncLocalDB {
475
475
  async putDBEntryWithLiveBaseRevision(note, baseRevision, onlyChunks) {
476
476
  return await this.managers.entryManager.putDBEntryWithLiveBaseRevision(note, baseRevision, onlyChunks);
477
477
  }
478
+ /** Preserve existing branches and delegate a fresh root write to the entry manager. */
479
+ async putDBEntryAsIndependentRoot(note) {
480
+ return await this.managers.entryManager.putDBEntryAsIndependentRoot(note);
481
+ }
478
482
  async getConflictedDoc(path, rev) {
479
483
  return await this.managers.conflictManager.getConflictedDoc(path, rev);
480
484
  }
@@ -1,8 +1,16 @@
1
+ import { type P2PSyncSetting } from "../../common/types.js";
1
2
  import type { LiveSyncReplicatorEnv } from "../LiveSyncAbstractReplicator.js";
2
3
  import { P2PRoomSession } from "./P2PRoomSession.js";
3
4
  import type { ReplicatorHostEnv } from "./types.js";
4
5
  import { type P2PConnectionProbeAdmissionResult, type P2PConnectionProbeSettings } from "./P2PConnectionProbeAdmission.js";
5
6
  type P2PRoomSessionFactory = (env: ReplicatorHostEnv) => P2PRoomSession;
7
+ /** Prepare connection-only P2P settings for one room generation. */
8
+ export type PrepareP2PSettings = (settings: Readonly<P2PSyncSetting>, signal: AbortSignal) => Promise<P2PSyncSetting>;
9
+ export interface P2PRoomSessionOwnerOptions {
10
+ readonly prepareP2PSettings?: PrepareP2PSettings;
11
+ }
12
+ export declare const P2P_SETTINGS_PREPARATION_TIMEOUT_MS = 30000;
13
+ export declare const P2P_ICE_SERVER_EXPIRY_MARGIN_MS = 30000;
6
14
  /** Session operations shared by the stable service and its compatibility facade. */
7
15
  export interface P2PRoomSessionAccess {
8
16
  readonly currentSession: P2PRoomSession | undefined;
@@ -22,13 +30,16 @@ export type P2PPersistentRoomDemand = "explicit" | "automatic" | "rebuild-contin
22
30
  export declare class P2PRoomSessionOwner implements P2PRoomSessionAccess {
23
31
  private readonly env;
24
32
  private readonly createSession;
33
+ private readonly options;
25
34
  private current?;
26
35
  private lifecycleOperation;
27
36
  private readonly persistentDemands;
28
37
  private readonly finiteDemands;
29
38
  private readonly automationCoordinator;
30
39
  private activeBinding?;
31
- constructor(env: LiveSyncReplicatorEnv, createSession?: P2PRoomSessionFactory);
40
+ private pendingPreparation?;
41
+ private runtimeGeneration;
42
+ constructor(env: LiveSyncReplicatorEnv, createSession?: P2PRoomSessionFactory, options?: P2PRoomSessionOwnerOptions);
32
43
  get currentSession(): P2PRoomSession | undefined;
33
44
  get isConnected(): boolean;
34
45
  cancelActiveTransfers(): void;
@@ -52,6 +63,16 @@ export declare class P2PRoomSessionOwner implements P2PRoomSessionAccess {
52
63
  private enqueueLifecycleOperation;
53
64
  private closeTransport;
54
65
  private bindingsMatch;
66
+ private connectionSettingsRemainUsable;
67
+ private fencePreparationForCurrentSettings;
68
+ private invalidatePreparation;
69
+ private prepareConnectionSettings;
70
+ private prepareWithDeadline;
71
+ private copyPreparedIceServers;
72
+ private hasUsableIceServer;
73
+ private hasUsableTurnServer;
74
+ private withRoomOpenDeadline;
75
+ private assertReconciliationCurrent;
55
76
  /** Capture immutable session inputs while keeping policy live. */
56
77
  private getEffectiveBinding;
57
78
  /** Apply policy which can change without retiring the room session. */
@@ -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
- candidate = this.createSession(this.buildSessionEnv(binding));
130
- await candidate.open();
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 = binding;
163
+ this.activeBinding = connectionBinding;
141
164
  this.reconcileCurrentSessionPolicy(candidate);
142
165
  } catch (error) {
143
166
  await candidate?.retire(error).catch((retirementError) => {
144
- Logger(retirementError, LOG_LEVEL_VERBOSE);
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,7 @@
1
- import { TweakValuesShouldMatchedTemplate } from "../../common/types.js";
1
+ import {
2
+ TweakValuesShouldMatchedTemplate,
3
+ omitP2PRuntimeSettings
4
+ } from "../../common/types.js";
2
5
  import { assessTweakCompatibility } from "../../common/models/tweak.compatibility.js";
3
6
  import {
4
7
  LOG_LEVEL_INFO,
@@ -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);
@@ -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
- Logger(ex, LOG_LEVEL_VERBOSE);
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
- Logger(ex, LOG_LEVEL_VERBOSE);
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
- Logger(error, LOG_LEVEL_VERBOSE);
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
- services: host.services
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) => {