@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
@@ -0,0 +1,519 @@
1
+ /**
2
+ * WebRTC Proxy
3
+ *
4
+ * Thin transport layer that manages RTCPeerConnection in main thread.
5
+ * Worker controls all logic - this just executes commands and reports events.
6
+ *
7
+ * Main thread owns RTCPeerConnection because it's not available in workers.
8
+ * See: https://github.com/w3c/webrtc-extensions/issues/77
9
+ */
10
+
11
+ import type { WebRTCCommand, WebRTCEvent } from './protocol.js';
12
+ import { BoundedQueue } from './boundedQueue.js';
13
+ import { getErrorMessage } from './errorMessage.js';
14
+ import { UploadRateLimiter } from './uploadRateLimiter.js';
15
+
16
+ const REQUEST_MESSAGE_TYPE = 0x00;
17
+
18
+ const isTestMode = typeof globalThis !== 'undefined' &&
19
+ Boolean((globalThis as { __HTREE_P2P_TEST_MODE__?: boolean }).__HTREE_P2P_TEST_MODE__);
20
+ const ICE_SERVERS: RTCIceServer[] = isTestMode
21
+ ? []
22
+ : [
23
+ { urls: 'stun:stun.l.google.com:19302' },
24
+ { urls: 'stun:stun.cloudflare.com:3478' },
25
+ ];
26
+
27
+ interface PeerConnection {
28
+ pc: RTCPeerConnection;
29
+ dataChannel: RTCDataChannel | null;
30
+ pubkey: string;
31
+ pendingCandidates: RTCIceCandidateInit[];
32
+ sendQueue: BoundedQueue<Uint8Array>;
33
+ sending: boolean;
34
+ bufferHighSignaled: boolean; // Track if we've signaled high buffer to worker
35
+ drainTimeoutId: ReturnType<typeof setTimeout> | null;
36
+ }
37
+
38
+ type EventCallback = (event: WebRTCEvent) => void;
39
+ type WebRTCProxyConfig = {
40
+ maxUploadBytesPerSecond?: number | null;
41
+ };
42
+
43
+ export class WebRTCProxy {
44
+ private peers = new Map<string, PeerConnection>();
45
+ private onEvent: EventCallback;
46
+ private readonly uploadRateLimiter: UploadRateLimiter;
47
+
48
+ // Queue limits to prevent memory blowup on slow/stalled connections
49
+ private static readonly MAX_QUEUE_BYTES = 8 * 1024 * 1024; // 8MB per peer
50
+ private static readonly MAX_QUEUE_ITEMS = 100;
51
+
52
+ constructor(onEvent: EventCallback, config: WebRTCProxyConfig = {}) {
53
+ this.onEvent = onEvent;
54
+ this.uploadRateLimiter = new UploadRateLimiter({
55
+ bytesPerSecond: config.maxUploadBytesPerSecond,
56
+ });
57
+ }
58
+
59
+ private createSendQueue(peerId: string): BoundedQueue<Uint8Array> {
60
+ return new BoundedQueue<Uint8Array>({
61
+ maxItems: WebRTCProxy.MAX_QUEUE_ITEMS,
62
+ maxBytes: WebRTCProxy.MAX_QUEUE_BYTES,
63
+ getBytes: (item) => item.byteLength,
64
+ onDrop: (item) => {
65
+ console.warn(`[WebRTCProxy] Queue overflow for ${peerId.slice(0, 8)}, dropped ${item.byteLength}B`);
66
+ },
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Handle command from worker
72
+ */
73
+ handleCommand(cmd: WebRTCCommand): void {
74
+ switch (cmd.type) {
75
+ case 'rtc:createPeer':
76
+ this.createPeer(cmd.peerId, cmd.pubkey);
77
+ break;
78
+ case 'rtc:closePeer':
79
+ this.closePeer(cmd.peerId);
80
+ break;
81
+ case 'rtc:createOffer':
82
+ this.createOffer(cmd.peerId);
83
+ break;
84
+ case 'rtc:createAnswer':
85
+ this.createAnswer(cmd.peerId);
86
+ break;
87
+ case 'rtc:setLocalDescription':
88
+ this.setLocalDescription(cmd.peerId, cmd.sdp);
89
+ break;
90
+ case 'rtc:setRemoteDescription':
91
+ this.setRemoteDescription(cmd.peerId, cmd.sdp);
92
+ break;
93
+ case 'rtc:addIceCandidate':
94
+ this.addIceCandidate(cmd.peerId, cmd.candidate);
95
+ break;
96
+ case 'rtc:sendData':
97
+ this.sendData(cmd.peerId, cmd.data);
98
+ break;
99
+ }
100
+ }
101
+
102
+ private createPeer(peerId: string, pubkey: string): void {
103
+ // Clean up existing if present
104
+ if (this.peers.has(peerId)) {
105
+ this.closePeer(peerId);
106
+ }
107
+
108
+ const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
109
+
110
+ const peer: PeerConnection = {
111
+ pc,
112
+ dataChannel: null,
113
+ pubkey,
114
+ pendingCandidates: [],
115
+ sendQueue: this.createSendQueue(peerId),
116
+ sending: false,
117
+ bufferHighSignaled: false,
118
+ drainTimeoutId: null,
119
+ };
120
+
121
+ // Create data channel (offerer creates, answerer receives via ondatachannel)
122
+ const dc = pc.createDataChannel('hashtree', {
123
+ ordered: true,
124
+ });
125
+ this.setupDataChannel(peerId, dc);
126
+ peer.dataChannel = dc;
127
+
128
+ // Handle incoming data channel (for answerer)
129
+ pc.ondatachannel = (event) => {
130
+ this.setupDataChannel(peerId, event.channel);
131
+ peer.dataChannel = event.channel;
132
+ };
133
+
134
+ // ICE candidate gathering
135
+ pc.onicecandidate = (event) => {
136
+ this.onEvent({
137
+ type: 'rtc:iceCandidate',
138
+ peerId,
139
+ candidate: event.candidate?.toJSON() ?? null,
140
+ });
141
+
142
+ if (!event.candidate) {
143
+ this.onEvent({ type: 'rtc:iceGatheringComplete', peerId });
144
+ }
145
+ };
146
+
147
+ // Connection state changes
148
+ pc.onconnectionstatechange = () => {
149
+ this.onEvent({
150
+ type: 'rtc:peerStateChange',
151
+ peerId,
152
+ state: pc.connectionState,
153
+ });
154
+
155
+ if (pc.connectionState === 'closed' || pc.connectionState === 'failed') {
156
+ this.cleanupPeer(peerId);
157
+ }
158
+ };
159
+
160
+ this.peers.set(peerId, peer);
161
+ this.onEvent({ type: 'rtc:peerCreated', peerId });
162
+ }
163
+
164
+ private setupDataChannel(peerId: string, dc: RTCDataChannel): void {
165
+ dc.binaryType = 'arraybuffer';
166
+
167
+ dc.onopen = () => {
168
+ this.onEvent({ type: 'rtc:dataChannelOpen', peerId });
169
+ };
170
+
171
+ // If channel is already open (can happen with ondatachannel), fire event immediately
172
+ if (dc.readyState === 'open') {
173
+ this.onEvent({ type: 'rtc:dataChannelOpen', peerId });
174
+ }
175
+
176
+ dc.onclose = () => {
177
+ this.onEvent({ type: 'rtc:dataChannelClose', peerId });
178
+ };
179
+
180
+ dc.onerror = (event) => {
181
+ const errorEvent = event as RTCErrorEvent;
182
+ this.onEvent({
183
+ type: 'rtc:dataChannelError',
184
+ peerId,
185
+ error: errorEvent.error?.message || 'Unknown error',
186
+ });
187
+ };
188
+
189
+ dc.onmessage = (event) => {
190
+ const data = event.data instanceof ArrayBuffer
191
+ ? new Uint8Array(event.data)
192
+ : new Uint8Array(0);
193
+
194
+ this.onEvent({
195
+ type: 'rtc:dataChannelMessage',
196
+ peerId,
197
+ data,
198
+ });
199
+ };
200
+ }
201
+
202
+ private async createOffer(peerId: string): Promise<void> {
203
+ const peer = this.peers.get(peerId);
204
+ if (!peer) return;
205
+
206
+ try {
207
+ const offer = await peer.pc.createOffer();
208
+ this.onEvent({
209
+ type: 'rtc:offerCreated',
210
+ peerId,
211
+ sdp: offer,
212
+ });
213
+ } catch (err) {
214
+ console.error('[WebRTCProxy] Failed to create offer:', err);
215
+ }
216
+ }
217
+
218
+ private async createAnswer(peerId: string): Promise<void> {
219
+ const peer = this.peers.get(peerId);
220
+ if (!peer) return;
221
+
222
+ try {
223
+ const answer = await peer.pc.createAnswer();
224
+ this.onEvent({
225
+ type: 'rtc:answerCreated',
226
+ peerId,
227
+ sdp: answer,
228
+ });
229
+ } catch (err) {
230
+ console.error('[WebRTCProxy] Failed to create answer:', err);
231
+ }
232
+ }
233
+
234
+ private async setLocalDescription(peerId: string, sdp: RTCSessionDescriptionInit): Promise<void> {
235
+ const peer = this.peers.get(peerId);
236
+ if (!peer) return;
237
+
238
+ try {
239
+ await peer.pc.setLocalDescription(sdp);
240
+ this.onEvent({ type: 'rtc:descriptionSet', peerId });
241
+ } catch (err) {
242
+ this.onEvent({
243
+ type: 'rtc:descriptionSet',
244
+ peerId,
245
+ error: getErrorMessage(err),
246
+ });
247
+ }
248
+ }
249
+
250
+ private async setRemoteDescription(peerId: string, sdp: RTCSessionDescriptionInit): Promise<void> {
251
+ const peer = this.peers.get(peerId);
252
+ if (!peer) return;
253
+
254
+ try {
255
+ if (sdp.type === 'offer'
256
+ && peer.pc.signalingState !== 'stable'
257
+ && peer.pc.signalingState !== 'closed') {
258
+ await peer.pc.setLocalDescription({ type: 'rollback' });
259
+ }
260
+
261
+ await peer.pc.setRemoteDescription(sdp);
262
+
263
+ // Apply any pending ICE candidates
264
+ for (const candidate of peer.pendingCandidates) {
265
+ await peer.pc.addIceCandidate(candidate);
266
+ }
267
+ peer.pendingCandidates = [];
268
+
269
+ this.onEvent({ type: 'rtc:descriptionSet', peerId });
270
+ } catch (err) {
271
+ this.onEvent({
272
+ type: 'rtc:descriptionSet',
273
+ peerId,
274
+ error: getErrorMessage(err),
275
+ });
276
+ }
277
+ }
278
+
279
+ private async addIceCandidate(peerId: string, candidate: RTCIceCandidateInit): Promise<void> {
280
+ const peer = this.peers.get(peerId);
281
+ if (!peer) return;
282
+
283
+ // Queue if remote description not set yet
284
+ if (!peer.pc.remoteDescription) {
285
+ peer.pendingCandidates.push(candidate);
286
+ return;
287
+ }
288
+
289
+ try {
290
+ await peer.pc.addIceCandidate(candidate);
291
+ } catch (err) {
292
+ console.error('[WebRTCProxy] Failed to add ICE candidate:', err);
293
+ }
294
+ }
295
+
296
+ // 256KB threshold - pause sending when buffer exceeds this
297
+ private static readonly BUFFER_THRESHOLD = 256 * 1024;
298
+ // 4MB threshold for sendQueue - signal worker to pause when exceeded
299
+ private static readonly QUEUE_HIGH_THRESHOLD = 4 * 1024 * 1024;
300
+ // 1MB threshold for sendQueue - signal worker to resume when below
301
+ private static readonly QUEUE_LOW_THRESHOLD = 1 * 1024 * 1024;
302
+
303
+ private getQueueSize(peer: PeerConnection): number {
304
+ return peer.sendQueue.bytes;
305
+ }
306
+
307
+ private isPriorityDataMessage(data: Uint8Array): boolean {
308
+ return data.byteLength > 0 && data[0] === REQUEST_MESSAGE_TYPE;
309
+ }
310
+
311
+ private sendData(peerId: string, data: Uint8Array): void {
312
+ const peer = this.peers.get(peerId);
313
+ if (!peer?.dataChannel || peer.dataChannel.readyState !== 'open') {
314
+ return;
315
+ }
316
+
317
+ // Small request frames should overtake bulky response traffic so cache misses
318
+ // are not starved by background uploads on the same peer connection.
319
+ if (this.isPriorityDataMessage(data)) {
320
+ peer.sendQueue.unshift(data);
321
+ } else {
322
+ peer.sendQueue.push(data);
323
+ }
324
+
325
+ // Check if queue is getting too large - signal worker to slow down
326
+ const queueSize = this.getQueueSize(peer);
327
+ if (!peer.bufferHighSignaled && queueSize > WebRTCProxy.QUEUE_HIGH_THRESHOLD) {
328
+ peer.bufferHighSignaled = true;
329
+ this.onEvent({ type: 'rtc:bufferHigh', peerId });
330
+ }
331
+
332
+ // Start draining if not already
333
+ if (!peer.sending) {
334
+ this.drainQueue(peerId);
335
+ }
336
+ }
337
+
338
+ private drainQueue(peerId: string): void {
339
+ const peer = this.peers.get(peerId);
340
+ if (!peer?.dataChannel || peer.dataChannel.readyState !== 'open') {
341
+ return;
342
+ }
343
+
344
+ if (peer.drainTimeoutId) {
345
+ clearTimeout(peer.drainTimeoutId);
346
+ peer.drainTimeoutId = null;
347
+ }
348
+
349
+ peer.sending = true;
350
+ const dc = peer.dataChannel;
351
+
352
+ // Send as much as we can without overflowing the buffer
353
+ while (!peer.sendQueue.isEmpty && dc.bufferedAmount < WebRTCProxy.BUFFER_THRESHOLD) {
354
+ const nextData = peer.sendQueue.peek();
355
+ if (!nextData) {
356
+ break;
357
+ }
358
+
359
+ const reservation = this.uploadRateLimiter.reserve(nextData.byteLength);
360
+ if (!reservation.allowed) {
361
+ peer.sending = false;
362
+ this.scheduleRateLimitedDrain(peerId, reservation.delayMs);
363
+ return;
364
+ }
365
+
366
+ const data = peer.sendQueue.shift()!;
367
+ try {
368
+ const payload = data.byteOffset === 0 && data.byteLength === data.buffer.byteLength
369
+ ? data.buffer
370
+ : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
371
+ dc.send(payload as ArrayBuffer);
372
+ } catch {
373
+ // Drop on failure instead of infinite re-queue (prevents memory blowup)
374
+ console.warn(`[WebRTCProxy] Send failed for ${peerId.slice(0, 8)}, dropped ${data.byteLength}B`);
375
+ break;
376
+ }
377
+ }
378
+
379
+ // Check if queue has drained enough to signal worker to resume
380
+ if (peer.bufferHighSignaled) {
381
+ const queueSize = this.getQueueSize(peer);
382
+ if (queueSize < WebRTCProxy.QUEUE_LOW_THRESHOLD) {
383
+ peer.bufferHighSignaled = false;
384
+ this.onEvent({ type: 'rtc:bufferLow', peerId });
385
+ }
386
+ }
387
+
388
+ // If there's more to send, wait for buffer to drain
389
+ if (!peer.sendQueue.isEmpty) {
390
+ dc.bufferedAmountLowThreshold = WebRTCProxy.BUFFER_THRESHOLD / 2;
391
+ dc.onbufferedamountlow = () => {
392
+ dc.onbufferedamountlow = null;
393
+ this.drainQueue(peerId);
394
+ };
395
+ } else {
396
+ peer.sending = false;
397
+ }
398
+ }
399
+
400
+ private closePeer(peerId: string): void {
401
+ const peer = this.peers.get(peerId);
402
+ if (!peer) return;
403
+
404
+ this.cleanupPeer(peerId);
405
+ this.onEvent({ type: 'rtc:peerClosed', peerId });
406
+ }
407
+
408
+ private cleanupPeer(peerId: string): void {
409
+ const peer = this.peers.get(peerId);
410
+ if (!peer) return;
411
+
412
+ // Clear send queue
413
+ peer.sendQueue.clear();
414
+ peer.sending = false;
415
+ if (peer.drainTimeoutId) {
416
+ clearTimeout(peer.drainTimeoutId);
417
+ peer.drainTimeoutId = null;
418
+ }
419
+
420
+ // Close data channel
421
+ if (peer.dataChannel) {
422
+ peer.dataChannel.onopen = null;
423
+ peer.dataChannel.onclose = null;
424
+ peer.dataChannel.onerror = null;
425
+ peer.dataChannel.onmessage = null;
426
+ peer.dataChannel.onbufferedamountlow = null;
427
+ peer.dataChannel.close();
428
+ }
429
+
430
+ // Close peer connection
431
+ peer.pc.onicecandidate = null;
432
+ peer.pc.ondatachannel = null;
433
+ peer.pc.onconnectionstatechange = null;
434
+ peer.pc.close();
435
+
436
+ this.peers.delete(peerId);
437
+ }
438
+
439
+ /**
440
+ * Close all connections
441
+ */
442
+ close(): void {
443
+ for (const peerId of this.peers.keys()) {
444
+ this.closePeer(peerId);
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Get connected peer count
450
+ */
451
+ getConnectedCount(): number {
452
+ let count = 0;
453
+ for (const peer of this.peers.values()) {
454
+ if (peer.pc.connectionState === 'connected' &&
455
+ peer.dataChannel?.readyState === 'open') {
456
+ count++;
457
+ }
458
+ }
459
+ return count;
460
+ }
461
+
462
+ /**
463
+ * Get all peer IDs
464
+ */
465
+ getPeerIds(): string[] {
466
+ return Array.from(this.peers.keys());
467
+ }
468
+
469
+ setUploadLimitBytesPerSecond(maxUploadBytesPerSecond?: number | null): void {
470
+ this.uploadRateLimiter.setBytesPerSecond(maxUploadBytesPerSecond);
471
+ for (const peerId of this.peers.keys()) {
472
+ const peer = this.peers.get(peerId);
473
+ if (!peer || peer.sendQueue.isEmpty) {
474
+ continue;
475
+ }
476
+ if (!peer.sending) {
477
+ this.drainQueue(peerId);
478
+ }
479
+ }
480
+ }
481
+
482
+ private scheduleRateLimitedDrain(peerId: string, delayMs: number): void {
483
+ const peer = this.peers.get(peerId);
484
+ if (!peer || peer.drainTimeoutId) {
485
+ return;
486
+ }
487
+
488
+ peer.drainTimeoutId = setTimeout(() => {
489
+ const activePeer = this.peers.get(peerId);
490
+ if (!activePeer) {
491
+ return;
492
+ }
493
+ activePeer.drainTimeoutId = null;
494
+ this.drainQueue(peerId);
495
+ }, delayMs);
496
+ }
497
+ }
498
+
499
+ // Singleton instance
500
+ let instance: WebRTCProxy | null = null;
501
+
502
+ export function initWebRTCProxy(onEvent: EventCallback): WebRTCProxy {
503
+ if (instance) {
504
+ instance.close();
505
+ }
506
+ instance = new WebRTCProxy(onEvent);
507
+ return instance;
508
+ }
509
+
510
+ export function getWebRTCProxy(): WebRTCProxy | null {
511
+ return instance;
512
+ }
513
+
514
+ export function closeWebRTCProxy(): void {
515
+ if (instance) {
516
+ instance.close();
517
+ instance = null;
518
+ }
519
+ }
@@ -0,0 +1,31 @@
1
+ import type { CID } from '@hashtree/core';
2
+
3
+ const ENCRYPTION_KEY_BYTES = 32;
4
+
5
+ /**
6
+ * Uploads must always point to encrypted content CIDs.
7
+ */
8
+ export function assertEncryptedUploadCid(cid: CID): void {
9
+ if (!cid.key) {
10
+ throw new Error('Refusing to upload unencrypted CID');
11
+ }
12
+ if (cid.key.length !== ENCRYPTION_KEY_BYTES) {
13
+ throw new Error('Refusing to upload CID with invalid encryption key');
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Mark known-encrypted block hashes as safe for peer serving.
19
+ */
20
+ export function markEncryptedHashes(hashes: Iterable<string>, allowlist: Set<string>): void {
21
+ for (const hashHex of hashes) {
22
+ allowlist.add(hashHex.toLowerCase());
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Peer responses are restricted to hashes explicitly marked as encrypted.
28
+ */
29
+ export function shouldServeHashToPeer(hashHex: string, allowlist: ReadonlySet<string>): boolean {
30
+ return allowlist.has(hashHex.toLowerCase());
31
+ }
@@ -0,0 +1,124 @@
1
+ import type { CID } from '@hashtree/core';
2
+
3
+ export interface BlossomServerConfig {
4
+ url: string;
5
+ read?: boolean;
6
+ write?: boolean;
7
+ }
8
+
9
+ export interface WorkerConfig {
10
+ storeName?: string;
11
+ blossomServers?: BlossomServerConfig[];
12
+ relays?: string[];
13
+ storageMaxBytes?: number;
14
+ connectivityProbeIntervalMs?: number;
15
+ diagnosticsEnabled?: boolean;
16
+ diagnosticsMirrorToConsole?: boolean;
17
+ }
18
+
19
+ export type WorkerDiagnosticLevel = 'debug' | 'info' | 'warn' | 'error';
20
+ export type WorkerDiagnosticDataValue = string | number | boolean | null;
21
+
22
+ export interface WorkerDiagnosticEvent {
23
+ scope: string;
24
+ code: string;
25
+ level: WorkerDiagnosticLevel;
26
+ message: string;
27
+ timestamp: number;
28
+ data?: Record<string, WorkerDiagnosticDataValue>;
29
+ }
30
+
31
+ export interface ConnectivityState {
32
+ online: boolean;
33
+ reachableReadServers: number;
34
+ totalReadServers: number;
35
+ reachableWriteServers: number;
36
+ totalWriteServers: number;
37
+ updatedAt: number;
38
+ }
39
+
40
+ export type BlobSource = 'idb' | 'blossom' | 'p2p';
41
+
42
+ export interface UploadServerStatus {
43
+ url: string;
44
+ uploaded: number;
45
+ skipped: number;
46
+ failed: number;
47
+ }
48
+
49
+ export interface UploadProgressState {
50
+ hashHex: string;
51
+ nhash: string;
52
+ totalServers: number;
53
+ processedServers: number;
54
+ uploadedServers: number;
55
+ skippedServers: number;
56
+ failedServers: number;
57
+ totalChunks?: number;
58
+ processedChunks?: number;
59
+ /** 0..1 normalized progress for chunk upload traversal */
60
+ progressRatio?: number;
61
+ serverStatuses?: UploadServerStatus[];
62
+ complete: boolean;
63
+ error?: string;
64
+ }
65
+
66
+ export interface BlossomBandwidthServerStats {
67
+ url: string;
68
+ bytesSent: number;
69
+ bytesReceived: number;
70
+ }
71
+
72
+ export interface BlossomBandwidthState {
73
+ totalBytesSent: number;
74
+ totalBytesReceived: number;
75
+ updatedAt: number;
76
+ servers: BlossomBandwidthServerStats[];
77
+ }
78
+
79
+ export interface BlobStreamStarted {
80
+ id: string;
81
+ streamId: string;
82
+ }
83
+
84
+ export interface RootResolveOptions {
85
+ timeoutMs?: number;
86
+ settleMs?: number;
87
+ }
88
+
89
+ export type WorkerRequest =
90
+ | { type: 'init'; id: string; config: WorkerConfig }
91
+ | { type: 'close'; id: string }
92
+ | { type: 'putBlob'; id: string; data: Uint8Array; mimeType?: string; upload?: boolean }
93
+ | { type: 'beginPutBlobStream'; id: string; mimeType?: string; upload?: boolean }
94
+ | { type: 'appendPutBlobStream'; id: string; streamId: string; chunk: Uint8Array }
95
+ | { type: 'finishPutBlobStream'; id: string; streamId: string }
96
+ | { type: 'cancelPutBlobStream'; id: string; streamId: string }
97
+ | { type: 'p2pFetchResult'; id: string; requestId: string; data?: Uint8Array; error?: string }
98
+ | { type: 'getBlob'; id: string; hashHex: string; forPeer?: boolean }
99
+ | { type: 'registerMediaPort'; id: string; port: MessagePort }
100
+ | { type: 'setBlossomServers'; id: string; servers: BlossomServerConfig[] }
101
+ | { type: 'setStorageMaxBytes'; id: string; maxBytes: number }
102
+ | { type: 'getStorageStats'; id: string }
103
+ | { type: 'probeConnectivity'; id: string }
104
+ | { type: 'resolveRoot'; id: string; npub: string; path?: string; timeoutMs?: number; settleMs?: number }
105
+ | { type: 'watchRoot'; id: string; npub: string; path?: string; timeoutMs?: number; settleMs?: number }
106
+ | { type: 'unwatchRoot'; id: string; watchId: string };
107
+
108
+ export type WorkerResponse =
109
+ | { type: 'ready'; id: string }
110
+ | { type: 'error'; id?: string; error: string }
111
+ | { type: 'diagnostic'; event: WorkerDiagnosticEvent }
112
+ | { type: 'p2pFetch'; requestId: string; hashHex: string }
113
+ | { type: 'blobStreamStarted'; id: string; streamId: string }
114
+ | { type: 'blobStored'; id: string; hashHex: string; nhash: string }
115
+ | { type: 'blob'; id: string; data?: Uint8Array; source?: BlobSource; error?: string }
116
+ | { type: 'cid'; id: string; cid?: CID; error?: string }
117
+ | { type: 'void'; id: string; error?: string }
118
+ | { type: 'storageStats'; id: string; items: number; bytes: number; maxBytes: number; error?: string }
119
+ | { type: 'connectivity'; id: string; state?: ConnectivityState; error?: string }
120
+ | { type: 'rootWatchStarted'; id: string; watchId: string; cid?: CID; error?: string }
121
+ | { type: 'rootUpdate'; watchId: string; cid?: CID }
122
+ | { type: 'connectivityUpdate'; state: ConnectivityState }
123
+ | { type: 'blossomBandwidth'; stats: BlossomBandwidthState }
124
+ | { type: 'uploadProgress'; progress: UploadProgressState };