@hashtree/worker 0.1.26 → 0.2.1

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 (258) hide show
  1. package/README.md +16 -16
  2. package/package.json +23 -19
  3. package/src/app-runtime.ts +393 -0
  4. package/src/capabilities/blossomBandwidthTracker.ts +74 -0
  5. package/src/capabilities/blossomTransport.ts +179 -0
  6. package/src/capabilities/connectivity.ts +54 -0
  7. package/src/capabilities/idbStorage.ts +94 -0
  8. package/src/capabilities/meshRouterStore.ts +426 -0
  9. package/src/capabilities/rootResolver.ts +497 -0
  10. package/src/client-id.ts +137 -0
  11. package/src/client.ts +501 -0
  12. package/{dist/entry.js → src/entry.ts} +1 -1
  13. package/src/htree-path.ts +53 -0
  14. package/src/htree-url.ts +156 -0
  15. package/src/index.ts +76 -0
  16. package/src/mediaStreaming.ts +64 -0
  17. package/src/p2p/boundedQueue.ts +168 -0
  18. package/src/p2p/errorMessage.ts +6 -0
  19. package/src/p2p/index.ts +48 -0
  20. package/src/p2p/lruCache.ts +78 -0
  21. package/src/p2p/meshQueryRouter.ts +361 -0
  22. package/src/p2p/protocol.ts +11 -0
  23. package/src/p2p/queryForwardingMachine.ts +197 -0
  24. package/src/p2p/signaling.ts +284 -0
  25. package/src/p2p/uploadRateLimiter.ts +85 -0
  26. package/src/p2p/webrtcController.ts +1168 -0
  27. package/src/p2p/webrtcProxy.ts +519 -0
  28. package/src/privacyGuards.ts +31 -0
  29. package/src/protocol.ts +124 -0
  30. package/src/relay/identity.ts +86 -0
  31. package/src/relay/mediaHandler.ts +1633 -0
  32. package/src/relay/ndk.ts +590 -0
  33. package/{dist/iris/nostr-wasm.js → src/relay/nostr-wasm.ts} +4 -1
  34. package/src/relay/nostr.ts +249 -0
  35. package/src/relay/protocol.ts +361 -0
  36. package/src/relay/publicAssetUrl.ts +25 -0
  37. package/src/relay/rootPathResolver.ts +50 -0
  38. package/src/relay/shims.d.ts +17 -0
  39. package/src/relay/signing.ts +332 -0
  40. package/src/relay/treeRootCache.ts +354 -0
  41. package/src/relay/treeRootSubscription.ts +577 -0
  42. package/src/relay/utils/constants.ts +139 -0
  43. package/src/relay/utils/errorMessage.ts +7 -0
  44. package/src/relay/utils/lruCache.ts +79 -0
  45. package/src/relay/webrtc.ts +5 -0
  46. package/src/relay/webrtcSignaling.ts +108 -0
  47. package/src/relay/worker.ts +1787 -0
  48. package/src/relay-client.ts +265 -0
  49. package/src/relay-entry.ts +1 -0
  50. package/src/runtime-network.ts +134 -0
  51. package/src/runtime.ts +153 -0
  52. package/src/transferableBytes.ts +5 -0
  53. package/src/tree-root.ts +851 -0
  54. package/src/types.ts +8 -0
  55. package/src/worker.ts +975 -0
  56. package/LICENSE +0 -21
  57. package/dist/app-runtime.d.ts +0 -60
  58. package/dist/app-runtime.d.ts.map +0 -1
  59. package/dist/app-runtime.js +0 -271
  60. package/dist/app-runtime.js.map +0 -1
  61. package/dist/capabilities/blossomBandwidthTracker.d.ts +0 -26
  62. package/dist/capabilities/blossomBandwidthTracker.d.ts.map +0 -1
  63. package/dist/capabilities/blossomBandwidthTracker.js +0 -53
  64. package/dist/capabilities/blossomBandwidthTracker.js.map +0 -1
  65. package/dist/capabilities/blossomTransport.d.ts +0 -22
  66. package/dist/capabilities/blossomTransport.d.ts.map +0 -1
  67. package/dist/capabilities/blossomTransport.js +0 -144
  68. package/dist/capabilities/blossomTransport.js.map +0 -1
  69. package/dist/capabilities/connectivity.d.ts +0 -3
  70. package/dist/capabilities/connectivity.d.ts.map +0 -1
  71. package/dist/capabilities/connectivity.js +0 -49
  72. package/dist/capabilities/connectivity.js.map +0 -1
  73. package/dist/capabilities/idbStorage.d.ts +0 -25
  74. package/dist/capabilities/idbStorage.d.ts.map +0 -1
  75. package/dist/capabilities/idbStorage.js +0 -73
  76. package/dist/capabilities/idbStorage.js.map +0 -1
  77. package/dist/capabilities/meshRouterStore.d.ts +0 -71
  78. package/dist/capabilities/meshRouterStore.d.ts.map +0 -1
  79. package/dist/capabilities/meshRouterStore.js +0 -316
  80. package/dist/capabilities/meshRouterStore.js.map +0 -1
  81. package/dist/capabilities/rootResolver.d.ts +0 -10
  82. package/dist/capabilities/rootResolver.d.ts.map +0 -1
  83. package/dist/capabilities/rootResolver.js +0 -393
  84. package/dist/capabilities/rootResolver.js.map +0 -1
  85. package/dist/client-id.d.ts +0 -18
  86. package/dist/client-id.d.ts.map +0 -1
  87. package/dist/client-id.js +0 -98
  88. package/dist/client-id.js.map +0 -1
  89. package/dist/client.d.ts +0 -61
  90. package/dist/client.d.ts.map +0 -1
  91. package/dist/client.js +0 -417
  92. package/dist/client.js.map +0 -1
  93. package/dist/entry.d.ts +0 -2
  94. package/dist/entry.d.ts.map +0 -1
  95. package/dist/entry.js.map +0 -1
  96. package/dist/htree-path.d.ts +0 -13
  97. package/dist/htree-path.d.ts.map +0 -1
  98. package/dist/htree-path.js +0 -38
  99. package/dist/htree-path.js.map +0 -1
  100. package/dist/htree-url.d.ts +0 -22
  101. package/dist/htree-url.d.ts.map +0 -1
  102. package/dist/htree-url.js +0 -118
  103. package/dist/htree-url.js.map +0 -1
  104. package/dist/index.d.ts +0 -17
  105. package/dist/index.d.ts.map +0 -1
  106. package/dist/index.js +0 -8
  107. package/dist/index.js.map +0 -1
  108. package/dist/iris/identity.d.ts +0 -36
  109. package/dist/iris/identity.d.ts.map +0 -1
  110. package/dist/iris/identity.js +0 -78
  111. package/dist/iris/identity.js.map +0 -1
  112. package/dist/iris/mediaHandler.d.ts +0 -64
  113. package/dist/iris/mediaHandler.d.ts.map +0 -1
  114. package/dist/iris/mediaHandler.js +0 -1285
  115. package/dist/iris/mediaHandler.js.map +0 -1
  116. package/dist/iris/ndk.d.ts +0 -96
  117. package/dist/iris/ndk.d.ts.map +0 -1
  118. package/dist/iris/ndk.js +0 -502
  119. package/dist/iris/ndk.js.map +0 -1
  120. package/dist/iris/nostr-wasm.d.ts +0 -14
  121. package/dist/iris/nostr-wasm.d.ts.map +0 -1
  122. package/dist/iris/nostr-wasm.js.map +0 -1
  123. package/dist/iris/nostr.d.ts +0 -60
  124. package/dist/iris/nostr.d.ts.map +0 -1
  125. package/dist/iris/nostr.js +0 -207
  126. package/dist/iris/nostr.js.map +0 -1
  127. package/dist/iris/protocol.d.ts +0 -583
  128. package/dist/iris/protocol.d.ts.map +0 -1
  129. package/dist/iris/protocol.js +0 -16
  130. package/dist/iris/protocol.js.map +0 -1
  131. package/dist/iris/publicAssetUrl.d.ts +0 -6
  132. package/dist/iris/publicAssetUrl.d.ts.map +0 -1
  133. package/dist/iris/publicAssetUrl.js +0 -14
  134. package/dist/iris/publicAssetUrl.js.map +0 -1
  135. package/dist/iris/rootPathResolver.d.ts +0 -9
  136. package/dist/iris/rootPathResolver.d.ts.map +0 -1
  137. package/dist/iris/rootPathResolver.js +0 -32
  138. package/dist/iris/rootPathResolver.js.map +0 -1
  139. package/dist/iris/signing.d.ts +0 -50
  140. package/dist/iris/signing.d.ts.map +0 -1
  141. package/dist/iris/signing.js +0 -299
  142. package/dist/iris/signing.js.map +0 -1
  143. package/dist/iris/treeRootCache.d.ts +0 -86
  144. package/dist/iris/treeRootCache.d.ts.map +0 -1
  145. package/dist/iris/treeRootCache.js +0 -269
  146. package/dist/iris/treeRootCache.js.map +0 -1
  147. package/dist/iris/treeRootSubscription.d.ts +0 -55
  148. package/dist/iris/treeRootSubscription.d.ts.map +0 -1
  149. package/dist/iris/treeRootSubscription.js +0 -479
  150. package/dist/iris/treeRootSubscription.js.map +0 -1
  151. package/dist/iris/utils/constants.d.ts +0 -76
  152. package/dist/iris/utils/constants.d.ts.map +0 -1
  153. package/dist/iris/utils/constants.js +0 -113
  154. package/dist/iris/utils/constants.js.map +0 -1
  155. package/dist/iris/utils/errorMessage.d.ts +0 -5
  156. package/dist/iris/utils/errorMessage.d.ts.map +0 -1
  157. package/dist/iris/utils/errorMessage.js +0 -8
  158. package/dist/iris/utils/errorMessage.js.map +0 -1
  159. package/dist/iris/utils/lruCache.d.ts +0 -26
  160. package/dist/iris/utils/lruCache.d.ts.map +0 -1
  161. package/dist/iris/utils/lruCache.js +0 -66
  162. package/dist/iris/utils/lruCache.js.map +0 -1
  163. package/dist/iris/webrtc.d.ts +0 -2
  164. package/dist/iris/webrtc.d.ts.map +0 -1
  165. package/dist/iris/webrtc.js +0 -3
  166. package/dist/iris/webrtc.js.map +0 -1
  167. package/dist/iris/webrtcSignaling.d.ts +0 -37
  168. package/dist/iris/webrtcSignaling.d.ts.map +0 -1
  169. package/dist/iris/webrtcSignaling.js +0 -86
  170. package/dist/iris/webrtcSignaling.js.map +0 -1
  171. package/dist/iris/worker.d.ts +0 -12
  172. package/dist/iris/worker.d.ts.map +0 -1
  173. package/dist/iris/worker.js +0 -1529
  174. package/dist/iris/worker.js.map +0 -1
  175. package/dist/iris-client.d.ts +0 -31
  176. package/dist/iris-client.d.ts.map +0 -1
  177. package/dist/iris-client.js +0 -197
  178. package/dist/iris-client.js.map +0 -1
  179. package/dist/iris-entry.d.ts +0 -2
  180. package/dist/iris-entry.d.ts.map +0 -1
  181. package/dist/iris-entry.js +0 -2
  182. package/dist/iris-entry.js.map +0 -1
  183. package/dist/mediaStreaming.d.ts +0 -7
  184. package/dist/mediaStreaming.d.ts.map +0 -1
  185. package/dist/mediaStreaming.js +0 -48
  186. package/dist/mediaStreaming.js.map +0 -1
  187. package/dist/p2p/boundedQueue.d.ts +0 -79
  188. package/dist/p2p/boundedQueue.d.ts.map +0 -1
  189. package/dist/p2p/boundedQueue.js +0 -134
  190. package/dist/p2p/boundedQueue.js.map +0 -1
  191. package/dist/p2p/errorMessage.d.ts +0 -5
  192. package/dist/p2p/errorMessage.d.ts.map +0 -1
  193. package/dist/p2p/errorMessage.js +0 -7
  194. package/dist/p2p/errorMessage.js.map +0 -1
  195. package/dist/p2p/index.d.ts +0 -8
  196. package/dist/p2p/index.d.ts.map +0 -1
  197. package/dist/p2p/index.js +0 -6
  198. package/dist/p2p/index.js.map +0 -1
  199. package/dist/p2p/lruCache.d.ts +0 -26
  200. package/dist/p2p/lruCache.d.ts.map +0 -1
  201. package/dist/p2p/lruCache.js +0 -65
  202. package/dist/p2p/lruCache.js.map +0 -1
  203. package/dist/p2p/meshQueryRouter.d.ts +0 -44
  204. package/dist/p2p/meshQueryRouter.d.ts.map +0 -1
  205. package/dist/p2p/meshQueryRouter.js +0 -228
  206. package/dist/p2p/meshQueryRouter.js.map +0 -1
  207. package/dist/p2p/protocol.d.ts +0 -10
  208. package/dist/p2p/protocol.d.ts.map +0 -1
  209. package/dist/p2p/protocol.js +0 -2
  210. package/dist/p2p/protocol.js.map +0 -1
  211. package/dist/p2p/queryForwardingMachine.d.ts +0 -46
  212. package/dist/p2p/queryForwardingMachine.d.ts.map +0 -1
  213. package/dist/p2p/queryForwardingMachine.js +0 -144
  214. package/dist/p2p/queryForwardingMachine.js.map +0 -1
  215. package/dist/p2p/signaling.d.ts +0 -63
  216. package/dist/p2p/signaling.d.ts.map +0 -1
  217. package/dist/p2p/signaling.js +0 -185
  218. package/dist/p2p/signaling.js.map +0 -1
  219. package/dist/p2p/uploadRateLimiter.d.ts +0 -21
  220. package/dist/p2p/uploadRateLimiter.d.ts.map +0 -1
  221. package/dist/p2p/uploadRateLimiter.js +0 -62
  222. package/dist/p2p/uploadRateLimiter.js.map +0 -1
  223. package/dist/p2p/webrtcController.d.ts +0 -168
  224. package/dist/p2p/webrtcController.d.ts.map +0 -1
  225. package/dist/p2p/webrtcController.js +0 -902
  226. package/dist/p2p/webrtcController.js.map +0 -1
  227. package/dist/p2p/webrtcProxy.d.ts +0 -62
  228. package/dist/p2p/webrtcProxy.d.ts.map +0 -1
  229. package/dist/p2p/webrtcProxy.js +0 -447
  230. package/dist/p2p/webrtcProxy.js.map +0 -1
  231. package/dist/privacyGuards.d.ts +0 -14
  232. package/dist/privacyGuards.d.ts.map +0 -1
  233. package/dist/privacyGuards.js +0 -27
  234. package/dist/privacyGuards.js.map +0 -1
  235. package/dist/protocol.d.ts +0 -225
  236. package/dist/protocol.d.ts.map +0 -1
  237. package/dist/protocol.js +0 -2
  238. package/dist/protocol.js.map +0 -1
  239. package/dist/runtime-network.d.ts +0 -23
  240. package/dist/runtime-network.d.ts.map +0 -1
  241. package/dist/runtime-network.js +0 -105
  242. package/dist/runtime-network.js.map +0 -1
  243. package/dist/runtime.d.ts +0 -23
  244. package/dist/runtime.d.ts.map +0 -1
  245. package/dist/runtime.js +0 -122
  246. package/dist/runtime.js.map +0 -1
  247. package/dist/tree-root.d.ts +0 -201
  248. package/dist/tree-root.d.ts.map +0 -1
  249. package/dist/tree-root.js +0 -632
  250. package/dist/tree-root.js.map +0 -1
  251. package/dist/types.d.ts +0 -2
  252. package/dist/types.d.ts.map +0 -1
  253. package/dist/types.js +0 -2
  254. package/dist/types.js.map +0 -1
  255. package/dist/worker.d.ts +0 -9
  256. package/dist/worker.d.ts.map +0 -1
  257. package/dist/worker.js +0 -797
  258. package/dist/worker.js.map +0 -1
@@ -1,902 +0,0 @@
1
- /**
2
- * Worker WebRTC Controller
3
- *
4
- * Controls WebRTC connections from the worker thread.
5
- * Main thread proxy executes RTCPeerConnection operations.
6
- *
7
- * Worker owns:
8
- * - Peer state tracking
9
- * - Connection lifecycle decisions
10
- * - Data protocol (request/response)
11
- * - Signaling message handling
12
- *
13
- * Main thread proxy owns:
14
- * - RTCPeerConnection instances (not available in workers)
15
- * - Data channel I/O
16
- */
17
- import { fromHex, sha256, toHex } from '@hashtree/core';
18
- import { MAX_HTL, MSG_TYPE_REQUEST, MSG_TYPE_RESPONSE, FRAGMENT_SIZE, PeerId, encodeRequest, encodeResponse, parseMessage, createRequest, createResponse, createFragmentResponse, hashToKey, verifyHash, generatePeerHTLConfig, PeerSelector, buildHedgedWavePlan, normalizeDispatchConfig, syncSelectorPeers, } from '@hashtree/nostr';
19
- import { LRUCache } from './lruCache.js';
20
- import { MeshQueryRouter, encodeForwardRequest } from './meshQueryRouter.js';
21
- const PEER_METADATA_POINTER_SLOT_KEY = 'hashtree-webrtc/peer-metadata/latest/v1';
22
- const DEFAULT_REQUEST_DISPATCH = {
23
- initialFanout: 2,
24
- hedgeFanout: 1,
25
- maxFanout: 8,
26
- hedgeIntervalMs: 120,
27
- };
28
- // ============================================================================
29
- // Controller
30
- // ============================================================================
31
- export class WebRTCController {
32
- myPeerId;
33
- peers = new Map();
34
- pendingRemoteCandidates = new Map();
35
- localStore;
36
- sendCommand;
37
- sendSignaling;
38
- classifyPeer;
39
- requestTimeout;
40
- debug;
41
- recentRequests = new LRUCache(1000);
42
- meshRouter;
43
- peerSelector;
44
- routing;
45
- // Pool configuration - reasonable defaults, settings sync will override
46
- poolConfig = {
47
- follows: { maxConnections: 20, satisfiedConnections: 10 },
48
- other: { maxConnections: 16, satisfiedConnections: 8 },
49
- };
50
- // Hello interval - 5s for faster peer discovery
51
- helloInterval;
52
- HELLO_INTERVAL = 5000;
53
- constructor(config) {
54
- this.myPeerId = new PeerId(config.pubkey);
55
- this.localStore = config.localStore;
56
- this.sendCommand = config.sendCommand;
57
- this.sendSignaling = config.sendSignaling;
58
- this.requestTimeout = config.requestTimeout ?? 1000;
59
- this.debug = config.debug ?? false;
60
- this.routing = {
61
- selectionStrategy: config.requestSelectionStrategy ?? 'titForTat',
62
- fairnessEnabled: config.requestFairnessEnabled ?? true,
63
- dispatch: config.requestDispatch ?? DEFAULT_REQUEST_DISPATCH,
64
- };
65
- this.peerSelector = PeerSelector.withStrategy(this.routing.selectionStrategy);
66
- this.peerSelector.setFairness(this.routing.fairnessEnabled);
67
- this.meshRouter = new MeshQueryRouter({
68
- localStore: this.localStore,
69
- requestTimeoutMs: this.requestTimeout,
70
- upstreamFetch: config.upstreamFetch,
71
- maxForwardsPerPeerWindow: config.forwardRateLimit?.maxForwardsPerPeerWindow,
72
- forwardRateLimitWindowMs: config.forwardRateLimit?.windowMs,
73
- });
74
- // Default classifier: check if pubkey is in follows
75
- const getFollows = config.getFollows ?? (() => new Set());
76
- this.classifyPeer = (pubkey) => {
77
- const follows = getFollows();
78
- const isFollow = follows.has(pubkey);
79
- return isFollow ? 'follows' : 'other';
80
- };
81
- }
82
- // ============================================================================
83
- // Lifecycle
84
- // ============================================================================
85
- start() {
86
- this.log('Starting WebRTC controller');
87
- // Send hello periodically
88
- this.helloInterval = setInterval(() => {
89
- this.sendHello();
90
- }, this.HELLO_INTERVAL);
91
- // Send initial hello
92
- this.sendHello();
93
- }
94
- stop() {
95
- this.log('Stopping WebRTC controller');
96
- if (this.helloInterval) {
97
- clearInterval(this.helloInterval);
98
- this.helloInterval = undefined;
99
- }
100
- // Close all peers
101
- for (const peerId of this.peers.keys()) {
102
- this.closePeer(peerId);
103
- }
104
- this.meshRouter.stop();
105
- }
106
- // ============================================================================
107
- // Signaling
108
- // ============================================================================
109
- sendHello() {
110
- const msg = {
111
- type: 'hello',
112
- peerId: this.myPeerId.toString(),
113
- };
114
- this.sendSignaling(msg).catch(err => {
115
- console.error('[WebRTC] sendSignaling error:', err);
116
- });
117
- }
118
- /**
119
- * Public method to trigger a hello broadcast.
120
- * Used for testing to force peer discovery after follows are set up.
121
- */
122
- broadcastHello() {
123
- this.sendHello();
124
- }
125
- /**
126
- * Handle incoming signaling message (from Nostr kind 25050)
127
- *
128
- * `peerId` is the remote endpoint identity.
129
- */
130
- async handleSignalingMessage(msg, senderPubkey) {
131
- this.log(`Signaling from ${senderPubkey.slice(0, 8)}:`, msg.type);
132
- switch (msg.type) {
133
- case 'hello':
134
- await this.handleHello(senderPubkey);
135
- break;
136
- case 'offer':
137
- if (msg.peerId === this.myPeerId.toString()) {
138
- return; // Skip messages from ourselves
139
- }
140
- if (this.isMessageForUs(msg)) {
141
- // Construct RTCSessionDescriptionInit from flat sdp field
142
- await this.handleOffer(msg.peerId, senderPubkey, { type: 'offer', sdp: msg.sdp });
143
- }
144
- break;
145
- case 'answer':
146
- if (msg.peerId === this.myPeerId.toString()) {
147
- return;
148
- }
149
- if (this.isMessageForUs(msg)) {
150
- // Construct RTCSessionDescriptionInit from flat sdp field
151
- await this.handleAnswer(msg.peerId, { type: 'answer', sdp: msg.sdp });
152
- }
153
- break;
154
- case 'candidate':
155
- if (msg.peerId === this.myPeerId.toString()) {
156
- return;
157
- }
158
- if (this.isMessageForUs(msg)) {
159
- // Construct RTCIceCandidateInit from flat fields
160
- await this.handleIceCandidate(msg.peerId, {
161
- candidate: msg.candidate,
162
- sdpMLineIndex: msg.sdpMLineIndex,
163
- sdpMid: msg.sdpMid,
164
- });
165
- }
166
- break;
167
- case 'candidates':
168
- if (msg.peerId === this.myPeerId.toString()) {
169
- return;
170
- }
171
- if (this.isMessageForUs(msg)) {
172
- for (const c of msg.candidates) {
173
- await this.handleIceCandidate(msg.peerId, {
174
- candidate: c.candidate,
175
- sdpMLineIndex: c.sdpMLineIndex,
176
- sdpMid: c.sdpMid,
177
- });
178
- }
179
- }
180
- break;
181
- }
182
- }
183
- isMessageForUs(msg) {
184
- if ('targetPeerId' in msg && msg.targetPeerId) {
185
- return msg.targetPeerId === this.myPeerId.toString();
186
- }
187
- return true;
188
- }
189
- async handleHello(senderPubkey) {
190
- const peerId = new PeerId(senderPubkey).toString();
191
- // Already connected?
192
- if (this.peers.has(peerId)) {
193
- return;
194
- }
195
- // Check pool limits
196
- const pool = this.classifyPeer(senderPubkey);
197
- if (!this.shouldConnect(pool)) {
198
- this.log(`Pool ${pool} at capacity, ignoring hello`);
199
- return;
200
- }
201
- // In 'other' pool, only allow 1 connection per pubkey
202
- if (pool === 'other' && this.hasOtherPoolPubkey(senderPubkey)) {
203
- this.log(`Already have connection from ${senderPubkey.slice(0, 8)} in other pool`);
204
- return;
205
- }
206
- // Tie-breaking: lower endpoint ID initiates
207
- const shouldInitiate = this.myPeerId.toString() < peerId;
208
- if (shouldInitiate) {
209
- this.log(`Initiating connection to ${peerId.slice(0, 20)}`);
210
- await this.createOutboundPeer(peerId, senderPubkey, pool);
211
- }
212
- else {
213
- this.log(`Waiting for offer from ${peerId.slice(0, 20)}`);
214
- }
215
- }
216
- async handleOffer(peerId, pubkey, offer) {
217
- this.log(`handleOffer from ${pubkey.slice(0, 8)}, peerId: ${peerId.slice(0, 20)}`);
218
- let peer = this.peers.get(peerId);
219
- if (!peer) {
220
- const pool = this.classifyPeer(pubkey);
221
- if (!this.shouldConnect(pool)) {
222
- this.log(`Pool ${pool} at capacity, rejecting offer`);
223
- return;
224
- }
225
- if (pool === 'other' && this.hasOtherPoolPubkey(pubkey)) {
226
- this.log(`Already have connection from ${pubkey.slice(0, 8)} in other pool, rejecting offer`);
227
- return;
228
- }
229
- this.log(`Creating inbound peer for ${pubkey.slice(0, 8)}`);
230
- peer = this.createPeer(peerId, pubkey, pool, 'inbound');
231
- }
232
- else if (peer.direction === 'outbound' && peer.state === 'connecting') {
233
- const isPolite = this.myPeerId.toString() < peerId;
234
- if (!isPolite) {
235
- this.log(`Ignoring offer collision from ${pubkey.slice(0, 8)} as impolite peer`);
236
- return;
237
- }
238
- // Perfect negotiation: the polite peer abandons its local offer and
239
- // switches into answerer mode for the remote offer.
240
- peer.direction = 'inbound';
241
- peer.answerCreated = false;
242
- }
243
- this.log(`Setting remote description for ${peerId.slice(0, 20)}`);
244
- this.sendCommand({ type: 'rtc:setRemoteDescription', peerId, sdp: offer });
245
- }
246
- async handleAnswer(peerId, answer) {
247
- const peer = this.peers.get(peerId);
248
- if (!peer) {
249
- this.log(`Answer for unknown peer: ${peerId}`);
250
- return;
251
- }
252
- this.sendCommand({ type: 'rtc:setRemoteDescription', peerId, sdp: answer });
253
- }
254
- async handleIceCandidate(peerId, candidate) {
255
- const peer = this.peers.get(peerId);
256
- if (!peer) {
257
- const queued = this.pendingRemoteCandidates.get(peerId) ?? [];
258
- queued.push(candidate);
259
- this.pendingRemoteCandidates.set(peerId, queued);
260
- return;
261
- }
262
- this.sendCommand({ type: 'rtc:addIceCandidate', peerId, candidate });
263
- }
264
- // ============================================================================
265
- // Peer Management
266
- // ============================================================================
267
- shouldConnect(pool) {
268
- const config = this.poolConfig[pool];
269
- const count = this.getPoolCount(pool);
270
- return count < config.maxConnections;
271
- }
272
- getPoolCount(pool) {
273
- let count = 0;
274
- for (const peer of this.peers.values()) {
275
- if (peer.pool === pool && peer.state !== 'disconnected') {
276
- count++;
277
- }
278
- }
279
- return count;
280
- }
281
- /**
282
- * Check if we already have a connection from this pubkey in the 'other' pool.
283
- * In the 'other' pool, we only allow 1 connection per pubkey to prevent spam.
284
- */
285
- hasOtherPoolPubkey(pubkey) {
286
- for (const peer of this.peers.values()) {
287
- if (peer.pool === 'other' && peer.pubkey === pubkey && peer.state !== 'disconnected') {
288
- return true;
289
- }
290
- }
291
- return false;
292
- }
293
- createPeer(peerId, pubkey, pool, direction) {
294
- const peer = {
295
- peerId,
296
- pubkey,
297
- pool,
298
- direction,
299
- state: 'connecting',
300
- dataChannelReady: false,
301
- answerCreated: false,
302
- htlConfig: generatePeerHTLConfig(),
303
- pendingRequests: new Map(),
304
- stats: {
305
- requestsSent: 0,
306
- requestsReceived: 0,
307
- responsesSent: 0,
308
- responsesReceived: 0,
309
- bytesSent: 0,
310
- bytesReceived: 0,
311
- forwardedRequests: 0,
312
- forwardedResolved: 0,
313
- forwardedSuppressed: 0,
314
- },
315
- createdAt: Date.now(),
316
- bufferPaused: false,
317
- deferredRequests: [],
318
- };
319
- this.peers.set(peerId, peer);
320
- this.peerSelector.addPeer(peerId);
321
- this.meshRouter.registerPeer({
322
- peerId,
323
- canSend: () => peer.dataChannelReady,
324
- getHtlConfig: () => peer.htlConfig,
325
- sendRequest: (hash, htl) => this.sendRequestToPeer(peer, hash, htl),
326
- sendResponse: async (hash, data) => this.sendResponse(peer, hash, data),
327
- onForwardedRequest: () => {
328
- peer.stats.forwardedRequests++;
329
- },
330
- onForwardedResolved: () => {
331
- peer.stats.forwardedResolved++;
332
- },
333
- onForwardedSuppressed: () => {
334
- peer.stats.forwardedSuppressed++;
335
- },
336
- });
337
- this.sendCommand({ type: 'rtc:createPeer', peerId, pubkey });
338
- return peer;
339
- }
340
- async createOutboundPeer(peerId, pubkey, pool) {
341
- this.createPeer(peerId, pubkey, pool, 'outbound');
342
- // Proxy will create peer and we'll get rtc:peerCreated, then request offer
343
- }
344
- closePeer(peerId) {
345
- const peer = this.peers.get(peerId);
346
- if (!peer)
347
- return;
348
- // Clear pending requests
349
- for (const pending of peer.pendingRequests.values()) {
350
- clearTimeout(pending.timeout);
351
- pending.resolve(null);
352
- }
353
- peer.state = 'disconnected';
354
- this.sendCommand({ type: 'rtc:closePeer', peerId });
355
- this.peers.delete(peerId);
356
- this.pendingRemoteCandidates.delete(peerId);
357
- this.peerSelector.removePeer(peerId);
358
- this.meshRouter.removePeer(peerId);
359
- this.log(`Closed peer: ${peerId.slice(0, 20)}`);
360
- }
361
- // ============================================================================
362
- // Proxy Events
363
- // ============================================================================
364
- /**
365
- * Handle event from main thread proxy
366
- */
367
- handleProxyEvent(event) {
368
- switch (event.type) {
369
- case 'rtc:peerCreated':
370
- this.onPeerCreated(event.peerId);
371
- break;
372
- case 'rtc:peerStateChange':
373
- this.onPeerStateChange(event.peerId, event.state);
374
- break;
375
- case 'rtc:peerClosed':
376
- this.onPeerClosed(event.peerId);
377
- break;
378
- case 'rtc:offerCreated':
379
- this.onOfferCreated(event.peerId, event.sdp);
380
- break;
381
- case 'rtc:answerCreated':
382
- this.onAnswerCreated(event.peerId, event.sdp);
383
- break;
384
- case 'rtc:descriptionSet':
385
- this.onDescriptionSet(event.peerId, event.error);
386
- break;
387
- case 'rtc:iceCandidate':
388
- this.onIceCandidate(event.peerId, event.candidate);
389
- break;
390
- case 'rtc:dataChannelOpen':
391
- this.onDataChannelOpen(event.peerId);
392
- break;
393
- case 'rtc:dataChannelMessage':
394
- this.onDataChannelMessage(event.peerId, event.data);
395
- break;
396
- case 'rtc:dataChannelClose':
397
- this.onDataChannelClose(event.peerId);
398
- break;
399
- case 'rtc:dataChannelError':
400
- this.onDataChannelError(event.peerId, event.error);
401
- break;
402
- case 'rtc:bufferHigh':
403
- this.onBufferHigh(event.peerId);
404
- break;
405
- case 'rtc:bufferLow':
406
- this.onBufferLow(event.peerId);
407
- break;
408
- }
409
- }
410
- onPeerCreated(peerId) {
411
- const peer = this.peers.get(peerId);
412
- if (!peer)
413
- return;
414
- const queuedCandidates = this.pendingRemoteCandidates.get(peerId);
415
- if (queuedCandidates?.length) {
416
- for (const candidate of queuedCandidates) {
417
- this.sendCommand({ type: 'rtc:addIceCandidate', peerId, candidate });
418
- }
419
- this.pendingRemoteCandidates.delete(peerId);
420
- }
421
- // If outbound, create offer
422
- if (peer.direction === 'outbound') {
423
- this.sendCommand({ type: 'rtc:createOffer', peerId });
424
- }
425
- }
426
- onPeerStateChange(peerId, state) {
427
- const peer = this.peers.get(peerId);
428
- if (!peer)
429
- return;
430
- this.log(`Peer ${peerId.slice(0, 20)} state: ${state}`);
431
- if (state === 'connected') {
432
- peer.state = 'connected';
433
- peer.connectedAt = Date.now();
434
- }
435
- else if (state === 'failed' || state === 'closed') {
436
- this.closePeer(peerId);
437
- }
438
- }
439
- onPeerClosed(peerId) {
440
- this.peers.delete(peerId);
441
- this.peerSelector.removePeer(peerId);
442
- }
443
- onOfferCreated(peerId, sdp) {
444
- const peer = this.peers.get(peerId);
445
- if (!peer)
446
- return;
447
- // Set local description
448
- this.sendCommand({ type: 'rtc:setLocalDescription', peerId, sdp });
449
- // Send offer via signaling using endpoint identities.
450
- const msg = {
451
- type: 'offer',
452
- sdp: sdp.sdp,
453
- targetPeerId: peerId,
454
- peerId: this.myPeerId.toString(),
455
- };
456
- this.sendSignaling(msg, peer.pubkey);
457
- }
458
- onAnswerCreated(peerId, sdp) {
459
- this.log(`onAnswerCreated for ${peerId.slice(0, 20)}`);
460
- const peer = this.peers.get(peerId);
461
- if (!peer) {
462
- this.log(`onAnswerCreated: peer not found for ${peerId.slice(0, 20)}`);
463
- return;
464
- }
465
- this.sendCommand({ type: 'rtc:setLocalDescription', peerId, sdp });
466
- this.log(`Sending answer to ${peer.pubkey.slice(0, 8)}`);
467
- const msg = {
468
- type: 'answer',
469
- sdp: sdp.sdp,
470
- targetPeerId: peerId,
471
- peerId: this.myPeerId.toString(),
472
- };
473
- this.sendSignaling(msg, peer.pubkey);
474
- }
475
- onDescriptionSet(peerId, error) {
476
- if (error) {
477
- this.log(`Description set error for ${peerId.slice(0, 20)}: ${error}`);
478
- return;
479
- }
480
- const peer = this.peers.get(peerId);
481
- if (!peer) {
482
- this.log(`onDescriptionSet: peer not found for ${peerId.slice(0, 20)}`);
483
- return;
484
- }
485
- this.log(`onDescriptionSet for ${peerId.slice(0, 20)}: direction=${peer.direction}, state=${peer.state}, answerCreated=${peer.answerCreated}`);
486
- if (peer.direction === 'inbound' && peer.state === 'connecting' && !peer.answerCreated) {
487
- peer.answerCreated = true;
488
- this.log(`Creating answer for ${peerId.slice(0, 20)}`);
489
- this.sendCommand({ type: 'rtc:createAnswer', peerId });
490
- }
491
- }
492
- onIceCandidate(peerId, candidate) {
493
- if (!candidate || !candidate.candidate)
494
- return;
495
- const peer = this.peers.get(peerId);
496
- if (!peer)
497
- return;
498
- // Send candidate via signaling using endpoint identities.
499
- const msg = {
500
- type: 'candidate',
501
- candidate: candidate.candidate,
502
- sdpMLineIndex: candidate.sdpMLineIndex ?? undefined,
503
- sdpMid: candidate.sdpMid ?? undefined,
504
- targetPeerId: peerId,
505
- peerId: this.myPeerId.toString(),
506
- };
507
- this.sendSignaling(msg, peer.pubkey);
508
- }
509
- onDataChannelOpen(peerId) {
510
- const peer = this.peers.get(peerId);
511
- if (!peer)
512
- return;
513
- peer.dataChannelReady = true;
514
- this.log(`Data channel open: ${peerId.slice(0, 20)}`);
515
- }
516
- onDataChannelClose(peerId) {
517
- const peer = this.peers.get(peerId);
518
- if (!peer)
519
- return;
520
- peer.dataChannelReady = false;
521
- this.closePeer(peerId);
522
- }
523
- onDataChannelError(peerId, error) {
524
- this.log(`Data channel error for ${peerId}: ${error}`);
525
- }
526
- onBufferHigh(peerId) {
527
- const peer = this.peers.get(peerId);
528
- if (!peer)
529
- return;
530
- peer.bufferPaused = true;
531
- this.log(`Buffer high for ${peerId.slice(0, 20)}, pausing responses`);
532
- }
533
- onBufferLow(peerId) {
534
- const peer = this.peers.get(peerId);
535
- if (!peer)
536
- return;
537
- peer.bufferPaused = false;
538
- this.log(`Buffer low for ${peerId.slice(0, 20)}, resuming responses`);
539
- // Process deferred requests
540
- this.processDeferredRequests(peer);
541
- }
542
- async processDeferredRequests(peer) {
543
- while (!peer.bufferPaused && peer.deferredRequests.length > 0) {
544
- const req = peer.deferredRequests.shift();
545
- await this.processRequest(peer, req);
546
- }
547
- }
548
- orderedConnectedPeers(excludePeerId) {
549
- const connectedAll = Array.from(this.peers.values())
550
- .filter((peer) => peer.dataChannelReady);
551
- if (connectedAll.length === 0)
552
- return [];
553
- const peerIds = connectedAll.map((peer) => peer.peerId);
554
- syncSelectorPeers(this.peerSelector, peerIds);
555
- const connectedPeers = connectedAll
556
- .filter((peer) => !excludePeerId || peer.peerId !== excludePeerId);
557
- const selectorOrder = this.peerSelector.selectPeers();
558
- const rank = new Map(selectorOrder.map((peerId, idx) => [peerId, idx]));
559
- connectedPeers.sort((a, b) => {
560
- if (a.pool === 'follows' && b.pool !== 'follows')
561
- return -1;
562
- if (a.pool !== 'follows' && b.pool === 'follows')
563
- return 1;
564
- return (rank.get(a.peerId) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b.peerId) ?? Number.MAX_SAFE_INTEGER);
565
- });
566
- return connectedPeers;
567
- }
568
- async peerMetadataPointerHash() {
569
- return sha256(new TextEncoder().encode(PEER_METADATA_POINTER_SLOT_KEY));
570
- }
571
- createInFlightRequest(peer, hash, htl) {
572
- const hashKey = hashToKey(hash);
573
- const startedAt = Date.now();
574
- this.peerSelector.recordRequest(peer.peerId, 40);
575
- const promise = new Promise((resolve) => {
576
- const timeout = setTimeout(() => {
577
- peer.pendingRequests.delete(hashKey);
578
- this.peerSelector.recordTimeout(peer.peerId);
579
- resolve({ peerId: peer.peerId, data: null, elapsedMs: Math.max(1, Date.now() - startedAt) });
580
- }, this.requestTimeout);
581
- peer.pendingRequests.set(hashKey, {
582
- hash,
583
- startedAt,
584
- resolve: (data) => {
585
- resolve({ peerId: peer.peerId, data, elapsedMs: Math.max(1, Date.now() - startedAt) });
586
- },
587
- timeout,
588
- });
589
- peer.stats.requestsSent++;
590
- const req = createRequest(hash, htl);
591
- const encoded = new Uint8Array(encodeRequest(req));
592
- this.sendDataToPeer(peer, encoded);
593
- });
594
- return {
595
- peerId: peer.peerId,
596
- settled: false,
597
- promise,
598
- };
599
- }
600
- async waitForInFlightResult(inFlight, waitMs) {
601
- const active = inFlight.filter((task) => !task.settled);
602
- if (active.length === 0 || waitMs <= 0)
603
- return null;
604
- const timeout = new Promise((resolve) => {
605
- setTimeout(() => resolve(null), waitMs);
606
- });
607
- const outcome = await Promise.race([
608
- timeout,
609
- ...active.map((task) => task.promise.then((result) => ({
610
- task,
611
- data: result.data,
612
- elapsedMs: result.elapsedMs,
613
- }))),
614
- ]);
615
- if (!outcome)
616
- return null;
617
- outcome.task.settled = true;
618
- return outcome;
619
- }
620
- clearPendingHashFromPeers(hashKey, keepPeerId) {
621
- for (const peer of this.peers.values()) {
622
- if (keepPeerId && peer.peerId === keepPeerId)
623
- continue;
624
- const pending = peer.pendingRequests.get(hashKey);
625
- if (!pending)
626
- continue;
627
- clearTimeout(pending.timeout);
628
- peer.pendingRequests.delete(hashKey);
629
- }
630
- }
631
- /**
632
- * Persist selector metadata snapshot to local store.
633
- * Returns the snapshot hash.
634
- */
635
- async persistPeerMetadata() {
636
- const snapshot = this.peerSelector.exportPeerMetadataSnapshot();
637
- const bytes = new TextEncoder().encode(JSON.stringify(snapshot));
638
- const snapshotHash = await sha256(bytes);
639
- await this.localStore.put(snapshotHash, bytes);
640
- const pointerHash = await this.peerMetadataPointerHash();
641
- await this.localStore.delete(pointerHash);
642
- await this.localStore.put(pointerHash, new TextEncoder().encode(toHex(snapshotHash)));
643
- return snapshotHash;
644
- }
645
- /**
646
- * Load selector metadata snapshot from local store.
647
- */
648
- async loadPeerMetadata() {
649
- const pointerHash = await this.peerMetadataPointerHash();
650
- const pointerBytes = await this.localStore.get(pointerHash);
651
- if (!pointerBytes)
652
- return false;
653
- const pointerHex = new TextDecoder().decode(pointerBytes).trim();
654
- if (pointerHex.length !== 64)
655
- return false;
656
- const snapshotHash = fromHex(pointerHex);
657
- if (snapshotHash.length !== 32)
658
- return false;
659
- const snapshotBytes = await this.localStore.get(snapshotHash);
660
- if (!snapshotBytes)
661
- return false;
662
- let snapshot;
663
- try {
664
- snapshot = JSON.parse(new TextDecoder().decode(snapshotBytes));
665
- }
666
- catch {
667
- return false;
668
- }
669
- this.peerSelector.importPeerMetadataSnapshot(snapshot);
670
- syncSelectorPeers(this.peerSelector, Array.from(this.peers.keys()));
671
- return true;
672
- }
673
- // ============================================================================
674
- // Data Protocol
675
- // ============================================================================
676
- sendDataToPeer(peer, data) {
677
- peer.stats.bytesSent += data.byteLength;
678
- this.sendCommand({ type: 'rtc:sendData', peerId: peer.peerId, data });
679
- }
680
- async onDataChannelMessage(peerId, data) {
681
- const peer = this.peers.get(peerId);
682
- if (!peer)
683
- return;
684
- // Count all inbound DataChannel bytes (requests + responses + protocol overhead).
685
- peer.stats.bytesReceived += data.byteLength;
686
- const msg = parseMessage(data);
687
- if (!msg) {
688
- this.log(`Failed to parse message from ${peerId}`);
689
- return;
690
- }
691
- if (msg.type === MSG_TYPE_REQUEST) {
692
- await this.handleRequest(peer, msg.body);
693
- }
694
- else if (msg.type === MSG_TYPE_RESPONSE) {
695
- await this.handleResponse(peer, msg.body);
696
- }
697
- }
698
- async handleRequest(peer, req) {
699
- peer.stats.requestsReceived++;
700
- // If buffer is full, defer the request for later processing
701
- if (peer.bufferPaused) {
702
- // Limit deferred requests to prevent memory issues
703
- if (peer.deferredRequests.length < 100) {
704
- peer.deferredRequests.push(req);
705
- }
706
- return;
707
- }
708
- await this.processRequest(peer, req);
709
- }
710
- async processRequest(peer, req) {
711
- await this.meshRouter.handleRequest(peer.peerId, req);
712
- }
713
- async handleResponse(peer, res) {
714
- peer.stats.responsesReceived++;
715
- const hashKey = hashToKey(res.h);
716
- const pending = peer.pendingRequests.get(hashKey);
717
- if (!pending) {
718
- const hasRequesters = this.meshRouter.hasInFlight(hashKey);
719
- // Late response: cache if we requested this hash recently
720
- const requestedAt = this.recentRequests.get(hashKey);
721
- if (!requestedAt && !hasRequesters)
722
- return;
723
- if (requestedAt && Date.now() - requestedAt > 60000) {
724
- this.recentRequests.delete(hashKey);
725
- if (!hasRequesters)
726
- return;
727
- }
728
- const valid = await verifyHash(res.d, res.h);
729
- if (valid) {
730
- await this.localStore.put(res.h, res.d);
731
- if (requestedAt) {
732
- this.recentRequests.delete(hashKey);
733
- }
734
- if (hasRequesters) {
735
- await this.meshRouter.resolve(res.h, res.d);
736
- }
737
- }
738
- return;
739
- }
740
- clearTimeout(pending.timeout);
741
- peer.pendingRequests.delete(hashKey);
742
- // Verify hash
743
- const valid = await verifyHash(res.d, res.h);
744
- const elapsedMs = pending.startedAt ? Math.max(1, Date.now() - pending.startedAt) : this.requestTimeout;
745
- if (valid) {
746
- // Store locally
747
- await this.localStore.put(res.h, res.d);
748
- this.peerSelector.recordSuccess(peer.peerId, elapsedMs, res.d.length);
749
- pending.resolve(res.d);
750
- await this.meshRouter.resolve(res.h, res.d);
751
- }
752
- else {
753
- this.log(`Hash mismatch from ${peer.peerId}`);
754
- this.peerSelector.recordFailure(peer.peerId);
755
- pending.resolve(null);
756
- }
757
- }
758
- async sendResponse(peer, hash, data) {
759
- if (!peer.dataChannelReady)
760
- return;
761
- peer.stats.responsesSent++;
762
- // Fragment if needed
763
- if (data.length > FRAGMENT_SIZE) {
764
- const totalFragments = Math.ceil(data.length / FRAGMENT_SIZE);
765
- for (let i = 0; i < totalFragments; i++) {
766
- const start = i * FRAGMENT_SIZE;
767
- const end = Math.min(start + FRAGMENT_SIZE, data.length);
768
- const fragment = data.slice(start, end);
769
- const res = createFragmentResponse(hash, fragment, i, totalFragments);
770
- const encoded = new Uint8Array(encodeResponse(res));
771
- this.sendDataToPeer(peer, encoded);
772
- }
773
- }
774
- else {
775
- const res = createResponse(hash, data);
776
- const encoded = new Uint8Array(encodeResponse(res));
777
- this.sendDataToPeer(peer, encoded);
778
- }
779
- }
780
- sendRequestToPeer(peer, hash, htl) {
781
- if (!peer.dataChannelReady) {
782
- return false;
783
- }
784
- const encoded = encodeForwardRequest(hash, htl);
785
- this.sendDataToPeer(peer, encoded);
786
- return true;
787
- }
788
- // ============================================================================
789
- // Public API
790
- // ============================================================================
791
- /**
792
- * Request data from peers
793
- */
794
- async get(hash) {
795
- const orderedPeers = this.orderedConnectedPeers();
796
- if (orderedPeers.length === 0)
797
- return null;
798
- const dispatch = normalizeDispatchConfig(this.routing.dispatch, orderedPeers.length);
799
- const wavePlan = buildHedgedWavePlan(orderedPeers.length, dispatch);
800
- if (wavePlan.length === 0)
801
- return null;
802
- const hashKey = hashToKey(hash);
803
- this.recentRequests.set(hashKey, Date.now());
804
- const deadline = Date.now() + this.requestTimeout;
805
- const inFlight = [];
806
- let nextPeerIdx = 0;
807
- for (let waveIdx = 0; waveIdx < wavePlan.length; waveIdx++) {
808
- const waveSize = wavePlan[waveIdx];
809
- const from = nextPeerIdx;
810
- const to = Math.min(from + waveSize, orderedPeers.length);
811
- nextPeerIdx = to;
812
- for (const peer of orderedPeers.slice(from, to)) {
813
- inFlight.push(this.createInFlightRequest(peer, hash, MAX_HTL));
814
- }
815
- const isLastWave = waveIdx === wavePlan.length - 1 || nextPeerIdx >= orderedPeers.length;
816
- const windowEnd = isLastWave
817
- ? deadline
818
- : Math.min(deadline, Date.now() + dispatch.hedgeIntervalMs);
819
- while (Date.now() < windowEnd) {
820
- const remaining = windowEnd - Date.now();
821
- const result = await this.waitForInFlightResult(inFlight, remaining);
822
- if (!result)
823
- break;
824
- if (!result.data)
825
- continue;
826
- this.clearPendingHashFromPeers(hashKey, result.task.peerId);
827
- return result.data;
828
- }
829
- if (Date.now() >= deadline)
830
- break;
831
- }
832
- this.clearPendingHashFromPeers(hashKey);
833
- return null;
834
- }
835
- /**
836
- * Get peer stats for UI
837
- */
838
- getPeerStats() {
839
- return Array.from(this.peers.values()).map(peer => ({
840
- peerId: peer.peerId,
841
- pubkey: peer.pubkey,
842
- connected: peer.state === 'connected' && peer.dataChannelReady,
843
- pool: peer.pool,
844
- requestsSent: peer.stats.requestsSent,
845
- requestsReceived: peer.stats.requestsReceived,
846
- responsesSent: peer.stats.responsesSent,
847
- responsesReceived: peer.stats.responsesReceived,
848
- bytesSent: peer.stats.bytesSent,
849
- bytesReceived: peer.stats.bytesReceived,
850
- forwardedRequests: peer.stats.forwardedRequests,
851
- forwardedResolved: peer.stats.forwardedResolved,
852
- forwardedSuppressed: peer.stats.forwardedSuppressed,
853
- }));
854
- }
855
- /**
856
- * Get connected peer count
857
- */
858
- getConnectedCount() {
859
- let count = 0;
860
- for (const peer of this.peers.values()) {
861
- if (peer.state === 'connected' && peer.dataChannelReady) {
862
- count++;
863
- }
864
- }
865
- return count;
866
- }
867
- /**
868
- * Set pool configuration
869
- */
870
- setPoolConfig(config) {
871
- this.poolConfig = {
872
- follows: { maxConnections: config.follows.max, satisfiedConnections: config.follows.satisfied },
873
- other: { maxConnections: config.other.max, satisfiedConnections: config.other.satisfied },
874
- };
875
- this.log('Pool config updated:', this.poolConfig);
876
- // Re-broadcast hello to trigger peer discovery with new limits
877
- this.sendHello();
878
- }
879
- /**
880
- * Update identity (pubkey) and restart signaling if already running.
881
- * This keeps peerId consistent with the current account.
882
- */
883
- setIdentity(pubkey) {
884
- if (this.myPeerId.pubkey === pubkey)
885
- return;
886
- const wasStarted = !!this.helloInterval;
887
- this.stop();
888
- this.myPeerId = new PeerId(pubkey);
889
- if (wasStarted) {
890
- this.start();
891
- }
892
- }
893
- // ============================================================================
894
- // Helpers
895
- // ============================================================================
896
- log(...args) {
897
- if (this.debug) {
898
- console.log('[WebRTC]', ...args);
899
- }
900
- }
901
- }
902
- //# sourceMappingURL=webrtcController.js.map