@hashtree/worker 0.2.0 → 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 (259) hide show
  1. package/package.json +7 -3
  2. package/src/app-runtime.ts +393 -0
  3. package/src/capabilities/blossomBandwidthTracker.ts +74 -0
  4. package/src/capabilities/blossomTransport.ts +179 -0
  5. package/src/capabilities/connectivity.ts +54 -0
  6. package/src/capabilities/idbStorage.ts +94 -0
  7. package/src/capabilities/meshRouterStore.ts +426 -0
  8. package/src/capabilities/rootResolver.ts +497 -0
  9. package/src/client-id.ts +137 -0
  10. package/src/client.ts +501 -0
  11. package/{dist/entry.js → src/entry.ts} +1 -1
  12. package/src/htree-path.ts +53 -0
  13. package/src/htree-url.ts +156 -0
  14. package/src/index.ts +76 -0
  15. package/src/mediaStreaming.ts +64 -0
  16. package/src/p2p/boundedQueue.ts +168 -0
  17. package/src/p2p/errorMessage.ts +6 -0
  18. package/src/p2p/index.ts +48 -0
  19. package/src/p2p/lruCache.ts +78 -0
  20. package/src/p2p/meshQueryRouter.ts +361 -0
  21. package/src/p2p/protocol.ts +11 -0
  22. package/src/p2p/queryForwardingMachine.ts +197 -0
  23. package/src/p2p/signaling.ts +284 -0
  24. package/src/p2p/uploadRateLimiter.ts +85 -0
  25. package/src/p2p/webrtcController.ts +1168 -0
  26. package/src/p2p/webrtcProxy.ts +519 -0
  27. package/src/privacyGuards.ts +31 -0
  28. package/src/protocol.ts +124 -0
  29. package/src/relay/identity.ts +86 -0
  30. package/src/relay/mediaHandler.ts +1633 -0
  31. package/src/relay/ndk.ts +590 -0
  32. package/{dist/relay/nostr-wasm.js → src/relay/nostr-wasm.ts} +4 -1
  33. package/src/relay/nostr.ts +249 -0
  34. package/src/relay/protocol.ts +361 -0
  35. package/src/relay/publicAssetUrl.ts +25 -0
  36. package/src/relay/rootPathResolver.ts +50 -0
  37. package/src/relay/shims.d.ts +17 -0
  38. package/src/relay/signing.ts +332 -0
  39. package/src/relay/treeRootCache.ts +354 -0
  40. package/src/relay/treeRootSubscription.ts +577 -0
  41. package/src/relay/utils/constants.ts +139 -0
  42. package/src/relay/utils/errorMessage.ts +7 -0
  43. package/src/relay/utils/lruCache.ts +79 -0
  44. package/src/relay/webrtc.ts +5 -0
  45. package/src/relay/webrtcSignaling.ts +108 -0
  46. package/src/relay/worker.ts +1787 -0
  47. package/src/relay-client.ts +265 -0
  48. package/src/relay-entry.ts +1 -0
  49. package/src/runtime-network.ts +134 -0
  50. package/src/runtime.ts +153 -0
  51. package/{dist/transferableBytes.js → src/transferableBytes.ts} +2 -3
  52. package/src/tree-root.ts +851 -0
  53. package/src/types.ts +8 -0
  54. package/src/worker.ts +975 -0
  55. package/dist/app-runtime.d.ts +0 -60
  56. package/dist/app-runtime.d.ts.map +0 -1
  57. package/dist/app-runtime.js +0 -271
  58. package/dist/app-runtime.js.map +0 -1
  59. package/dist/capabilities/blossomBandwidthTracker.d.ts +0 -26
  60. package/dist/capabilities/blossomBandwidthTracker.d.ts.map +0 -1
  61. package/dist/capabilities/blossomBandwidthTracker.js +0 -53
  62. package/dist/capabilities/blossomBandwidthTracker.js.map +0 -1
  63. package/dist/capabilities/blossomTransport.d.ts +0 -22
  64. package/dist/capabilities/blossomTransport.d.ts.map +0 -1
  65. package/dist/capabilities/blossomTransport.js +0 -139
  66. package/dist/capabilities/blossomTransport.js.map +0 -1
  67. package/dist/capabilities/connectivity.d.ts +0 -3
  68. package/dist/capabilities/connectivity.d.ts.map +0 -1
  69. package/dist/capabilities/connectivity.js +0 -49
  70. package/dist/capabilities/connectivity.js.map +0 -1
  71. package/dist/capabilities/idbStorage.d.ts +0 -25
  72. package/dist/capabilities/idbStorage.d.ts.map +0 -1
  73. package/dist/capabilities/idbStorage.js +0 -73
  74. package/dist/capabilities/idbStorage.js.map +0 -1
  75. package/dist/capabilities/meshRouterStore.d.ts +0 -71
  76. package/dist/capabilities/meshRouterStore.d.ts.map +0 -1
  77. package/dist/capabilities/meshRouterStore.js +0 -316
  78. package/dist/capabilities/meshRouterStore.js.map +0 -1
  79. package/dist/capabilities/rootResolver.d.ts +0 -10
  80. package/dist/capabilities/rootResolver.d.ts.map +0 -1
  81. package/dist/capabilities/rootResolver.js +0 -392
  82. package/dist/capabilities/rootResolver.js.map +0 -1
  83. package/dist/client-id.d.ts +0 -18
  84. package/dist/client-id.d.ts.map +0 -1
  85. package/dist/client-id.js +0 -98
  86. package/dist/client-id.js.map +0 -1
  87. package/dist/client.d.ts +0 -61
  88. package/dist/client.d.ts.map +0 -1
  89. package/dist/client.js +0 -417
  90. package/dist/client.js.map +0 -1
  91. package/dist/entry.d.ts +0 -2
  92. package/dist/entry.d.ts.map +0 -1
  93. package/dist/entry.js.map +0 -1
  94. package/dist/htree-path.d.ts +0 -13
  95. package/dist/htree-path.d.ts.map +0 -1
  96. package/dist/htree-path.js +0 -38
  97. package/dist/htree-path.js.map +0 -1
  98. package/dist/htree-url.d.ts +0 -22
  99. package/dist/htree-url.d.ts.map +0 -1
  100. package/dist/htree-url.js +0 -118
  101. package/dist/htree-url.js.map +0 -1
  102. package/dist/index.d.ts +0 -17
  103. package/dist/index.d.ts.map +0 -1
  104. package/dist/index.js +0 -8
  105. package/dist/index.js.map +0 -1
  106. package/dist/mediaStreaming.d.ts +0 -7
  107. package/dist/mediaStreaming.d.ts.map +0 -1
  108. package/dist/mediaStreaming.js +0 -48
  109. package/dist/mediaStreaming.js.map +0 -1
  110. package/dist/p2p/boundedQueue.d.ts +0 -79
  111. package/dist/p2p/boundedQueue.d.ts.map +0 -1
  112. package/dist/p2p/boundedQueue.js +0 -134
  113. package/dist/p2p/boundedQueue.js.map +0 -1
  114. package/dist/p2p/errorMessage.d.ts +0 -5
  115. package/dist/p2p/errorMessage.d.ts.map +0 -1
  116. package/dist/p2p/errorMessage.js +0 -7
  117. package/dist/p2p/errorMessage.js.map +0 -1
  118. package/dist/p2p/index.d.ts +0 -8
  119. package/dist/p2p/index.d.ts.map +0 -1
  120. package/dist/p2p/index.js +0 -6
  121. package/dist/p2p/index.js.map +0 -1
  122. package/dist/p2p/lruCache.d.ts +0 -26
  123. package/dist/p2p/lruCache.d.ts.map +0 -1
  124. package/dist/p2p/lruCache.js +0 -65
  125. package/dist/p2p/lruCache.js.map +0 -1
  126. package/dist/p2p/meshQueryRouter.d.ts +0 -57
  127. package/dist/p2p/meshQueryRouter.d.ts.map +0 -1
  128. package/dist/p2p/meshQueryRouter.js +0 -264
  129. package/dist/p2p/meshQueryRouter.js.map +0 -1
  130. package/dist/p2p/protocol.d.ts +0 -10
  131. package/dist/p2p/protocol.d.ts.map +0 -1
  132. package/dist/p2p/protocol.js +0 -2
  133. package/dist/p2p/protocol.js.map +0 -1
  134. package/dist/p2p/queryForwardingMachine.d.ts +0 -46
  135. package/dist/p2p/queryForwardingMachine.d.ts.map +0 -1
  136. package/dist/p2p/queryForwardingMachine.js +0 -144
  137. package/dist/p2p/queryForwardingMachine.js.map +0 -1
  138. package/dist/p2p/signaling.d.ts +0 -63
  139. package/dist/p2p/signaling.d.ts.map +0 -1
  140. package/dist/p2p/signaling.js +0 -185
  141. package/dist/p2p/signaling.js.map +0 -1
  142. package/dist/p2p/uploadRateLimiter.d.ts +0 -21
  143. package/dist/p2p/uploadRateLimiter.d.ts.map +0 -1
  144. package/dist/p2p/uploadRateLimiter.js +0 -62
  145. package/dist/p2p/uploadRateLimiter.js.map +0 -1
  146. package/dist/p2p/webrtcController.d.ts +0 -176
  147. package/dist/p2p/webrtcController.d.ts.map +0 -1
  148. package/dist/p2p/webrtcController.js +0 -938
  149. package/dist/p2p/webrtcController.js.map +0 -1
  150. package/dist/p2p/webrtcProxy.d.ts +0 -62
  151. package/dist/p2p/webrtcProxy.d.ts.map +0 -1
  152. package/dist/p2p/webrtcProxy.js +0 -447
  153. package/dist/p2p/webrtcProxy.js.map +0 -1
  154. package/dist/privacyGuards.d.ts +0 -14
  155. package/dist/privacyGuards.d.ts.map +0 -1
  156. package/dist/privacyGuards.js +0 -27
  157. package/dist/privacyGuards.js.map +0 -1
  158. package/dist/protocol.d.ts +0 -225
  159. package/dist/protocol.d.ts.map +0 -1
  160. package/dist/protocol.js +0 -2
  161. package/dist/protocol.js.map +0 -1
  162. package/dist/relay/identity.d.ts +0 -36
  163. package/dist/relay/identity.d.ts.map +0 -1
  164. package/dist/relay/identity.js +0 -78
  165. package/dist/relay/identity.js.map +0 -1
  166. package/dist/relay/mediaHandler.d.ts +0 -64
  167. package/dist/relay/mediaHandler.d.ts.map +0 -1
  168. package/dist/relay/mediaHandler.js +0 -1285
  169. package/dist/relay/mediaHandler.js.map +0 -1
  170. package/dist/relay/ndk.d.ts +0 -96
  171. package/dist/relay/ndk.d.ts.map +0 -1
  172. package/dist/relay/ndk.js +0 -502
  173. package/dist/relay/ndk.js.map +0 -1
  174. package/dist/relay/nostr-wasm.d.ts +0 -14
  175. package/dist/relay/nostr-wasm.d.ts.map +0 -1
  176. package/dist/relay/nostr-wasm.js.map +0 -1
  177. package/dist/relay/nostr.d.ts +0 -60
  178. package/dist/relay/nostr.d.ts.map +0 -1
  179. package/dist/relay/nostr.js +0 -207
  180. package/dist/relay/nostr.js.map +0 -1
  181. package/dist/relay/protocol.d.ts +0 -592
  182. package/dist/relay/protocol.d.ts.map +0 -1
  183. package/dist/relay/protocol.js +0 -16
  184. package/dist/relay/protocol.js.map +0 -1
  185. package/dist/relay/publicAssetUrl.d.ts +0 -6
  186. package/dist/relay/publicAssetUrl.d.ts.map +0 -1
  187. package/dist/relay/publicAssetUrl.js +0 -14
  188. package/dist/relay/publicAssetUrl.js.map +0 -1
  189. package/dist/relay/rootPathResolver.d.ts +0 -9
  190. package/dist/relay/rootPathResolver.d.ts.map +0 -1
  191. package/dist/relay/rootPathResolver.js +0 -32
  192. package/dist/relay/rootPathResolver.js.map +0 -1
  193. package/dist/relay/signing.d.ts +0 -50
  194. package/dist/relay/signing.d.ts.map +0 -1
  195. package/dist/relay/signing.js +0 -299
  196. package/dist/relay/signing.js.map +0 -1
  197. package/dist/relay/treeRootCache.d.ts +0 -86
  198. package/dist/relay/treeRootCache.d.ts.map +0 -1
  199. package/dist/relay/treeRootCache.js +0 -269
  200. package/dist/relay/treeRootCache.js.map +0 -1
  201. package/dist/relay/treeRootSubscription.d.ts +0 -55
  202. package/dist/relay/treeRootSubscription.d.ts.map +0 -1
  203. package/dist/relay/treeRootSubscription.js +0 -478
  204. package/dist/relay/treeRootSubscription.js.map +0 -1
  205. package/dist/relay/utils/constants.d.ts +0 -76
  206. package/dist/relay/utils/constants.d.ts.map +0 -1
  207. package/dist/relay/utils/constants.js +0 -113
  208. package/dist/relay/utils/constants.js.map +0 -1
  209. package/dist/relay/utils/errorMessage.d.ts +0 -5
  210. package/dist/relay/utils/errorMessage.d.ts.map +0 -1
  211. package/dist/relay/utils/errorMessage.js +0 -8
  212. package/dist/relay/utils/errorMessage.js.map +0 -1
  213. package/dist/relay/utils/lruCache.d.ts +0 -26
  214. package/dist/relay/utils/lruCache.d.ts.map +0 -1
  215. package/dist/relay/utils/lruCache.js +0 -66
  216. package/dist/relay/utils/lruCache.js.map +0 -1
  217. package/dist/relay/webrtc.d.ts +0 -2
  218. package/dist/relay/webrtc.d.ts.map +0 -1
  219. package/dist/relay/webrtc.js +0 -3
  220. package/dist/relay/webrtc.js.map +0 -1
  221. package/dist/relay/webrtcSignaling.d.ts +0 -37
  222. package/dist/relay/webrtcSignaling.d.ts.map +0 -1
  223. package/dist/relay/webrtcSignaling.js +0 -86
  224. package/dist/relay/webrtcSignaling.js.map +0 -1
  225. package/dist/relay/worker.d.ts +0 -12
  226. package/dist/relay/worker.d.ts.map +0 -1
  227. package/dist/relay/worker.js +0 -1540
  228. package/dist/relay/worker.js.map +0 -1
  229. package/dist/relay-client.d.ts +0 -31
  230. package/dist/relay-client.d.ts.map +0 -1
  231. package/dist/relay-client.js +0 -197
  232. package/dist/relay-client.js.map +0 -1
  233. package/dist/relay-entry.d.ts +0 -2
  234. package/dist/relay-entry.d.ts.map +0 -1
  235. package/dist/relay-entry.js +0 -2
  236. package/dist/relay-entry.js.map +0 -1
  237. package/dist/runtime-network.d.ts +0 -23
  238. package/dist/runtime-network.d.ts.map +0 -1
  239. package/dist/runtime-network.js +0 -105
  240. package/dist/runtime-network.js.map +0 -1
  241. package/dist/runtime.d.ts +0 -24
  242. package/dist/runtime.d.ts.map +0 -1
  243. package/dist/runtime.js +0 -126
  244. package/dist/runtime.js.map +0 -1
  245. package/dist/transferableBytes.d.ts +0 -2
  246. package/dist/transferableBytes.d.ts.map +0 -1
  247. package/dist/transferableBytes.js.map +0 -1
  248. package/dist/tree-root.d.ts +0 -201
  249. package/dist/tree-root.d.ts.map +0 -1
  250. package/dist/tree-root.js +0 -632
  251. package/dist/tree-root.js.map +0 -1
  252. package/dist/types.d.ts +0 -2
  253. package/dist/types.d.ts.map +0 -1
  254. package/dist/types.js +0 -2
  255. package/dist/types.js.map +0 -1
  256. package/dist/worker.d.ts +0 -9
  257. package/dist/worker.d.ts.map +0 -1
  258. package/dist/worker.js +0 -792
  259. 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
+ }