@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
@@ -10,22 +10,29 @@ class TrysteroReplicatorP2PClient {
10
10
  get remoteDB() {
11
11
  return this._remoteDB;
12
12
  }
13
+ /** Bind the reverse database RPC calls to one finite-operation signal. */
14
+ getRemoteDB(signal) {
15
+ return signal ? this._bindRemoteDB(signal) : this._remoteDB;
16
+ }
13
17
  constructor(server, connectedPeerId) {
14
18
  this._server = server;
15
19
  this._connectedPeerId = connectedPeerId;
16
20
  this._remoteDB = this._bindRemoteDB();
17
21
  }
18
- _bindRemoteDB() {
22
+ _bindRemoteDB(signal) {
19
23
  return {
20
- info: this.bindRemoteFunction("info"),
21
- changes: this.bindRemoteFunction("changes"),
22
- revsDiff: this.bindRemoteFunction("revsDiff"),
23
- bulkDocs: this.bindRemoteFunction("bulkDocs"),
24
- bulkGet: this.bindRemoteFunction("bulkGet", BULK_GET_RPC_TIMEOUT),
25
- put: this.bindRemoteFunction("put"),
26
- get: this.bindRemoteFunction("get")
24
+ info: this.bindRemoteFunction("info", DEFAULT_RPC_TIMEOUT, signal),
25
+ changes: this.bindRemoteFunction("changes", DEFAULT_RPC_TIMEOUT, signal),
26
+ revsDiff: this.bindRemoteFunction("revsDiff", DEFAULT_RPC_TIMEOUT, signal),
27
+ bulkDocs: this.bindRemoteFunction("bulkDocs", DEFAULT_RPC_TIMEOUT, signal),
28
+ bulkGet: this.bindRemoteFunction("bulkGet", BULK_GET_RPC_TIMEOUT, signal),
29
+ put: this.bindRemoteFunction("put", DEFAULT_RPC_TIMEOUT, signal),
30
+ get: this.bindRemoteFunction("get", DEFAULT_RPC_TIMEOUT, signal)
27
31
  };
28
32
  }
33
+ getCallOptions(timeout, signal) {
34
+ return { timeoutMs: timeout, signal };
35
+ }
29
36
  _sendRPC(type, args, timeout = DEFAULT_RPC_TIMEOUT) {
30
37
  const room = this._server?.rpcRoom;
31
38
  if (!room) {
@@ -36,21 +43,21 @@ class TrysteroReplicatorP2PClient {
36
43
  }
37
44
  __onResponse(_data) {
38
45
  }
39
- bindRemoteFunction(type, timeout = DEFAULT_RPC_TIMEOUT) {
46
+ bindRemoteFunction(type, timeout = DEFAULT_RPC_TIMEOUT, signal) {
40
47
  return async (...args) => {
41
48
  const room = this._server?.rpcRoom;
42
49
  if (!room) {
43
50
  throw new Error("Not connected to any room");
44
51
  }
45
- return await room.session(this._connectedPeerId).call(toRpcMethodName(type), args, timeout);
52
+ return await room.session(this._connectedPeerId).call(toRpcMethodName(type), args, this.getCallOptions(timeout, signal));
46
53
  };
47
54
  }
48
- async invokeRemoteFunction(type, args, timeout = DEFAULT_RPC_TIMEOUT) {
55
+ async invokeRemoteFunction(type, args, timeout = DEFAULT_RPC_TIMEOUT, signal) {
49
56
  const room = this._server?.rpcRoom;
50
57
  if (!room) {
51
58
  throw new Error("Not connected to any room");
52
59
  }
53
- return await room.session(this._connectedPeerId).call(toRpcMethodName(type), args, timeout);
60
+ return await room.session(this._connectedPeerId).call(toRpcMethodName(type), args, this.getCallOptions(timeout, signal));
54
61
  }
55
62
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic function binder, so we can't be more specific about the types here.
56
63
  bindRemoteObjectFunctions(key, timeout = DEFAULT_RPC_TIMEOUT) {
@@ -63,12 +70,12 @@ class TrysteroReplicatorP2PClient {
63
70
  };
64
71
  }
65
72
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic function binder, so we can't be more specific about the types here.
66
- async invokeRemoteObjectFunction(key, args, timeout = DEFAULT_RPC_TIMEOUT) {
73
+ async invokeRemoteObjectFunction(key, args, timeout = DEFAULT_RPC_TIMEOUT, signal) {
67
74
  const room = this._server?.rpcRoom;
68
75
  if (!room) {
69
76
  throw new Error("Not connected to any room");
70
77
  }
71
- return await room.session(this._connectedPeerId).call(toRpcMethodName(key.toString()), args, timeout);
78
+ return await room.session(this._connectedPeerId).call(toRpcMethodName(key.toString()), args, this.getCallOptions(timeout, signal));
72
79
  }
73
80
  close() {
74
81
  this._remoteDB = void 0;
@@ -4,7 +4,7 @@ import { type ReplicatorHostEnv, type FullFilledDeviceInfo, type Request, type R
4
4
  import { StoredMapLike } from "../../dataobject/StoredMap.js";
5
5
  import { TrysteroReplicatorP2PClient } from "./TrysteroReplicatorP2PClient.js";
6
6
  import { Computed } from "octagonal-wheels/dataobject/Computed";
7
- import { RpcRoom, type JsonLike } from "../../rpc/index.js";
7
+ import { RpcRoom, type JsonLike, type RpcRequestContext } from "../../rpc/index.js";
8
8
  import { type DiagRTCStats } from "../../rpc/transports/DiagRTCPeerConnections.types.js";
9
9
  export type PeerInfo = Advertisement & {
10
10
  isAccepted: boolean | undefined;
@@ -20,6 +20,8 @@ export type RevokeAcceptanceDecision = {
20
20
  peerId: string;
21
21
  name: string;
22
22
  };
23
+ /** Non-interactive result of evaluating the persisted and automatic peer policy. */
24
+ export type P2PPeerAcceptance = "accepted" | "rejected" | "undecided" | "unknown";
23
25
  export type P2PServerInfo = {
24
26
  isConnected: boolean;
25
27
  knownAdvertisements: PeerInfo[];
@@ -27,6 +29,11 @@ export type P2PServerInfo = {
27
29
  roomId: string;
28
30
  diag: DiagRTCStats;
29
31
  };
32
+ /** Peer callbacks bound to one room session rather than the global event hub. */
33
+ export interface P2PSessionPeerHandlers {
34
+ onAdvertisement(peer: Advertisement): void | Promise<void>;
35
+ onPeerLeft(peerId: string): void;
36
+ }
30
37
  export declare const EVENT_SERVER_STATUS = "p2p-server-status";
31
38
  export declare const EVENT_MAKE_DECISION = "make-decision-p2p-peer";
32
39
  export declare const EVENT_REVOKE_DECISION = "revoke-decision-p2p-peer";
@@ -63,6 +70,9 @@ export declare class TrysteroReplicatorP2PServer {
63
70
  _rpcRoom?: RpcRoom;
64
71
  protected _peerStatusEventCleanup: (() => void) | undefined;
65
72
  protected _peerFailureAnalysisCleanup: (() => void) | undefined;
73
+ private _platformUnloadCleanup;
74
+ private _sessionPeerHandlers;
75
+ private _hostLifetimeEnded;
66
76
  protected _peerConnectionEventCleanup(): void;
67
77
  _diagStats: DiagRTCStats;
68
78
  get isDisposed(): boolean;
@@ -70,8 +80,12 @@ export declare class TrysteroReplicatorP2PServer {
70
80
  ensureLeaved(): Promise<void>;
71
81
  setRoom(room: Room): Promise<void>;
72
82
  shutdown(): Promise<void>;
83
+ /** Release subscriptions whose lifetime is the host object rather than one transport join. */
84
+ dispose(): void;
73
85
  dispatchConnectionStatus(): Promise<void>;
74
86
  constructor(env: ReplicatorHostEnv, _serverPeerId?: string);
87
+ /** Attach callbacks owned by the current room session and return their disposer. */
88
+ setSessionPeerHandlers(handlers: P2PSessionPeerHandlers): () => void;
75
89
  makeDecision(decision: AcceptanceDecision): Promise<void>;
76
90
  revokeDecision(decision: RevokeAcceptanceDecision): Promise<void>;
77
91
  get room(): Room;
@@ -79,6 +93,8 @@ export declare class TrysteroReplicatorP2PServer {
79
93
  get db(): PouchDB.Database<import("../../common/types.js").EntryDoc>;
80
94
  get confirm(): import("../../interfaces/Confirm.js").Confirm;
81
95
  get settings(): P2PSyncSetting;
96
+ /** Latest admission and enablement policy for this room. */
97
+ get currentSettings(): P2PSyncSetting;
82
98
  get isEnabled(): boolean;
83
99
  get deviceInfo(): FullFilledDeviceInfo;
84
100
  _sendAdvertisement?: (data: Advertisement, peerId?: string) => Promise<void>;
@@ -92,20 +108,27 @@ export declare class TrysteroReplicatorP2PServer {
92
108
  _confirmUserToAccept(peerId: string): Promise<boolean>;
93
109
  _acceptablePeers: Computed<[settings: P2PSyncSetting], RegExp[]>;
94
110
  _shouldDenyPeers: Computed<[settings: P2PSyncSetting], RegExp[]>;
111
+ /**
112
+ * Evaluate peer admission without opening a dialogue or changing a stored
113
+ * decision. Unattended automation uses this boundary before starting RPC.
114
+ */
115
+ evaluatePeerAcceptance(peerId: string): Promise<P2PPeerAcceptance>;
95
116
  isAcceptablePeer(peerId: string): Promise<boolean>;
96
117
  __send(data: Payload, peerId: string): Promise<void>;
97
118
  processArrivedRPC(data: Payload, peerId: string): Promise<void>;
98
119
  private _onPeerJoin;
99
120
  private _onPeerLeave;
100
121
  onAfterJoinRoom(): void;
101
- startService(bindings?: BindableObject[]): Promise<void>;
102
- start(bindings?: BindableObject[]): Promise<void>;
122
+ startService(bindings?: BindableObject[], beforeAdvertisement?: () => void): Promise<void>;
123
+ start(bindings?: BindableObject[], beforeAdvertisement?: () => void): Promise<void>;
103
124
  /**
104
125
  * @deprecated Use serveFunction or serveObject instead. This is only for backward compatibility and may be removed in the future.
105
126
  * @param type
106
127
  * @param func
107
128
  */
108
129
  serveFunction<T extends JsonLike[], U>(type: string, func: (peerId: string, ...args: T) => U | Promise<U>): void;
130
+ /** Serve one function whose finite work can observe caller cancellation. */
131
+ serveCancellationAwareFunction<T extends JsonLike[], U>(type: string, func: (context: RpcRequestContext, peerId: string, ...args: T) => U | Promise<U>): void;
109
132
  serveObject<T>(obj: BindableObject<T>): void;
110
133
  __onResponse(data: Response, peerId: string): void;
111
134
  __onRequest(data: Request, peerId: string): Promise<void>;
@@ -1,8 +1,9 @@
1
1
  import { selfId, joinRoom } from "@trystero-p2p/nostr";
2
- import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE } from "../../common/types.js";
2
+ import { AutoAccepting, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE } from "../../common/types.js";
3
3
  import { LOG_LEVEL_VERBOSE, Logger } from "../../common/logger.js";
4
4
  import {
5
5
  DIRECTION_RESPONSE,
6
+ resolveCurrentP2PSettings,
6
7
  ResponsePreventedError,
7
8
  DIRECTION_REQUEST
8
9
  } from "./types.js";
@@ -19,6 +20,7 @@ import { toRpcMethodName } from "./rpcCompat.js";
19
20
  import { generateJoinRoomOptions } from "../../rpc/transports/trysteroUtils.js";
20
21
  import { subscribeConnectionStatus, subscribeFailureDiagnosis } from "../../rpc/transports/DiagRTCPeerConnections.js";
21
22
  import {} from "../../rpc/transports/DiagRTCPeerConnections.types.js";
23
+ import { asRpcErrorShape } from "../../rpc/errors.js";
22
24
  const EVENT_SERVER_STATUS = "p2p-server-status";
23
25
  const EVENT_MAKE_DECISION = "make-decision-p2p-peer";
24
26
  const EVENT_REVOKE_DECISION = "revoke-decision-p2p-peer";
@@ -42,6 +44,9 @@ class TrysteroReplicatorP2PServer {
42
44
  _rpcRoom;
43
45
  _peerStatusEventCleanup = void 0;
44
46
  _peerFailureAnalysisCleanup = void 0;
47
+ _platformUnloadCleanup;
48
+ _sessionPeerHandlers;
49
+ _hostLifetimeEnded = false;
45
50
  _peerConnectionEventCleanup() {
46
51
  if (this._peerStatusEventCleanup) {
47
52
  this._peerStatusEventCleanup();
@@ -95,7 +100,15 @@ class TrysteroReplicatorP2PServer {
95
100
  Logger(ex, LOG_LEVEL_VERBOSE);
96
101
  }
97
102
  }
103
+ /** Release subscriptions whose lifetime is the host object rather than one transport join. */
104
+ dispose() {
105
+ this._hostLifetimeEnded = true;
106
+ this._platformUnloadCleanup?.();
107
+ this._platformUnloadCleanup = void 0;
108
+ this._sessionPeerHandlers = void 0;
109
+ }
98
110
  async dispatchConnectionStatus() {
111
+ if (this._hostLifetimeEnded) return;
99
112
  const adsTasks = [...this.knownAdvertisements].map(async (e) => {
100
113
  const isAccepted = await this.acceptedPeers.get(e.name);
101
114
  const isTemporaryAccepted = this.temporaryAcceptedPeers.get(e.peerId);
@@ -106,6 +119,7 @@ class TrysteroReplicatorP2PServer {
106
119
  };
107
120
  });
108
121
  const ads = await Promise.all(adsTasks);
122
+ if (this._hostLifetimeEnded) return;
109
123
  this._env.events.emitEvent(EVENT_SERVER_STATUS, {
110
124
  isConnected: this.isServing,
111
125
  knownAdvertisements: ads,
@@ -117,7 +131,7 @@ class TrysteroReplicatorP2PServer {
117
131
  constructor(env, _serverPeerId = selfId) {
118
132
  this._env = env;
119
133
  this._serverPeerId = _serverPeerId;
120
- this._env.events.onEvent(EVENT_PLATFORM_UNLOADED, () => {
134
+ this._platformUnloadCleanup = this._env.events.onEvent(EVENT_PLATFORM_UNLOADED, () => {
121
135
  void this.shutdown();
122
136
  });
123
137
  this.acceptedPeers = new StoredMapLike(
@@ -125,6 +139,15 @@ class TrysteroReplicatorP2PServer {
125
139
  "p2p-device-decisions"
126
140
  );
127
141
  }
142
+ /** Attach callbacks owned by the current room session and return their disposer. */
143
+ setSessionPeerHandlers(handlers) {
144
+ this._sessionPeerHandlers = handlers;
145
+ return () => {
146
+ if (this._sessionPeerHandlers === handlers) {
147
+ this._sessionPeerHandlers = void 0;
148
+ }
149
+ };
150
+ }
128
151
  async makeDecision(decision) {
129
152
  if (decision.decision) {
130
153
  if (decision.isTemporary) {
@@ -161,8 +184,12 @@ class TrysteroReplicatorP2PServer {
161
184
  get settings() {
162
185
  return this._env.settings;
163
186
  }
187
+ /** Latest admission and enablement policy for this room. */
188
+ get currentSettings() {
189
+ return resolveCurrentP2PSettings(this._env);
190
+ }
164
191
  get isEnabled() {
165
- return this.settings.P2P_Enabled;
192
+ return this.currentSettings.P2P_Enabled;
166
193
  }
167
194
  get deviceInfo() {
168
195
  return {
@@ -173,7 +200,6 @@ class TrysteroReplicatorP2PServer {
173
200
  };
174
201
  }
175
202
  _sendAdvertisement;
176
- // _advertisementTimer?: ReturnType<typeof setInterval>;
177
203
  sendAdvertisement(peerId) {
178
204
  if (!this.isEnabled) return;
179
205
  const devInfo = this.deviceInfo;
@@ -187,28 +213,12 @@ class TrysteroReplicatorP2PServer {
187
213
  void this._sendAdvertisement(data, peerId);
188
214
  }
189
215
  }
190
- // startAdvertisementBroadcast() {
191
- // if (this._advertisementTimer) {
192
- // clearInterval(this._advertisementTimer);
193
- // }
194
- // this._advertisementTimer = setInterval(() => {
195
- // if (!this.isServing || !this.isEnabled) {
196
- // return;
197
- // }
198
- // this.sendAdvertisement();
199
- // }, ADVERTISEMENT_REBROADCAST_INTERVAL_MS);
200
- // }
201
- // stopAdvertisementBroadcast() {
202
- // if (this._advertisementTimer) {
203
- // clearInterval(this._advertisementTimer);
204
- // this._advertisementTimer = undefined;
205
- // }
206
- // }
207
216
  _knownAdvertisements = /* @__PURE__ */ new Map();
208
217
  get knownAdvertisements() {
209
218
  return [...this._knownAdvertisements.values()];
210
219
  }
211
220
  onAdvertisement(data, peerId) {
221
+ if (this._hostLifetimeEnded) return;
212
222
  if (!this.isEnabled) return;
213
223
  Logger(`Advertisement from ${peerId}`, LOG_LEVEL_VERBOSE);
214
224
  if (peerId === this.serverPeerId) return;
@@ -216,6 +226,7 @@ class TrysteroReplicatorP2PServer {
216
226
  if (data.peerId !== peerId) return;
217
227
  this._knownAdvertisements.set(peerId, data);
218
228
  void this.dispatchConnectionStatus();
229
+ void Promise.resolve().then(() => this._sessionPeerHandlers?.onAdvertisement(data)).catch((error) => Logger(error, LOG_LEVEL_VERBOSE));
219
230
  void this._env.events.emitEvent(EVENT_ADVERTISEMENT_RECEIVED, data);
220
231
  }
221
232
  acceptedPeers;
@@ -278,25 +289,40 @@ You can chose as follows:
278
289
  return `${settings?.P2P_AutoDenyingPeers ?? ""}`.split(",").map((e) => e.trim()).filter((e) => !!e).map((e) => e.startsWith("~") ? new RegExp(e.substring(1), "i") : new RegExp(`^${e}$`, "i"));
279
290
  }
280
291
  });
281
- async isAcceptablePeer(peerId) {
282
- if (!this.isEnabled) return void 0;
292
+ /**
293
+ * Evaluate peer admission without opening a dialogue or changing a stored
294
+ * decision. Unattended automation uses this boundary before starting RPC.
295
+ */
296
+ async evaluatePeerAcceptance(peerId) {
297
+ if (!this.isEnabled) return "unknown";
283
298
  const peerInfo = this._knownAdvertisements.get(peerId);
284
- if (!peerInfo) return false;
299
+ if (!peerInfo) return "unknown";
285
300
  const peerName = peerInfo.name;
286
- if (this.temporaryAcceptedPeers.has(peerId)) return this.temporaryAcceptedPeers.get(peerId);
301
+ if (this.temporaryAcceptedPeers.has(peerId)) {
302
+ return this.temporaryAcceptedPeers.get(peerId) ? "accepted" : "rejected";
303
+ }
287
304
  const accepted = await this.acceptedPeers.get(peerName);
288
- if (accepted !== void 0 && accepted !== null) return accepted;
289
- const isAcceptable = (await this._acceptablePeers.update(this.settings)).value.some((e) => e.test(peerName));
290
- const isDeny = (await this._shouldDenyPeers.update(this.settings)).value.some((e) => e.test(peerName));
291
- if (isAcceptable) {
292
- if (isDeny) return false;
293
- this.temporaryAcceptedPeers.set(peerId, true);
294
- void this.dispatchConnectionStatus();
305
+ if (accepted !== void 0 && accepted !== null) return accepted ? "accepted" : "rejected";
306
+ const currentSettings = { ...this.currentSettings };
307
+ const isAcceptable = (await this._acceptablePeers.update(currentSettings)).value.some((e) => e.test(peerName));
308
+ const isDeny = (await this._shouldDenyPeers.update(currentSettings)).value.some((e) => e.test(peerName));
309
+ if (isDeny) return "rejected";
310
+ if (isAcceptable || currentSettings.P2P_AutoAccepting === AutoAccepting.ALL) return "accepted";
311
+ return "undecided";
312
+ }
313
+ async isAcceptablePeer(peerId) {
314
+ const acceptance = await this.evaluatePeerAcceptance(peerId);
315
+ if (acceptance === "accepted") {
316
+ const peerName = this._knownAdvertisements.get(peerId)?.name;
317
+ const persisted = peerName ? await this.acceptedPeers.get(peerName) : void 0;
318
+ if (!this.temporaryAcceptedPeers.has(peerId) && persisted == null) {
319
+ this.temporaryAcceptedPeers.set(peerId, true);
320
+ void this.dispatchConnectionStatus();
321
+ }
295
322
  return true;
296
323
  }
297
- if (this.settings.P2P_IsHeadless) {
298
- return false;
299
- }
324
+ if (acceptance === "rejected" || acceptance === "unknown") return false;
325
+ if (this.currentSettings.P2P_IsHeadless) return false;
300
326
  return await this.confirmUserToAccept(peerId);
301
327
  }
302
328
  async __send(data, peerId) {
@@ -330,6 +356,7 @@ You can chose as follows:
330
356
  }
331
357
  }
332
358
  _onPeerJoin(peerId) {
359
+ if (this._hostLifetimeEnded) return;
333
360
  if (!this._room) {
334
361
  Logger(`Received peer join event from ${peerId}, but no active room. Ignoring.`, LOG_LEVEL_VERBOSE);
335
362
  return;
@@ -338,8 +365,10 @@ You can chose as follows:
338
365
  this.sendAdvertisement(peerId);
339
366
  }
340
367
  _onPeerLeave(peerId) {
368
+ if (this._hostLifetimeEnded) return;
341
369
  Logger(`Peer left: ${peerId}`, LOG_LEVEL_VERBOSE);
342
370
  this._knownAdvertisements.delete(peerId);
371
+ void Promise.resolve().then(() => this._sessionPeerHandlers?.onPeerLeft(peerId)).catch((error) => Logger(error, LOG_LEVEL_VERBOSE));
343
372
  void this._env.events.emitEvent(EVENT_DEVICE_LEAVED, peerId);
344
373
  void this.dispatchConnectionStatus();
345
374
  }
@@ -397,7 +426,7 @@ You can chose as follows:
397
426
  this._env.events.emitEvent(EVENT_P2P_CONNECTED);
398
427
  void this.dispatchConnectionStatus();
399
428
  }
400
- async startService(bindings = []) {
429
+ async startService(bindings = [], beforeAdvertisement) {
401
430
  if (!this.isEnabled) {
402
431
  Logger(this._env.translate("P2P.NotEnabled"), LOG_LEVEL_NOTICE);
403
432
  return;
@@ -407,9 +436,10 @@ You can chose as follows:
407
436
  this._bindingObjects.forEach((b) => {
408
437
  this.serveObject(b);
409
438
  });
439
+ beforeAdvertisement?.();
410
440
  await Promise.resolve(this.sendAdvertisement());
411
441
  }
412
- async start(bindings = []) {
442
+ async start(bindings = [], beforeAdvertisement) {
413
443
  await this.shutdown();
414
444
  if (!this.settings.P2P_Enabled) {
415
445
  Logger(this._env.translate("P2P.NotEnabled"), LOG_LEVEL_NOTICE);
@@ -436,7 +466,7 @@ You can chose as follows:
436
466
  this._activeRoomId = roomId;
437
467
  this.onAfterJoinRoom();
438
468
  void this.dispatchConnectionStatus();
439
- await this.startService(bindings);
469
+ await this.startService(bindings, beforeAdvertisement);
440
470
  }
441
471
  /**
442
472
  * @deprecated Use serveFunction or serveObject instead. This is only for backward compatibility and may be removed in the future.
@@ -449,6 +479,13 @@ You can chose as follows:
449
479
  return await Promise.resolve(func.apply(this, [peerId, ...args]));
450
480
  });
451
481
  }
482
+ /** Serve one function whose finite work can observe caller cancellation. */
483
+ serveCancellationAwareFunction(type, func) {
484
+ this._rpcRoom?.registerCancellable(
485
+ toRpcMethodName(type),
486
+ async (context, peerId, ...args) => await Promise.resolve(func(context, peerId, ...args))
487
+ );
488
+ }
452
489
  serveObject(obj) {
453
490
  const keys = Object.keys(obj);
454
491
  keys.forEach((key) => {
@@ -483,7 +520,15 @@ You can chose as follows:
483
520
  Logger(`Serving function: [FAILED] ${data.type} sending back the failure information`, LOG_LEVEL_VERBOSE);
484
521
  Logger(e instanceof Error ? e.message : e, LOG_LEVEL_VERBOSE);
485
522
  await this.__send(
486
- { type: data.type, seq: data.seq, direction: DIRECTION_RESPONSE, data: void 0, error: e },
523
+ {
524
+ type: data.type,
525
+ seq: data.seq,
526
+ direction: DIRECTION_RESPONSE,
527
+ data: void 0,
528
+ // This deprecated path predates RpcRoom, but it has the same
529
+ // JSON boundary and therefore must not send a native Error.
530
+ error: asRpcErrorShape(e)
531
+ },
487
532
  peerId
488
533
  );
489
534
  }
@@ -1,18 +1,22 @@
1
1
  import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
2
2
  import type { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator.js";
3
3
  import type { P2PLogCollector } from "./P2PLogCollector.js";
4
- export type UseP2PReplicatorResult = {
4
+ import type { P2PServiceViews } from "../../p2p/P2PService.js";
5
+ export type UseP2PReplicatorResult = P2PServiceViews & {
5
6
  /**
6
- * The current replicator selected by the host lifecycle.
7
+ * Compatibility facade retained while existing panes migrate to the
8
+ * focused service views.
7
9
  *
8
- * Read this property at the point of use. Retaining a destructured value
9
- * across settings or database lifecycle events can target a closed,
10
- * replaced instance.
10
+ * This facade is stable for the service lifetime, but it is not the active
11
+ * provider adapter. New consumers must request only the service view which
12
+ * they need.
13
+ *
14
+ * @deprecated Use the focused P2P service views instead.
11
15
  */
12
16
  readonly replicator: LiveSyncTrysteroReplicator;
13
17
  };
14
18
  export type P2PPaneParams = {
15
- /** Current replicator; preserve the result object so its getter remains live. */
19
+ /** @deprecated Migrate panes to the focused P2P service views. */
16
20
  readonly replicator: LiveSyncTrysteroReplicator;
17
21
  p2pLogCollector: P2PLogCollector;
18
22
  storeP2PStatusLine: ReactiveSource<string>;
@@ -1,10 +1,13 @@
1
1
  import type { LiveSyncEventHub } from "../../hub/hub.js";
2
2
  import type { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator.js";
3
3
  import type { Advertisement } from "./types.js";
4
+ import type { P2PServiceLifecycle } from "../../p2p/P2PService.js";
4
5
  /**
5
6
  * Minimal interface that a P2P replicator instance should satisfy for addP2PEventHandlers to work.
6
7
  */
7
8
  export interface P2PReplicatorLike {
9
+ /** True when peer callbacks are already fenced by the active room session. */
10
+ readonly handlesPeerEventsWithinSession?: boolean;
8
11
  onNewPeer(peer: Advertisement): Promise<void> | void;
9
12
  onPeerLeaved(peerId: string): void;
10
13
  requestStatus(): void;
@@ -17,21 +20,14 @@ export interface P2PReplicatorLike {
17
20
  isServing?: boolean;
18
21
  };
19
22
  }
23
+ /** Host lifecycle subset consumed by the legacy event bridge. */
24
+ export type P2PServiceEventTarget = Pick<P2PServiceLifecycle, "requestStatus" | "openAfterDatabaseRebuild" | "closeForLifecycle" | "reconcileAutoStart">;
20
25
  /** Resolves the replicator which currently owns P2P state. */
21
- export type P2PReplicatorProvider = () => P2PReplicatorLike;
26
+ export type P2PEventTarget = P2PReplicatorLike | P2PServiceEventTarget;
27
+ export type P2PReplicatorProvider = () => P2PEventTarget;
22
28
  /**
23
29
  * Add event handlers for P2P replication related events.
24
30
  * @param source A fixed compatibility instance or a provider for a replaceable replicator.
25
31
  */
26
- export declare function addP2PEventHandlers(source: P2PReplicatorLike | P2PReplicatorProvider, events: LiveSyncEventHub): void;
27
- /**
28
- * open P2P replicator if not opened yet.
29
- * @param instance
30
- */
31
- export declare function openP2PReplicator(instance: P2PReplicatorLike): Promise<void>;
32
- /**
33
- * close P2P replicator
34
- * @param instance
35
- */
36
- export declare function closeP2PReplicator(instance: P2PReplicatorLike): Promise<void>;
32
+ export declare function addP2PEventHandlers(source: P2PEventTarget | P2PReplicatorProvider, events: LiveSyncEventHub): void;
37
33
  export type { LiveSyncTrysteroReplicator };
@@ -1,42 +1,42 @@
1
1
  import { EVENT_DATABASE_REBUILT, EVENT_PLATFORM_UNLOADED, EVENT_SETTING_SAVED } from "../../events/coreEvents.js";
2
2
  import { EVENT_ADVERTISEMENT_RECEIVED, EVENT_DEVICE_LEAVED, EVENT_REQUEST_STATUS } from "./TrysteroReplicatorP2PServer.js";
3
+ function isServiceEventTarget(target) {
4
+ return "reconcileAutoStart" in target;
5
+ }
3
6
  function addP2PEventHandlers(source, events) {
4
7
  const current = () => typeof source === "function" ? source() : source;
5
8
  events.onEvent(EVENT_ADVERTISEMENT_RECEIVED, (peer) => {
6
- void current().onNewPeer(peer);
9
+ const target = current();
10
+ if ("onNewPeer" in target && target.handlesPeerEventsWithinSession !== true) void target.onNewPeer(peer);
7
11
  });
8
12
  events.onEvent(EVENT_DEVICE_LEAVED, (peerId) => {
9
- current().onPeerLeaved(peerId);
13
+ const target = current();
14
+ if ("onPeerLeaved" in target && target.handlesPeerEventsWithinSession !== true) target.onPeerLeaved(peerId);
10
15
  });
11
16
  events.onEvent(EVENT_REQUEST_STATUS, () => {
12
17
  current().requestStatus();
13
18
  });
14
19
  events.onEvent(EVENT_DATABASE_REBUILT, async () => {
15
- await current().open();
20
+ const target = current();
21
+ await (isServiceEventTarget(target) ? target.openAfterDatabaseRebuild() : target.open());
16
22
  });
17
23
  events.onEvent(EVENT_PLATFORM_UNLOADED, () => {
18
- void current().close();
24
+ const target = current();
25
+ void (isServiceEventTarget(target) ? target.closeForLifecycle() : target.close());
19
26
  });
20
27
  events.onEvent(EVENT_SETTING_SAVED, async (settings) => {
21
- const instance = current();
28
+ const target = current();
29
+ if (isServiceEventTarget(target)) {
30
+ await target.reconcileAutoStart(settings);
31
+ return;
32
+ }
22
33
  if (settings.P2P_Enabled && settings.P2P_AutoStart) {
23
- await instance.open();
34
+ await target.open();
24
35
  return;
25
36
  }
26
- await instance.close();
37
+ await target.close();
27
38
  });
28
39
  }
29
- async function openP2PReplicator(instance) {
30
- const isOpen = instance.isServing ?? instance.server?.isServing ?? false;
31
- if (!isOpen) {
32
- await instance.open();
33
- }
34
- }
35
- async function closeP2PReplicator(instance) {
36
- await instance.close();
37
- }
38
40
  export {
39
- addP2PEventHandlers,
40
- closeP2PReplicator,
41
- openP2PReplicator
41
+ addP2PEventHandlers
42
42
  };
@@ -0,0 +1,9 @@
1
+ import type { RemoteDBSettings } from "../../common/types.js";
2
+ /**
3
+ * Project only values which fix the effective P2P transport generation.
4
+ *
5
+ * Persisted profile identifiers, enablement, and room automation or admission
6
+ * policy are deliberately excluded. The returned value can contain
7
+ * credentials and must not be logged, persisted, or displayed.
8
+ */
9
+ export declare function getP2PReplicatorConfigurationIdentity(settings: RemoteDBSettings): string;
@@ -0,0 +1,30 @@
1
+ import { P2PConnectionPaths } from "../../common/models/setting.const.js";
2
+ import {
3
+ hasValidP2PTurnServerUrl,
4
+ normaliseP2PConnectionPath,
5
+ normaliseP2PMaxWirePayloadBytes,
6
+ splitP2PRelayUrls,
7
+ splitP2PTurnServerUrls
8
+ } from "../../common/models/setting.p2p.js";
9
+ function getP2PReplicatorConfigurationIdentity(settings) {
10
+ const turnServerValue = settings.P2P_turnServers ?? "";
11
+ const turnServers = splitP2PTurnServerUrls(turnServerValue);
12
+ 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
+ return JSON.stringify([
16
+ "p2p",
17
+ settings.P2P_AppID || "self-hosted-livesync",
18
+ settings.P2P_roomID,
19
+ settings.P2P_passphrase,
20
+ splitP2PRelayUrls(settings.P2P_relays),
21
+ turnServers,
22
+ turnAuthentication,
23
+ normaliseP2PMaxWirePayloadBytes(settings.P2P_maxWirePayloadBytes),
24
+ effectivePath,
25
+ settings.P2P_useDiagRTC ?? false
26
+ ]);
27
+ }
28
+ export {
29
+ getP2PReplicatorConfigurationIdentity
30
+ };
@@ -5,6 +5,7 @@ import type { Confirm } from "../../interfaces/Confirm.js";
5
5
  import type { AsyncActivityRunner } from "../../interfaces/AsyncActivityRunner.js";
6
6
  import type { LiveSyncEventHub } from "../../hub/hub.js";
7
7
  import type { MessageTranslator } from "../../services/base/MessageTranslator.js";
8
+ import type { P2PAutomationCoordinator } from "./P2PAutomationCoordinator.js";
8
9
  export declare const DIRECTION_REQUEST = "request";
9
10
  export type DIRECTION_REQUEST = typeof DIRECTION_REQUEST;
10
11
  export declare const DIRECTION_RESPONSE = "response";
@@ -81,14 +82,31 @@ export interface ReplicatorHostEnv extends ReplicatorHost {
81
82
  events: LiveSyncEventHub;
82
83
  /** Message translation owned by the containing Commonlib service composition. */
83
84
  translate: MessageTranslator;
85
+ /**
86
+ * Immutable settings snapshot captured for this room session.
87
+ *
88
+ * Room join options, RPC framing, and other session-owned resources must
89
+ * use this snapshot so one session cannot mix transport generations.
90
+ */
84
91
  settings: P2PSyncSetting;
92
+ /**
93
+ * Return the latest host settings for policy evaluated on an open room.
94
+ *
95
+ * Older direct consumers may omit this port; they retain snapshot
96
+ * behaviour through the `settings` fallback.
97
+ */
98
+ currentSettings?(): P2PSyncSetting;
85
99
  db: PouchDB.Database<EntryDoc>;
86
100
  simpleStore: SimpleStore<unknown>;
87
101
  runFiniteReplicationActivity?: AsyncActivityRunner["run"];
88
102
  /** Lightweight, repeatable host policy checked before ordinary P2P replication starts. */
89
103
  canStartOrdinaryReplication?(showMessage?: boolean): Promise<boolean>;
104
+ /** Stable automatic-transfer state supplied by the owning P2P service. */
105
+ automationCoordinator?: P2PAutomationCoordinator;
90
106
  processReplicatedDocs(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): void | Promise<void>;
91
107
  }
108
+ /** Resolve policy settings without weakening the immutable room snapshot. */
109
+ export declare function resolveCurrentP2PSettings(env: ReplicatorHostEnv): P2PSyncSetting;
92
110
  export type Advertisement = {
93
111
  peerId: string;
94
112
  name: string;