@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,426 @@
1
+ import type { Hash, Store } from '@hashtree/core';
2
+ import { toHex } from '@hashtree/core';
3
+ import {
4
+ buildHedgedWavePlan,
5
+ normalizeDispatchConfig,
6
+ type RequestDispatchConfig,
7
+ } from '@hashtree/nostr';
8
+
9
+ const DEFAULT_DISPATCH: RequestDispatchConfig = {
10
+ initialFanout: 1,
11
+ hedgeFanout: 1,
12
+ maxFanout: 4,
13
+ hedgeIntervalMs: 75,
14
+ };
15
+
16
+ const DEFAULT_REQUEST_TIMEOUT_MS = 5_500;
17
+ const INITIAL_BACKOFF_MS = 250;
18
+ const MAX_BACKOFF_MS = 10_000;
19
+ const SCORE_TIE_DELTA = 0.15;
20
+
21
+ export interface MeshReadSource {
22
+ id: string;
23
+ get(hash: Hash): Promise<Uint8Array | null>;
24
+ isAvailable?: () => boolean;
25
+ }
26
+
27
+ export interface MeshRouterGetOptions {
28
+ sourceIds?: readonly string[];
29
+ skipPrimary?: boolean;
30
+ }
31
+
32
+ export interface MeshRouterGetResult {
33
+ data: Uint8Array;
34
+ sourceId: string;
35
+ }
36
+
37
+ export interface MeshRouterStoreConfig {
38
+ primary: Store;
39
+ sources?: MeshReadSource[];
40
+ dispatch?: RequestDispatchConfig;
41
+ requestTimeoutMs?: number;
42
+ primarySourceId?: string;
43
+ }
44
+
45
+ interface SourceStats {
46
+ requests: number;
47
+ successes: number;
48
+ misses: number;
49
+ failures: number;
50
+ timeouts: number;
51
+ srttMs: number;
52
+ rttvarMs: number;
53
+ backoffLevel: number;
54
+ backedOffUntilMs?: number;
55
+ lastSuccessMs?: number;
56
+ lastFailureMs?: number;
57
+ }
58
+
59
+ interface InFlightSourceRequest {
60
+ source: MeshReadSource;
61
+ settled: boolean;
62
+ timeoutRecorded: boolean;
63
+ promise: Promise<{ sourceId: string; data: Uint8Array | null }>;
64
+ }
65
+
66
+ function defaultStats(): SourceStats {
67
+ return {
68
+ requests: 0,
69
+ successes: 0,
70
+ misses: 0,
71
+ failures: 0,
72
+ timeouts: 0,
73
+ srttMs: 0,
74
+ rttvarMs: 0,
75
+ backoffLevel: 0,
76
+ };
77
+ }
78
+
79
+ function reliabilityScore(stats: SourceStats): number {
80
+ return (stats.successes + 1) / (stats.requests + 2);
81
+ }
82
+
83
+ function latencyScore(stats: SourceStats): number {
84
+ if (stats.srttMs <= 0) return 0.5;
85
+ return Math.min(1, 500 / (stats.srttMs + 50));
86
+ }
87
+
88
+ function hasHistory(stats: SourceStats): boolean {
89
+ return stats.requests > 0 || stats.successes > 0 || stats.misses > 0 || stats.failures > 0 || stats.timeouts > 0;
90
+ }
91
+
92
+ function scoreSource(stats: SourceStats, now: number): number {
93
+ if (stats.backedOffUntilMs && stats.backedOffUntilMs > now) {
94
+ return Number.NEGATIVE_INFINITY;
95
+ }
96
+
97
+ const missPenalty = stats.requests > 0 ? (stats.misses / stats.requests) * 0.15 : 0;
98
+ const failurePenalty = stats.requests > 0 ? ((stats.failures + stats.timeouts) / stats.requests) * 0.3 : 0;
99
+ const recencyBonus =
100
+ stats.lastSuccessMs && now - stats.lastSuccessMs < 60_000
101
+ ? 0.1
102
+ : 0;
103
+
104
+ return (
105
+ 0.6 * reliabilityScore(stats) +
106
+ 0.3 * latencyScore(stats) +
107
+ recencyBonus -
108
+ missPenalty -
109
+ failurePenalty
110
+ );
111
+ }
112
+
113
+ export class MeshRouterStore implements Store {
114
+ private readonly primary: Store;
115
+ private readonly primarySourceId: string;
116
+ private readonly dispatch: RequestDispatchConfig;
117
+ private readonly requestTimeoutMs: number;
118
+ private readonly sources = new Map<string, MeshReadSource>();
119
+ private readonly statsBySource = new Map<string, SourceStats>();
120
+ private readonly inflightReads = new Map<string, Promise<MeshRouterGetResult | null>>();
121
+
122
+ constructor(config: MeshRouterStoreConfig) {
123
+ this.primary = config.primary;
124
+ this.primarySourceId = config.primarySourceId ?? 'primary';
125
+ this.dispatch = config.dispatch ?? DEFAULT_DISPATCH;
126
+ this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
127
+ this.setSources(config.sources ?? []);
128
+ }
129
+
130
+ setSources(sources: MeshReadSource[]): void {
131
+ this.sources.clear();
132
+ for (const source of sources) {
133
+ this.sources.set(source.id, source);
134
+ this.statsBySource.set(source.id, this.statsBySource.get(source.id) ?? defaultStats());
135
+ }
136
+ }
137
+
138
+ addSource(source: MeshReadSource): void {
139
+ this.sources.set(source.id, source);
140
+ this.statsBySource.set(source.id, this.statsBySource.get(source.id) ?? defaultStats());
141
+ }
142
+
143
+ removeSource(sourceId: string): void {
144
+ this.sources.delete(sourceId);
145
+ }
146
+
147
+ async getDetailed(hash: Hash, options: MeshRouterGetOptions = {}): Promise<MeshRouterGetResult | null> {
148
+ if (!options.skipPrimary) {
149
+ const local = await this.primary.get(hash);
150
+ if (local) {
151
+ return { data: local, sourceId: this.primarySourceId };
152
+ }
153
+ }
154
+
155
+ const pendingKey = this.pendingReadKey(hash, options);
156
+ let pending = this.inflightReads.get(pendingKey);
157
+ if (!pending) {
158
+ pending = this.loadFromSources(hash, options).finally(() => {
159
+ if (this.inflightReads.get(pendingKey) === pending) {
160
+ this.inflightReads.delete(pendingKey);
161
+ }
162
+ });
163
+ this.inflightReads.set(pendingKey, pending);
164
+ }
165
+
166
+ return pending;
167
+ }
168
+
169
+ getSourceStats(): Record<string, SourceStats> {
170
+ return Object.fromEntries(
171
+ Array.from(this.statsBySource.entries()).map(([sourceId, stats]) => [sourceId, { ...stats }]),
172
+ );
173
+ }
174
+
175
+ async put(hash: Hash, data: Uint8Array): Promise<boolean> {
176
+ return this.primary.put(hash, data);
177
+ }
178
+
179
+ async get(hash: Hash): Promise<Uint8Array | null> {
180
+ return (await this.getDetailed(hash))?.data ?? null;
181
+ }
182
+
183
+ async has(hash: Hash): Promise<boolean> {
184
+ return this.primary.has(hash);
185
+ }
186
+
187
+ async delete(hash: Hash): Promise<boolean> {
188
+ return this.primary.delete(hash);
189
+ }
190
+
191
+ private pendingReadKey(hash: Hash, options: MeshRouterGetOptions): string {
192
+ const sourceKey = options.sourceIds && options.sourceIds.length > 0
193
+ ? [...options.sourceIds].sort().join(',')
194
+ : '*';
195
+ return `${toHex(hash)}:${options.skipPrimary === true ? 'skip-primary' : 'with-primary'}:${sourceKey}`;
196
+ }
197
+
198
+ private getCandidateSources(sourceIds?: readonly string[]): MeshReadSource[] {
199
+ const requested = sourceIds && sourceIds.length > 0
200
+ ? new Set(sourceIds)
201
+ : null;
202
+ const available = Array.from(this.sources.values()).filter((source) => {
203
+ if (requested && !requested.has(source.id)) return false;
204
+ return source.isAvailable ? source.isAvailable() : true;
205
+ });
206
+ if (available.length === 0) return [];
207
+
208
+ const now = Date.now();
209
+ const healthy = available.filter((source) => {
210
+ const stats = this.statsBySource.get(source.id) ?? defaultStats();
211
+ return !stats.backedOffUntilMs || stats.backedOffUntilMs <= now;
212
+ });
213
+
214
+ return healthy.length > 0 ? healthy : available;
215
+ }
216
+
217
+ private orderedSources(sourceIds?: readonly string[]): MeshReadSource[] {
218
+ const now = Date.now();
219
+ const candidates = this.getCandidateSources(sourceIds);
220
+ return candidates.sort((left, right) => {
221
+ const leftStats = this.statsBySource.get(left.id) ?? defaultStats();
222
+ const rightStats = this.statsBySource.get(right.id) ?? defaultStats();
223
+ const scoreDiff = scoreSource(rightStats, now) - scoreSource(leftStats, now);
224
+ if (scoreDiff !== 0) return scoreDiff;
225
+ return left.id.localeCompare(right.id);
226
+ });
227
+ }
228
+
229
+ private shouldProbeMultipleSources(orderedSources: MeshReadSource[]): boolean {
230
+ if (orderedSources.length <= 1) return false;
231
+
232
+ const [best, secondBest] = orderedSources;
233
+ const bestStats = this.statsBySource.get(best.id) ?? defaultStats();
234
+ const secondStats = this.statsBySource.get(secondBest.id) ?? defaultStats();
235
+ if (!hasHistory(bestStats) || !hasHistory(secondStats)) {
236
+ return true;
237
+ }
238
+
239
+ const now = Date.now();
240
+ const diff = scoreSource(bestStats, now) - scoreSource(secondStats, now);
241
+ return diff < SCORE_TIE_DELTA;
242
+ }
243
+
244
+ private dispatchFor(sourceCount: number, orderedSources: MeshReadSource[]): RequestDispatchConfig {
245
+ const probeMultiple = this.shouldProbeMultipleSources(orderedSources);
246
+ const initialFanout = probeMultiple
247
+ ? Math.min(sourceCount, 2)
248
+ : 1;
249
+ return {
250
+ initialFanout,
251
+ hedgeFanout: this.dispatch.hedgeFanout,
252
+ maxFanout: Math.min(this.dispatch.maxFanout, sourceCount),
253
+ hedgeIntervalMs: this.dispatch.hedgeIntervalMs,
254
+ };
255
+ }
256
+
257
+ private createInFlightSourceRequest(source: MeshReadSource, hash: Hash): InFlightSourceRequest {
258
+ const startedAt = Date.now();
259
+ this.recordRequest(source.id);
260
+
261
+ const task: InFlightSourceRequest = {
262
+ source,
263
+ settled: false,
264
+ timeoutRecorded: false,
265
+ promise: Promise.resolve({ sourceId: source.id, data: null }),
266
+ };
267
+
268
+ task.promise = source.get(hash)
269
+ .then(async (data) => {
270
+ const elapsedMs = Math.max(1, Date.now() - startedAt);
271
+ if (data) {
272
+ this.recordSuccess(source.id, elapsedMs);
273
+ await this.primary.put(hash, data).catch(() => false);
274
+ return { sourceId: source.id, data };
275
+ }
276
+
277
+ if (!task.timeoutRecorded) {
278
+ this.recordMiss(source.id);
279
+ }
280
+ return { sourceId: source.id, data: null };
281
+ })
282
+ .catch(() => {
283
+ if (!task.timeoutRecorded) {
284
+ this.recordFailure(source.id);
285
+ }
286
+ return { sourceId: source.id, data: null };
287
+ });
288
+
289
+ return task;
290
+ }
291
+
292
+ private async waitForNextResult(
293
+ inFlight: InFlightSourceRequest[],
294
+ waitMs: number,
295
+ ): Promise<{ task: InFlightSourceRequest; sourceId: string; data: Uint8Array | null } | null> {
296
+ const active = inFlight.filter((task) => !task.settled);
297
+ if (active.length === 0 || waitMs <= 0) return null;
298
+
299
+ const timeout = new Promise<null>((resolve) => {
300
+ setTimeout(() => resolve(null), waitMs);
301
+ });
302
+ const result = await Promise.race([
303
+ timeout,
304
+ ...active.map((task) => task.promise.then((value) => ({ task, ...value }))),
305
+ ]);
306
+ if (!result) return null;
307
+
308
+ result.task.settled = true;
309
+ return result;
310
+ }
311
+
312
+ private async loadFromSources(hash: Hash, options: MeshRouterGetOptions): Promise<MeshRouterGetResult | null> {
313
+ const orderedSources = this.orderedSources(options.sourceIds);
314
+ if (orderedSources.length === 0) {
315
+ return null;
316
+ }
317
+
318
+ const dispatch = normalizeDispatchConfig(
319
+ this.dispatchFor(orderedSources.length, orderedSources),
320
+ orderedSources.length,
321
+ );
322
+ const wavePlan = buildHedgedWavePlan(orderedSources.length, dispatch);
323
+ if (wavePlan.length === 0) return null;
324
+
325
+ const deadline = Date.now() + this.requestTimeoutMs;
326
+ const inFlight: InFlightSourceRequest[] = [];
327
+ let nextSourceIdx = 0;
328
+
329
+ for (let waveIdx = 0; waveIdx < wavePlan.length; waveIdx++) {
330
+ const waveSize = wavePlan[waveIdx];
331
+ const from = nextSourceIdx;
332
+ const to = Math.min(from + waveSize, orderedSources.length);
333
+ nextSourceIdx = to;
334
+
335
+ for (const source of orderedSources.slice(from, to)) {
336
+ inFlight.push(this.createInFlightSourceRequest(source, hash));
337
+ }
338
+
339
+ const isLastWave = waveIdx === wavePlan.length - 1 || nextSourceIdx >= orderedSources.length;
340
+ const windowEnd = isLastWave
341
+ ? deadline
342
+ : Math.min(deadline, Date.now() + dispatch.hedgeIntervalMs);
343
+
344
+ while (Date.now() < windowEnd) {
345
+ const remaining = windowEnd - Date.now();
346
+ const result = await this.waitForNextResult(inFlight, remaining);
347
+ if (!result) break;
348
+ if (result.data) {
349
+ return {
350
+ data: result.data,
351
+ sourceId: result.sourceId,
352
+ };
353
+ }
354
+ }
355
+
356
+ if (Date.now() >= deadline) {
357
+ break;
358
+ }
359
+ }
360
+
361
+ for (const task of inFlight) {
362
+ if (task.settled) continue;
363
+ task.timeoutRecorded = true;
364
+ this.recordTimeout(task.source.id);
365
+ }
366
+
367
+ return null;
368
+ }
369
+
370
+ private statsFor(sourceId: string): SourceStats {
371
+ const stats = this.statsBySource.get(sourceId);
372
+ if (stats) return stats;
373
+ const created = defaultStats();
374
+ this.statsBySource.set(sourceId, created);
375
+ return created;
376
+ }
377
+
378
+ private recordRequest(sourceId: string): void {
379
+ this.statsFor(sourceId).requests += 1;
380
+ }
381
+
382
+ private recordMiss(sourceId: string): void {
383
+ this.statsFor(sourceId).misses += 1;
384
+ }
385
+
386
+ private recordSuccess(sourceId: string, elapsedMs: number): void {
387
+ const stats = this.statsFor(sourceId);
388
+ const now = Date.now();
389
+ stats.successes += 1;
390
+ stats.lastSuccessMs = now;
391
+ stats.backoffLevel = 0;
392
+ stats.backedOffUntilMs = undefined;
393
+
394
+ if (stats.srttMs === 0) {
395
+ stats.srttMs = elapsedMs;
396
+ stats.rttvarMs = elapsedMs / 2;
397
+ return;
398
+ }
399
+
400
+ stats.rttvarMs = 0.75 * stats.rttvarMs + 0.25 * Math.abs(stats.srttMs - elapsedMs);
401
+ stats.srttMs = 0.875 * stats.srttMs + 0.125 * elapsedMs;
402
+ }
403
+
404
+ private recordFailure(sourceId: string): void {
405
+ const stats = this.statsFor(sourceId);
406
+ stats.failures += 1;
407
+ stats.lastFailureMs = Date.now();
408
+ this.applyBackoff(stats);
409
+ }
410
+
411
+ private recordTimeout(sourceId: string): void {
412
+ const stats = this.statsFor(sourceId);
413
+ stats.timeouts += 1;
414
+ stats.lastFailureMs = Date.now();
415
+ this.applyBackoff(stats);
416
+ }
417
+
418
+ private applyBackoff(stats: SourceStats): void {
419
+ stats.backoffLevel += 1;
420
+ const backoffMs = Math.min(
421
+ MAX_BACKOFF_MS,
422
+ INITIAL_BACKOFF_MS * (2 ** Math.max(0, stats.backoffLevel - 1)),
423
+ );
424
+ stats.backedOffUntilMs = Date.now() + backoffMs;
425
+ }
426
+ }