@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,361 @@
1
+ import type { Store } from '@hashtree/core';
2
+ import {
3
+ MAX_HTL,
4
+ createRequest,
5
+ decrementHTL,
6
+ encodeRequest,
7
+ hashToKey,
8
+ shouldForward,
9
+ verifyHash,
10
+ type DataRequest,
11
+ type PeerHTLConfig,
12
+ } from '@hashtree/nostr';
13
+
14
+ type TimeoutHandle = ReturnType<typeof setTimeout>;
15
+
16
+ export interface MeshQueryRouterPeer {
17
+ peerId: string;
18
+ canSend: () => boolean;
19
+ getHtlConfig: () => PeerHTLConfig;
20
+ sendRequest: (hash: Uint8Array, htl: number) => boolean;
21
+ sendResponse: (hash: Uint8Array, data: Uint8Array) => Promise<void>;
22
+ onForwardedRequest?: () => void;
23
+ onForwardedResolved?: () => void;
24
+ onForwardedSuppressed?: () => void;
25
+ }
26
+
27
+ export interface MeshPeerQueryOptions {
28
+ excludePeerId?: string;
29
+ htl: number;
30
+ }
31
+
32
+ export interface MeshQueryRouterConfig {
33
+ localStore: Store;
34
+ requestTimeoutMs: number;
35
+ upstreamFetch?: (hash: Uint8Array) => Promise<Uint8Array | null>;
36
+ queryPeers?: (hash: Uint8Array, options: MeshPeerQueryOptions) => Promise<Uint8Array | null>;
37
+ maxForwardsPerPeerWindow?: number;
38
+ forwardRateLimitWindowMs?: number;
39
+ }
40
+
41
+ export interface MeshForwardRateLimitConfig {
42
+ maxForwardsPerPeerWindow?: number;
43
+ windowMs?: number;
44
+ }
45
+
46
+ interface InFlightQuery {
47
+ requesterIds: Set<string>;
48
+ timeoutId: TimeoutHandle;
49
+ }
50
+
51
+ class SlidingWindowRateLimiter {
52
+ private readonly maxEvents: number;
53
+ private readonly windowMs: number;
54
+ private readonly eventsByPeer = new Map<string, number[]>();
55
+
56
+ constructor(maxEvents: number, windowMs: number) {
57
+ this.maxEvents = maxEvents;
58
+ this.windowMs = windowMs;
59
+ }
60
+
61
+ allow(peerId: string): boolean {
62
+ const now = Date.now();
63
+ const events = this.eventsByPeer.get(peerId) ?? [];
64
+ let firstActiveIndex = 0;
65
+ while (firstActiveIndex < events.length && now - events[firstActiveIndex] >= this.windowMs) {
66
+ firstActiveIndex += 1;
67
+ }
68
+ if (firstActiveIndex > 0) {
69
+ events.splice(0, firstActiveIndex);
70
+ }
71
+
72
+ if (events.length >= this.maxEvents) {
73
+ this.eventsByPeer.set(peerId, events);
74
+ return false;
75
+ }
76
+
77
+ events.push(now);
78
+ this.eventsByPeer.set(peerId, events);
79
+ return true;
80
+ }
81
+
82
+ resetPeer(peerId: string): void {
83
+ this.eventsByPeer.delete(peerId);
84
+ }
85
+
86
+ clear(): void {
87
+ this.eventsByPeer.clear();
88
+ }
89
+ }
90
+
91
+ export class MeshQueryRouter {
92
+ private readonly localStore: Store;
93
+ private readonly requestTimeoutMs: number;
94
+ private rateLimiter: SlidingWindowRateLimiter;
95
+ private readonly peers = new Map<string, MeshQueryRouterPeer>();
96
+ private readonly hashesByRequester = new Map<string, Set<string>>();
97
+ private readonly inFlightByHash = new Map<string, InFlightQuery>();
98
+ private readonly pendingUpstreamFetches = new Map<string, Promise<Uint8Array | null>>();
99
+ private upstreamFetch?: (hash: Uint8Array) => Promise<Uint8Array | null>;
100
+ private queryPeers?: (hash: Uint8Array, options: MeshPeerQueryOptions) => Promise<Uint8Array | null>;
101
+
102
+ constructor(config: MeshQueryRouterConfig) {
103
+ this.localStore = config.localStore;
104
+ this.requestTimeoutMs = config.requestTimeoutMs;
105
+ this.upstreamFetch = config.upstreamFetch;
106
+ this.queryPeers = config.queryPeers;
107
+ this.rateLimiter = this.createRateLimiter({
108
+ maxForwardsPerPeerWindow: config.maxForwardsPerPeerWindow,
109
+ windowMs: config.forwardRateLimitWindowMs,
110
+ });
111
+ }
112
+
113
+ registerPeer(peer: MeshQueryRouterPeer): void {
114
+ this.peers.set(peer.peerId, peer);
115
+ }
116
+
117
+ removePeer(peerId: string): void {
118
+ const hashes = this.hashesByRequester.get(peerId);
119
+ if (hashes) {
120
+ for (const hashKey of Array.from(hashes)) {
121
+ const inFlight = this.inFlightByHash.get(hashKey);
122
+ if (!inFlight) continue;
123
+ inFlight.requesterIds.delete(peerId);
124
+ if (inFlight.requesterIds.size === 0) {
125
+ this.clearQuery(hashKey);
126
+ }
127
+ }
128
+ }
129
+
130
+ this.hashesByRequester.delete(peerId);
131
+ this.peers.delete(peerId);
132
+ this.rateLimiter.resetPeer(peerId);
133
+ }
134
+
135
+ setUpstreamFetch(upstreamFetch?: (hash: Uint8Array) => Promise<Uint8Array | null>): void {
136
+ this.upstreamFetch = upstreamFetch;
137
+ }
138
+
139
+ setForwardRateLimit(config?: MeshForwardRateLimitConfig): void {
140
+ this.rateLimiter = this.createRateLimiter(config);
141
+ }
142
+
143
+ hasInFlight(hashKey: string): boolean {
144
+ return this.inFlightByHash.has(hashKey);
145
+ }
146
+
147
+ stop(): void {
148
+ for (const hashKey of Array.from(this.inFlightByHash.keys())) {
149
+ this.clearQuery(hashKey);
150
+ }
151
+ this.hashesByRequester.clear();
152
+ this.pendingUpstreamFetches.clear();
153
+ this.rateLimiter.clear();
154
+ }
155
+
156
+ private createRateLimiter(config?: MeshForwardRateLimitConfig): SlidingWindowRateLimiter {
157
+ return new SlidingWindowRateLimiter(
158
+ config?.maxForwardsPerPeerWindow ?? 64,
159
+ config?.windowMs ?? 1000,
160
+ );
161
+ }
162
+
163
+ async handleRequest(requesterId: string, req: DataRequest): Promise<void> {
164
+ const hashKey = hashToKey(req.h);
165
+ const requester = this.peers.get(requesterId);
166
+ if (!requester) {
167
+ return;
168
+ }
169
+
170
+ const local = await this.localStore.get(req.h);
171
+ if (local) {
172
+ await requester.sendResponse(req.h, local);
173
+ return;
174
+ }
175
+
176
+ const begin = this.beginQuery(hashKey, requesterId);
177
+ if (begin === 'suppressed') {
178
+ requester.onForwardedSuppressed?.();
179
+ return;
180
+ }
181
+ if (begin === 'rate_limited') {
182
+ return;
183
+ }
184
+
185
+ const peerQueryActive = this.startPeerQuery(hashKey, req.h, requesterId, req.htl ?? MAX_HTL);
186
+ const upstreamActive = this.startUpstreamFetch(hashKey, req.h);
187
+ const forwarded = peerQueryActive ? 1 : this.forwardRequest(requesterId, req.h, req.htl ?? MAX_HTL);
188
+ if (forwarded > 0 || upstreamActive) {
189
+ requester.onForwardedRequest?.();
190
+ return;
191
+ }
192
+
193
+ if (!upstreamActive) {
194
+ this.clearQuery(hashKey);
195
+ }
196
+ }
197
+
198
+ async resolve(hash: Uint8Array, data: Uint8Array): Promise<void> {
199
+ const hashKey = hashToKey(hash);
200
+ const requesterIds = this.clearQuery(hashKey);
201
+ if (requesterIds.length === 0) {
202
+ return;
203
+ }
204
+
205
+ await this.localStore.put(hash, data).catch(() => false);
206
+ for (const requesterId of requesterIds) {
207
+ const requester = this.peers.get(requesterId);
208
+ if (!requester) {
209
+ continue;
210
+ }
211
+ requester.onForwardedResolved?.();
212
+ await requester.sendResponse(hash, data);
213
+ }
214
+ }
215
+
216
+ private beginQuery(hashKey: string, requesterId: string): 'new' | 'suppressed' | 'rate_limited' {
217
+ const existing = this.inFlightByHash.get(hashKey);
218
+ if (existing) {
219
+ this.trackRequester(hashKey, existing.requesterIds, requesterId);
220
+ return 'suppressed';
221
+ }
222
+
223
+ if (!this.rateLimiter.allow(requesterId)) {
224
+ return 'rate_limited';
225
+ }
226
+
227
+ const requesterIds = new Set<string>();
228
+ this.trackRequester(hashKey, requesterIds, requesterId);
229
+ const timeoutId = setTimeout(() => {
230
+ this.clearQuery(hashKey);
231
+ }, this.requestTimeoutMs);
232
+
233
+ this.inFlightByHash.set(hashKey, { requesterIds, timeoutId });
234
+ return 'new';
235
+ }
236
+
237
+ private clearQuery(hashKey: string): string[] {
238
+ const inFlight = this.inFlightByHash.get(hashKey);
239
+ if (!inFlight) {
240
+ return [];
241
+ }
242
+
243
+ clearTimeout(inFlight.timeoutId);
244
+ this.inFlightByHash.delete(hashKey);
245
+
246
+ const requesterIds = Array.from(inFlight.requesterIds);
247
+ for (const requesterId of requesterIds) {
248
+ const hashes = this.hashesByRequester.get(requesterId);
249
+ if (!hashes) continue;
250
+ hashes.delete(hashKey);
251
+ if (hashes.size === 0) {
252
+ this.hashesByRequester.delete(requesterId);
253
+ }
254
+ }
255
+
256
+ return requesterIds;
257
+ }
258
+
259
+ private trackRequester(hashKey: string, requesterIds: Set<string>, requesterId: string): void {
260
+ requesterIds.add(requesterId);
261
+ let hashes = this.hashesByRequester.get(requesterId);
262
+ if (!hashes) {
263
+ hashes = new Set<string>();
264
+ this.hashesByRequester.set(requesterId, hashes);
265
+ }
266
+ hashes.add(hashKey);
267
+ }
268
+
269
+ private forwardRequest(requesterId: string, hash: Uint8Array, htl: number): number {
270
+ if (!shouldForward(htl)) {
271
+ return 0;
272
+ }
273
+
274
+ const requester = this.peers.get(requesterId);
275
+ if (!requester) {
276
+ return 0;
277
+ }
278
+
279
+ const nextHtl = decrementHTL(htl, requester.getHtlConfig());
280
+ let forwarded = 0;
281
+ for (const peer of this.peers.values()) {
282
+ if (peer.peerId === requesterId || !peer.canSend()) {
283
+ continue;
284
+ }
285
+
286
+ if (peer.sendRequest(hash, nextHtl)) {
287
+ forwarded += 1;
288
+ }
289
+ }
290
+ return forwarded;
291
+ }
292
+
293
+ private startPeerQuery(hashKey: string, hash: Uint8Array, requesterId: string, htl: number): boolean {
294
+ if (!this.queryPeers || !shouldForward(htl)) {
295
+ return false;
296
+ }
297
+
298
+ const requester = this.peers.get(requesterId);
299
+ if (!requester) {
300
+ return false;
301
+ }
302
+
303
+ const nextHtl = decrementHTL(htl, requester.getHtlConfig());
304
+ void this.queryPeers(hash, {
305
+ excludePeerId: requesterId,
306
+ htl: nextHtl,
307
+ }).then(async (data) => {
308
+ if (!data || !this.inFlightByHash.has(hashKey)) {
309
+ return;
310
+ }
311
+
312
+ const valid = await verifyHash(data, hash);
313
+ if (!valid) {
314
+ return;
315
+ }
316
+
317
+ await this.resolve(hash, data);
318
+ }).catch(() => undefined);
319
+ return true;
320
+ }
321
+
322
+ private startUpstreamFetch(hashKey: string, hash: Uint8Array): boolean {
323
+ if (!this.upstreamFetch) {
324
+ return false;
325
+ }
326
+
327
+ const existing = this.pendingUpstreamFetches.get(hashKey);
328
+ if (existing) {
329
+ return true;
330
+ }
331
+
332
+ let pending: Promise<Uint8Array | null>;
333
+ pending = this.upstreamFetch(hash)
334
+ .then(async (data) => {
335
+ if (!data) {
336
+ return null;
337
+ }
338
+
339
+ const valid = await verifyHash(data, hash);
340
+ if (!valid) {
341
+ return null;
342
+ }
343
+
344
+ await this.resolve(hash, data);
345
+ return data;
346
+ })
347
+ .catch(() => null)
348
+ .finally(() => {
349
+ if (this.pendingUpstreamFetches.get(hashKey) === pending) {
350
+ this.pendingUpstreamFetches.delete(hashKey);
351
+ }
352
+ });
353
+
354
+ this.pendingUpstreamFetches.set(hashKey, pending);
355
+ return true;
356
+ }
357
+ }
358
+
359
+ export function encodeForwardRequest(hash: Uint8Array, htl: number): Uint8Array {
360
+ return new Uint8Array(encodeRequest(createRequest(hash, htl)));
361
+ }
@@ -0,0 +1,11 @@
1
+ import type {
2
+ WebRTCCommand as CoreWebRTCCommand,
3
+ WebRTCEvent as CoreWebRTCEvent,
4
+ } from '@hashtree/core';
5
+
6
+ export type WebRTCCommand = CoreWebRTCCommand;
7
+
8
+ export type WebRTCEvent =
9
+ | CoreWebRTCEvent
10
+ | { type: 'rtc:bufferHigh'; peerId: string }
11
+ | { type: 'rtc:bufferLow'; peerId: string };
@@ -0,0 +1,197 @@
1
+ type TimeoutHandle = ReturnType<typeof setTimeout>;
2
+
3
+ export interface ForwardTimeoutEvent {
4
+ hashKey: string;
5
+ requesterIds: string[];
6
+ }
7
+
8
+ export type ForwardDecision =
9
+ | { kind: 'forward'; targets: string[] }
10
+ | { kind: 'suppressed' }
11
+ | { kind: 'rate_limited' }
12
+ | { kind: 'no_targets' };
13
+
14
+ export interface QueryForwardingMachineConfig {
15
+ requestTimeoutMs: number;
16
+ maxForwardsPerPeerWindow?: number;
17
+ forwardRateLimitWindowMs?: number;
18
+ now?: () => number;
19
+ scheduleTimeout?: (callback: () => void, delayMs: number) => TimeoutHandle;
20
+ clearScheduledTimeout?: (timeoutId: TimeoutHandle) => void;
21
+ onForwardTimeout?: (event: ForwardTimeoutEvent) => void;
22
+ }
23
+
24
+ interface InFlightForward {
25
+ requesters: Set<string>;
26
+ timeoutId: TimeoutHandle;
27
+ }
28
+
29
+ class SlidingWindowRateLimiter {
30
+ private readonly maxEvents: number;
31
+ private readonly windowMs: number;
32
+ private readonly now: () => number;
33
+ private readonly eventsByPeer = new Map<string, number[]>();
34
+
35
+ constructor(maxEvents: number, windowMs: number, now: () => number) {
36
+ this.maxEvents = maxEvents;
37
+ this.windowMs = windowMs;
38
+ this.now = now;
39
+ }
40
+
41
+ allow(peerId: string): boolean {
42
+ const now = this.now();
43
+ const events = this.eventsByPeer.get(peerId) ?? [];
44
+ let firstActiveIndex = 0;
45
+ while (firstActiveIndex < events.length && now - events[firstActiveIndex] >= this.windowMs) {
46
+ firstActiveIndex++;
47
+ }
48
+ if (firstActiveIndex > 0) {
49
+ events.splice(0, firstActiveIndex);
50
+ }
51
+
52
+ if (events.length >= this.maxEvents) {
53
+ this.eventsByPeer.set(peerId, events);
54
+ return false;
55
+ }
56
+
57
+ events.push(now);
58
+ this.eventsByPeer.set(peerId, events);
59
+ return true;
60
+ }
61
+
62
+ resetPeer(peerId: string): void {
63
+ this.eventsByPeer.delete(peerId);
64
+ }
65
+
66
+ clear(): void {
67
+ this.eventsByPeer.clear();
68
+ }
69
+ }
70
+
71
+ export class QueryForwardingMachine {
72
+ private readonly requestTimeoutMs: number;
73
+ private readonly scheduleTimeout: (callback: () => void, delayMs: number) => TimeoutHandle;
74
+ private readonly clearScheduledTimeout: (timeoutId: TimeoutHandle) => void;
75
+ private readonly onForwardTimeout?: (event: ForwardTimeoutEvent) => void;
76
+ private readonly hashesByRequester = new Map<string, Set<string>>();
77
+ private readonly inFlightByHash = new Map<string, InFlightForward>();
78
+ private readonly rateLimiter: SlidingWindowRateLimiter;
79
+
80
+ constructor(config: QueryForwardingMachineConfig) {
81
+ this.requestTimeoutMs = config.requestTimeoutMs;
82
+ this.scheduleTimeout = config.scheduleTimeout ?? ((callback, delayMs) => setTimeout(callback, delayMs));
83
+ this.clearScheduledTimeout = config.clearScheduledTimeout ?? ((timeoutId) => clearTimeout(timeoutId));
84
+ this.onForwardTimeout = config.onForwardTimeout;
85
+
86
+ const now = config.now ?? (() => Date.now());
87
+ const maxForwardsPerPeerWindow = config.maxForwardsPerPeerWindow ?? 64;
88
+ const forwardRateLimitWindowMs = config.forwardRateLimitWindowMs ?? 1000;
89
+ this.rateLimiter = new SlidingWindowRateLimiter(maxForwardsPerPeerWindow, forwardRateLimitWindowMs, now);
90
+ }
91
+
92
+ beginForward(hashKey: string, requesterId: string, candidateTargets: string[]): ForwardDecision {
93
+ const targets = candidateTargets.filter(target => target !== requesterId);
94
+ if (targets.length === 0) {
95
+ return { kind: 'no_targets' };
96
+ }
97
+
98
+ const existing = this.inFlightByHash.get(hashKey);
99
+ if (existing) {
100
+ this.trackRequester(hashKey, existing.requesters, requesterId);
101
+ return { kind: 'suppressed' };
102
+ }
103
+
104
+ if (!this.rateLimiter.allow(requesterId)) {
105
+ return { kind: 'rate_limited' };
106
+ }
107
+
108
+ const requesters = new Set<string>();
109
+ this.trackRequester(hashKey, requesters, requesterId);
110
+ const timeoutId = this.scheduleTimeout(() => {
111
+ this.handleForwardTimeout(hashKey);
112
+ }, this.requestTimeoutMs);
113
+
114
+ this.inFlightByHash.set(hashKey, { requesters, timeoutId });
115
+ return { kind: 'forward', targets };
116
+ }
117
+
118
+ resolveForward(hashKey: string): string[] {
119
+ return this.clearForward(hashKey, false);
120
+ }
121
+
122
+ cancelForward(hashKey: string): string[] {
123
+ return this.clearForward(hashKey, false);
124
+ }
125
+
126
+ removePeer(peerId: string): void {
127
+ const hashes = this.hashesByRequester.get(peerId);
128
+ if (hashes) {
129
+ for (const hashKey of Array.from(hashes)) {
130
+ const inFlight = this.inFlightByHash.get(hashKey);
131
+ if (!inFlight) continue;
132
+
133
+ inFlight.requesters.delete(peerId);
134
+ if (inFlight.requesters.size === 0) {
135
+ this.clearForward(hashKey, false);
136
+ }
137
+ }
138
+ }
139
+
140
+ this.hashesByRequester.delete(peerId);
141
+ this.rateLimiter.resetPeer(peerId);
142
+ }
143
+
144
+ stop(): void {
145
+ for (const hashKey of Array.from(this.inFlightByHash.keys())) {
146
+ this.clearForward(hashKey, false);
147
+ }
148
+ this.hashesByRequester.clear();
149
+ this.rateLimiter.clear();
150
+ }
151
+
152
+ isInFlight(hashKey: string): boolean {
153
+ return this.inFlightByHash.has(hashKey);
154
+ }
155
+
156
+ getInFlightCount(): number {
157
+ return this.inFlightByHash.size;
158
+ }
159
+
160
+ private handleForwardTimeout(hashKey: string): void {
161
+ this.clearForward(hashKey, true);
162
+ }
163
+
164
+ private clearForward(hashKey: string, notifyTimeout: boolean): string[] {
165
+ const inFlight = this.inFlightByHash.get(hashKey);
166
+ if (!inFlight) return [];
167
+
168
+ this.clearScheduledTimeout(inFlight.timeoutId);
169
+ this.inFlightByHash.delete(hashKey);
170
+
171
+ const requesterIds = Array.from(inFlight.requesters);
172
+ for (const requesterId of requesterIds) {
173
+ const hashes = this.hashesByRequester.get(requesterId);
174
+ if (!hashes) continue;
175
+ hashes.delete(hashKey);
176
+ if (hashes.size === 0) {
177
+ this.hashesByRequester.delete(requesterId);
178
+ }
179
+ }
180
+
181
+ if (notifyTimeout && this.onForwardTimeout) {
182
+ this.onForwardTimeout({ hashKey, requesterIds });
183
+ }
184
+
185
+ return requesterIds;
186
+ }
187
+
188
+ private trackRequester(hashKey: string, requesters: Set<string>, requesterId: string): void {
189
+ requesters.add(requesterId);
190
+ let hashes = this.hashesByRequester.get(requesterId);
191
+ if (!hashes) {
192
+ hashes = new Set<string>();
193
+ this.hashesByRequester.set(requesterId, hashes);
194
+ }
195
+ hashes.add(hashKey);
196
+ }
197
+ }