@hashtree/worker 0.1.26 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +16 -16
  2. package/package.json +23 -19
  3. package/src/app-runtime.ts +393 -0
  4. package/src/capabilities/blossomBandwidthTracker.ts +74 -0
  5. package/src/capabilities/blossomTransport.ts +179 -0
  6. package/src/capabilities/connectivity.ts +54 -0
  7. package/src/capabilities/idbStorage.ts +94 -0
  8. package/src/capabilities/meshRouterStore.ts +426 -0
  9. package/src/capabilities/rootResolver.ts +497 -0
  10. package/src/client-id.ts +137 -0
  11. package/src/client.ts +501 -0
  12. package/{dist/entry.js → src/entry.ts} +1 -1
  13. package/src/htree-path.ts +53 -0
  14. package/src/htree-url.ts +156 -0
  15. package/src/index.ts +76 -0
  16. package/src/mediaStreaming.ts +64 -0
  17. package/src/p2p/boundedQueue.ts +168 -0
  18. package/src/p2p/errorMessage.ts +6 -0
  19. package/src/p2p/index.ts +48 -0
  20. package/src/p2p/lruCache.ts +78 -0
  21. package/src/p2p/meshQueryRouter.ts +361 -0
  22. package/src/p2p/protocol.ts +11 -0
  23. package/src/p2p/queryForwardingMachine.ts +197 -0
  24. package/src/p2p/signaling.ts +284 -0
  25. package/src/p2p/uploadRateLimiter.ts +85 -0
  26. package/src/p2p/webrtcController.ts +1168 -0
  27. package/src/p2p/webrtcProxy.ts +519 -0
  28. package/src/privacyGuards.ts +31 -0
  29. package/src/protocol.ts +124 -0
  30. package/src/relay/identity.ts +86 -0
  31. package/src/relay/mediaHandler.ts +1633 -0
  32. package/src/relay/ndk.ts +590 -0
  33. package/{dist/iris/nostr-wasm.js → src/relay/nostr-wasm.ts} +4 -1
  34. package/src/relay/nostr.ts +249 -0
  35. package/src/relay/protocol.ts +361 -0
  36. package/src/relay/publicAssetUrl.ts +25 -0
  37. package/src/relay/rootPathResolver.ts +50 -0
  38. package/src/relay/shims.d.ts +17 -0
  39. package/src/relay/signing.ts +332 -0
  40. package/src/relay/treeRootCache.ts +354 -0
  41. package/src/relay/treeRootSubscription.ts +577 -0
  42. package/src/relay/utils/constants.ts +139 -0
  43. package/src/relay/utils/errorMessage.ts +7 -0
  44. package/src/relay/utils/lruCache.ts +79 -0
  45. package/src/relay/webrtc.ts +5 -0
  46. package/src/relay/webrtcSignaling.ts +108 -0
  47. package/src/relay/worker.ts +1787 -0
  48. package/src/relay-client.ts +265 -0
  49. package/src/relay-entry.ts +1 -0
  50. package/src/runtime-network.ts +134 -0
  51. package/src/runtime.ts +153 -0
  52. package/src/transferableBytes.ts +5 -0
  53. package/src/tree-root.ts +851 -0
  54. package/src/types.ts +8 -0
  55. package/src/worker.ts +975 -0
  56. package/LICENSE +0 -21
  57. package/dist/app-runtime.d.ts +0 -60
  58. package/dist/app-runtime.d.ts.map +0 -1
  59. package/dist/app-runtime.js +0 -271
  60. package/dist/app-runtime.js.map +0 -1
  61. package/dist/capabilities/blossomBandwidthTracker.d.ts +0 -26
  62. package/dist/capabilities/blossomBandwidthTracker.d.ts.map +0 -1
  63. package/dist/capabilities/blossomBandwidthTracker.js +0 -53
  64. package/dist/capabilities/blossomBandwidthTracker.js.map +0 -1
  65. package/dist/capabilities/blossomTransport.d.ts +0 -22
  66. package/dist/capabilities/blossomTransport.d.ts.map +0 -1
  67. package/dist/capabilities/blossomTransport.js +0 -144
  68. package/dist/capabilities/blossomTransport.js.map +0 -1
  69. package/dist/capabilities/connectivity.d.ts +0 -3
  70. package/dist/capabilities/connectivity.d.ts.map +0 -1
  71. package/dist/capabilities/connectivity.js +0 -49
  72. package/dist/capabilities/connectivity.js.map +0 -1
  73. package/dist/capabilities/idbStorage.d.ts +0 -25
  74. package/dist/capabilities/idbStorage.d.ts.map +0 -1
  75. package/dist/capabilities/idbStorage.js +0 -73
  76. package/dist/capabilities/idbStorage.js.map +0 -1
  77. package/dist/capabilities/meshRouterStore.d.ts +0 -71
  78. package/dist/capabilities/meshRouterStore.d.ts.map +0 -1
  79. package/dist/capabilities/meshRouterStore.js +0 -316
  80. package/dist/capabilities/meshRouterStore.js.map +0 -1
  81. package/dist/capabilities/rootResolver.d.ts +0 -10
  82. package/dist/capabilities/rootResolver.d.ts.map +0 -1
  83. package/dist/capabilities/rootResolver.js +0 -393
  84. package/dist/capabilities/rootResolver.js.map +0 -1
  85. package/dist/client-id.d.ts +0 -18
  86. package/dist/client-id.d.ts.map +0 -1
  87. package/dist/client-id.js +0 -98
  88. package/dist/client-id.js.map +0 -1
  89. package/dist/client.d.ts +0 -61
  90. package/dist/client.d.ts.map +0 -1
  91. package/dist/client.js +0 -417
  92. package/dist/client.js.map +0 -1
  93. package/dist/entry.d.ts +0 -2
  94. package/dist/entry.d.ts.map +0 -1
  95. package/dist/entry.js.map +0 -1
  96. package/dist/htree-path.d.ts +0 -13
  97. package/dist/htree-path.d.ts.map +0 -1
  98. package/dist/htree-path.js +0 -38
  99. package/dist/htree-path.js.map +0 -1
  100. package/dist/htree-url.d.ts +0 -22
  101. package/dist/htree-url.d.ts.map +0 -1
  102. package/dist/htree-url.js +0 -118
  103. package/dist/htree-url.js.map +0 -1
  104. package/dist/index.d.ts +0 -17
  105. package/dist/index.d.ts.map +0 -1
  106. package/dist/index.js +0 -8
  107. package/dist/index.js.map +0 -1
  108. package/dist/iris/identity.d.ts +0 -36
  109. package/dist/iris/identity.d.ts.map +0 -1
  110. package/dist/iris/identity.js +0 -78
  111. package/dist/iris/identity.js.map +0 -1
  112. package/dist/iris/mediaHandler.d.ts +0 -64
  113. package/dist/iris/mediaHandler.d.ts.map +0 -1
  114. package/dist/iris/mediaHandler.js +0 -1285
  115. package/dist/iris/mediaHandler.js.map +0 -1
  116. package/dist/iris/ndk.d.ts +0 -96
  117. package/dist/iris/ndk.d.ts.map +0 -1
  118. package/dist/iris/ndk.js +0 -502
  119. package/dist/iris/ndk.js.map +0 -1
  120. package/dist/iris/nostr-wasm.d.ts +0 -14
  121. package/dist/iris/nostr-wasm.d.ts.map +0 -1
  122. package/dist/iris/nostr-wasm.js.map +0 -1
  123. package/dist/iris/nostr.d.ts +0 -60
  124. package/dist/iris/nostr.d.ts.map +0 -1
  125. package/dist/iris/nostr.js +0 -207
  126. package/dist/iris/nostr.js.map +0 -1
  127. package/dist/iris/protocol.d.ts +0 -583
  128. package/dist/iris/protocol.d.ts.map +0 -1
  129. package/dist/iris/protocol.js +0 -16
  130. package/dist/iris/protocol.js.map +0 -1
  131. package/dist/iris/publicAssetUrl.d.ts +0 -6
  132. package/dist/iris/publicAssetUrl.d.ts.map +0 -1
  133. package/dist/iris/publicAssetUrl.js +0 -14
  134. package/dist/iris/publicAssetUrl.js.map +0 -1
  135. package/dist/iris/rootPathResolver.d.ts +0 -9
  136. package/dist/iris/rootPathResolver.d.ts.map +0 -1
  137. package/dist/iris/rootPathResolver.js +0 -32
  138. package/dist/iris/rootPathResolver.js.map +0 -1
  139. package/dist/iris/signing.d.ts +0 -50
  140. package/dist/iris/signing.d.ts.map +0 -1
  141. package/dist/iris/signing.js +0 -299
  142. package/dist/iris/signing.js.map +0 -1
  143. package/dist/iris/treeRootCache.d.ts +0 -86
  144. package/dist/iris/treeRootCache.d.ts.map +0 -1
  145. package/dist/iris/treeRootCache.js +0 -269
  146. package/dist/iris/treeRootCache.js.map +0 -1
  147. package/dist/iris/treeRootSubscription.d.ts +0 -55
  148. package/dist/iris/treeRootSubscription.d.ts.map +0 -1
  149. package/dist/iris/treeRootSubscription.js +0 -479
  150. package/dist/iris/treeRootSubscription.js.map +0 -1
  151. package/dist/iris/utils/constants.d.ts +0 -76
  152. package/dist/iris/utils/constants.d.ts.map +0 -1
  153. package/dist/iris/utils/constants.js +0 -113
  154. package/dist/iris/utils/constants.js.map +0 -1
  155. package/dist/iris/utils/errorMessage.d.ts +0 -5
  156. package/dist/iris/utils/errorMessage.d.ts.map +0 -1
  157. package/dist/iris/utils/errorMessage.js +0 -8
  158. package/dist/iris/utils/errorMessage.js.map +0 -1
  159. package/dist/iris/utils/lruCache.d.ts +0 -26
  160. package/dist/iris/utils/lruCache.d.ts.map +0 -1
  161. package/dist/iris/utils/lruCache.js +0 -66
  162. package/dist/iris/utils/lruCache.js.map +0 -1
  163. package/dist/iris/webrtc.d.ts +0 -2
  164. package/dist/iris/webrtc.d.ts.map +0 -1
  165. package/dist/iris/webrtc.js +0 -3
  166. package/dist/iris/webrtc.js.map +0 -1
  167. package/dist/iris/webrtcSignaling.d.ts +0 -37
  168. package/dist/iris/webrtcSignaling.d.ts.map +0 -1
  169. package/dist/iris/webrtcSignaling.js +0 -86
  170. package/dist/iris/webrtcSignaling.js.map +0 -1
  171. package/dist/iris/worker.d.ts +0 -12
  172. package/dist/iris/worker.d.ts.map +0 -1
  173. package/dist/iris/worker.js +0 -1529
  174. package/dist/iris/worker.js.map +0 -1
  175. package/dist/iris-client.d.ts +0 -31
  176. package/dist/iris-client.d.ts.map +0 -1
  177. package/dist/iris-client.js +0 -197
  178. package/dist/iris-client.js.map +0 -1
  179. package/dist/iris-entry.d.ts +0 -2
  180. package/dist/iris-entry.d.ts.map +0 -1
  181. package/dist/iris-entry.js +0 -2
  182. package/dist/iris-entry.js.map +0 -1
  183. package/dist/mediaStreaming.d.ts +0 -7
  184. package/dist/mediaStreaming.d.ts.map +0 -1
  185. package/dist/mediaStreaming.js +0 -48
  186. package/dist/mediaStreaming.js.map +0 -1
  187. package/dist/p2p/boundedQueue.d.ts +0 -79
  188. package/dist/p2p/boundedQueue.d.ts.map +0 -1
  189. package/dist/p2p/boundedQueue.js +0 -134
  190. package/dist/p2p/boundedQueue.js.map +0 -1
  191. package/dist/p2p/errorMessage.d.ts +0 -5
  192. package/dist/p2p/errorMessage.d.ts.map +0 -1
  193. package/dist/p2p/errorMessage.js +0 -7
  194. package/dist/p2p/errorMessage.js.map +0 -1
  195. package/dist/p2p/index.d.ts +0 -8
  196. package/dist/p2p/index.d.ts.map +0 -1
  197. package/dist/p2p/index.js +0 -6
  198. package/dist/p2p/index.js.map +0 -1
  199. package/dist/p2p/lruCache.d.ts +0 -26
  200. package/dist/p2p/lruCache.d.ts.map +0 -1
  201. package/dist/p2p/lruCache.js +0 -65
  202. package/dist/p2p/lruCache.js.map +0 -1
  203. package/dist/p2p/meshQueryRouter.d.ts +0 -44
  204. package/dist/p2p/meshQueryRouter.d.ts.map +0 -1
  205. package/dist/p2p/meshQueryRouter.js +0 -228
  206. package/dist/p2p/meshQueryRouter.js.map +0 -1
  207. package/dist/p2p/protocol.d.ts +0 -10
  208. package/dist/p2p/protocol.d.ts.map +0 -1
  209. package/dist/p2p/protocol.js +0 -2
  210. package/dist/p2p/protocol.js.map +0 -1
  211. package/dist/p2p/queryForwardingMachine.d.ts +0 -46
  212. package/dist/p2p/queryForwardingMachine.d.ts.map +0 -1
  213. package/dist/p2p/queryForwardingMachine.js +0 -144
  214. package/dist/p2p/queryForwardingMachine.js.map +0 -1
  215. package/dist/p2p/signaling.d.ts +0 -63
  216. package/dist/p2p/signaling.d.ts.map +0 -1
  217. package/dist/p2p/signaling.js +0 -185
  218. package/dist/p2p/signaling.js.map +0 -1
  219. package/dist/p2p/uploadRateLimiter.d.ts +0 -21
  220. package/dist/p2p/uploadRateLimiter.d.ts.map +0 -1
  221. package/dist/p2p/uploadRateLimiter.js +0 -62
  222. package/dist/p2p/uploadRateLimiter.js.map +0 -1
  223. package/dist/p2p/webrtcController.d.ts +0 -168
  224. package/dist/p2p/webrtcController.d.ts.map +0 -1
  225. package/dist/p2p/webrtcController.js +0 -902
  226. package/dist/p2p/webrtcController.js.map +0 -1
  227. package/dist/p2p/webrtcProxy.d.ts +0 -62
  228. package/dist/p2p/webrtcProxy.d.ts.map +0 -1
  229. package/dist/p2p/webrtcProxy.js +0 -447
  230. package/dist/p2p/webrtcProxy.js.map +0 -1
  231. package/dist/privacyGuards.d.ts +0 -14
  232. package/dist/privacyGuards.d.ts.map +0 -1
  233. package/dist/privacyGuards.js +0 -27
  234. package/dist/privacyGuards.js.map +0 -1
  235. package/dist/protocol.d.ts +0 -225
  236. package/dist/protocol.d.ts.map +0 -1
  237. package/dist/protocol.js +0 -2
  238. package/dist/protocol.js.map +0 -1
  239. package/dist/runtime-network.d.ts +0 -23
  240. package/dist/runtime-network.d.ts.map +0 -1
  241. package/dist/runtime-network.js +0 -105
  242. package/dist/runtime-network.js.map +0 -1
  243. package/dist/runtime.d.ts +0 -23
  244. package/dist/runtime.d.ts.map +0 -1
  245. package/dist/runtime.js +0 -122
  246. package/dist/runtime.js.map +0 -1
  247. package/dist/tree-root.d.ts +0 -201
  248. package/dist/tree-root.d.ts.map +0 -1
  249. package/dist/tree-root.js +0 -632
  250. package/dist/tree-root.js.map +0 -1
  251. package/dist/types.d.ts +0 -2
  252. package/dist/types.d.ts.map +0 -1
  253. package/dist/types.js +0 -2
  254. package/dist/types.js.map +0 -1
  255. package/dist/worker.d.ts +0 -9
  256. package/dist/worker.d.ts.map +0 -1
  257. package/dist/worker.js +0 -797
  258. package/dist/worker.js.map +0 -1
@@ -1,1529 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * Hashtree Worker
4
- *
5
- * Dedicated worker that owns:
6
- * - HashTree + DexieStore (IndexedDB storage)
7
- * - WebRTC peer connections (P2P data transfer)
8
- *
9
- * Main thread communicates via postMessage.
10
- * NIP-07 signing/encryption delegated back to main thread.
11
- */
12
- import { HashTree, BlossomStore } from '@hashtree/core';
13
- import { DexieStore } from '@hashtree/dexie';
14
- import { initTreeRootCache, getCachedRootInfo, setCachedRoot, mergeCachedRootKey, clearMemoryCache } from './treeRootCache';
15
- import { handleTreeRootEvent, isTreeRootEvent, setNotifyCallback as setTreeRootNotifyCallback, subscribeToTreeRoots, unsubscribeFromTreeRoots } from './treeRootSubscription';
16
- import { initNdk, closeNdk, subscribe as ndkSubscribe, unsubscribe as ndkUnsubscribe, publish as ndkPublish, setOnEvent, setOnEose, getRelayStats as getNdkRelayStats, republishTrees, republishTree, setRelays as ndkSetRelays, } from './ndk';
17
- import { initIdentity, setIdentity, clearIdentity } from './identity';
18
- import { setResponseSender, signEvent, handleSignedResponse, handleEncryptedResponse, handleDecryptedResponse, } from './signing';
19
- import { WebRTCController } from './webrtc';
20
- import { SocialGraph } from 'nostr-social-graph';
21
- import Dexie from 'dexie';
22
- import { LRUCache } from './utils/lruCache';
23
- import { getErrorMessage } from './utils/errorMessage';
24
- import { DEFAULT_BOOTSTRAP_PUBKEY } from './utils/constants';
25
- import { nip19 } from 'nostr-tools';
26
- // Dexie database for social graph persistence
27
- class SocialGraphDB extends Dexie {
28
- socialGraph;
29
- constructor() {
30
- super('hashtree-social-graph');
31
- this.version(1).stores({
32
- socialGraph: '&id',
33
- });
34
- }
35
- }
36
- const socialGraphDB = new SocialGraphDB();
37
- import { initMediaHandler, registerMediaPort } from './mediaHandler';
38
- import { resolveRootPath } from './rootPathResolver';
39
- import { initWebRTCSignaling, sendWebRTCSignaling, setupWebRTCSignalingSubscription, handleWebRTCSignalingEvent, resubscribeWebRTCSignaling, } from './webrtcSignaling';
40
- import { BlossomBandwidthTracker } from '../capabilities/blossomBandwidthTracker';
41
- import { MeshRouterStore } from '../capabilities/meshRouterStore';
42
- // Worker state
43
- let tree = null;
44
- let store = null;
45
- let meshStore = null;
46
- let blossomStore = null;
47
- const blossomBandwidthTracker = new BlossomBandwidthTracker((stats) => {
48
- respond({ type: 'blossomBandwidth', stats });
49
- });
50
- let webrtc = null;
51
- let webrtcStarted = false;
52
- let _config = null;
53
- const WEBRTC_REQUEST_TIMEOUT_MS = 5000;
54
- const REMOTE_READ_TIMEOUT_MS = 15000;
55
- const treeRootSubscriptionRefs = new Map();
56
- // Storage quota management
57
- let storageMaxBytes = 1024 * 1024 * 1024; // Default 1GB
58
- let evictionCheckPending = false;
59
- const EVICTION_CHECK_DEBOUNCE_MS = 5000; // Debounce eviction checks
60
- /**
61
- * Run storage eviction if over limit.
62
- * Debounced to avoid running too frequently after many puts.
63
- */
64
- function runEvictionCheck() {
65
- if (evictionCheckPending || !store)
66
- return;
67
- evictionCheckPending = true;
68
- setTimeout(async () => {
69
- evictionCheckPending = false;
70
- if (!store)
71
- return;
72
- try {
73
- const evicted = await store.evict(storageMaxBytes);
74
- if (evicted > 0) {
75
- console.log(`[Worker] Eviction completed, removed ${evicted} entries`);
76
- }
77
- }
78
- catch (err) {
79
- console.error('[Worker] Eviction error:', err);
80
- }
81
- }, EVICTION_CHECK_DEBOUNCE_MS);
82
- }
83
- let blossomProgress = null;
84
- const BLOSSOM_PROGRESS_THROTTLE_MS = 100; // Throttle progress updates to every 100ms
85
- function initBlossomProgress(sessionId, totalChunks, serverUrls) {
86
- const serverStatus = new Map();
87
- for (const url of serverUrls) {
88
- serverStatus.set(url, { url, uploaded: 0, failed: 0, skipped: 0 });
89
- }
90
- blossomProgress = {
91
- sessionId,
92
- totalChunks,
93
- processedChunks: 0,
94
- serverStatus,
95
- lastNotifyTime: 0,
96
- };
97
- }
98
- function updateBlossomProgress(serverUrl, status) {
99
- if (!blossomProgress)
100
- return;
101
- const serverStat = blossomProgress.serverStatus.get(serverUrl);
102
- if (serverStat) {
103
- serverStat[status]++;
104
- }
105
- // Count as processed when all servers have responded for this chunk
106
- // We track per-server, so check if any server completed this chunk
107
- const allServersDone = Array.from(blossomProgress.serverStatus.values()).every(s => (s.uploaded + s.skipped + s.failed) > (blossomProgress.processedChunks));
108
- if (allServersDone) {
109
- blossomProgress.processedChunks++;
110
- }
111
- // Throttle progress updates
112
- const now = Date.now();
113
- if (now - blossomProgress.lastNotifyTime >= BLOSSOM_PROGRESS_THROTTLE_MS ||
114
- blossomProgress.processedChunks >= blossomProgress.totalChunks) {
115
- blossomProgress.lastNotifyTime = now;
116
- notifyBlossomProgress();
117
- }
118
- }
119
- function notifyBlossomProgress() {
120
- if (!blossomProgress)
121
- return;
122
- const progress = {
123
- sessionId: blossomProgress.sessionId,
124
- totalChunks: blossomProgress.totalChunks,
125
- processedChunks: blossomProgress.processedChunks,
126
- servers: Array.from(blossomProgress.serverStatus.values()),
127
- };
128
- respond({ type: 'blossomUploadProgress', progress });
129
- }
130
- function clearBlossomProgress() {
131
- blossomProgress = null;
132
- }
133
- /**
134
- * Push a tree to blossom servers explicitly
135
- * Uses tree.push() to walk the tree and upload all chunks
136
- */
137
- async function handlePushToBlossom(id, cidHash, cidKey, treeName) {
138
- if (!tree || !blossomStore) {
139
- respond({ type: 'blossomPushResult', id, pushed: 0, skipped: 0, failed: 0, error: 'Tree or BlossomStore not initialized' });
140
- return;
141
- }
142
- const cid = cidKey ? { hash: cidHash, key: cidKey } : { hash: cidHash };
143
- const name = treeName || 'unknown';
144
- let lastNotify = 0;
145
- try {
146
- console.log('[Worker] Starting blossom push for:', name);
147
- const result = await tree.push(cid, blossomStore, {
148
- onProgress: (current, total) => {
149
- // Update old blossom progress if session is active
150
- if (blossomProgress) {
151
- blossomProgress.processedChunks = current;
152
- blossomProgress.totalChunks = total;
153
- notifyBlossomProgress();
154
- }
155
- // Also emit new-style progress events (throttled)
156
- const now = Date.now();
157
- if (now - lastNotify >= 100 || current === total) {
158
- lastNotify = now;
159
- respond({ type: 'blossomPushProgress', treeName: name, current, total });
160
- }
161
- },
162
- });
163
- console.log('[Worker] Blossom push complete:', name, result);
164
- // Extract error messages from failed uploads
165
- const errorMessages = result.errors.map(e => e.error.message);
166
- // Emit completion event
167
- respond({
168
- type: 'blossomPushComplete',
169
- treeName: name,
170
- pushed: result.pushed,
171
- skipped: result.skipped,
172
- failed: result.failed,
173
- });
174
- respond({
175
- type: 'blossomPushResult',
176
- id,
177
- pushed: result.pushed,
178
- skipped: result.skipped,
179
- failed: result.failed,
180
- errors: errorMessages.length > 0 ? errorMessages : undefined,
181
- });
182
- }
183
- catch (err) {
184
- const error = getErrorMessage(err);
185
- console.error('[Worker] Blossom push failed:', error);
186
- respond({ type: 'blossomPushResult', id, pushed: 0, skipped: 0, failed: 0, error });
187
- }
188
- }
189
- /**
190
- * Count unique bytes in first 256 bytes (entropy check)
191
- */
192
- function countUniqueBytes(data) {
193
- const sampleSize = Math.min(data.length, 256);
194
- const seen = new Set();
195
- for (let i = 0; i < sampleSize; i++) {
196
- seen.add(data[i]);
197
- }
198
- return seen.size;
199
- }
200
- const ENTROPY_THRESHOLD = 111; // From Blossom error "Unique: 97 (min: 111)"
201
- /**
202
- * Push to Blossom with progress reporting
203
- */
204
- async function pushToBlossomWithProgress(treeName, hash, key) {
205
- if (!tree || !blossomStore) {
206
- return { pushed: 0, skipped: 0, failed: 0, errors: [] };
207
- }
208
- const cid = key ? { hash, key } : { hash };
209
- let lastNotify = 0;
210
- // Pre-check: walk blocks and check entropy before uploading
211
- let lowEntropyCount = 0;
212
- for await (const block of tree.walkBlocks(cid)) {
213
- if (block.data.length >= 256) {
214
- const uniqueBytes = countUniqueBytes(block.data);
215
- if (uniqueBytes < ENTROPY_THRESHOLD) {
216
- lowEntropyCount++;
217
- console.warn(`[Worker] Low entropy blob detected: ${uniqueBytes} unique bytes (min: ${ENTROPY_THRESHOLD}), size: ${block.data.length}`);
218
- }
219
- }
220
- }
221
- if (lowEntropyCount > 0) {
222
- console.error(`[Worker] Found ${lowEntropyCount} low-entropy blobs in ${treeName} - data may not be encrypted!`);
223
- return {
224
- pushed: 0,
225
- skipped: 0,
226
- failed: lowEntropyCount,
227
- errors: [`Data not encrypted. Found ${lowEntropyCount} blobs with low entropy (< ${ENTROPY_THRESHOLD} unique bytes). Re-encryption required.`],
228
- };
229
- }
230
- const result = await tree.push(cid, blossomStore, {
231
- onProgress: (current, total) => {
232
- const now = Date.now();
233
- // Throttle to every 100ms
234
- if (now - lastNotify >= 100 || current === total) {
235
- lastNotify = now;
236
- respond({ type: 'blossomPushProgress', treeName, current, total });
237
- }
238
- },
239
- });
240
- const errorMessages = result.errors.map(e => e.error.message);
241
- respond({
242
- type: 'blossomPushComplete',
243
- treeName,
244
- pushed: result.pushed,
245
- skipped: result.skipped,
246
- failed: result.failed,
247
- });
248
- return { pushed: result.pushed, skipped: result.skipped, failed: result.failed, errors: errorMessages };
249
- }
250
- /**
251
- * Republish all cached tree events to relays and push blobs to Blossom
252
- * @param prefix - Optional URL-encoded prefix to filter trees by d-tag
253
- */
254
- async function handleRepublishTrees(id, prefix) {
255
- if (!_config?.pubkey) {
256
- respond({ type: 'republishResult', id, count: 0, error: 'Not logged in' });
257
- return;
258
- }
259
- // Track trees with encryption errors
260
- const encryptionErrors = [];
261
- // Create a push function that uses tree.push() with progress reporting
262
- const pushToBlossom = async (hash, key, treeName) => {
263
- // If no key, tree is unencrypted - add to encryption errors list
264
- if (!key) {
265
- console.log(`[Worker] Tree ${treeName} has no key - needs encryption`);
266
- encryptionErrors.push(treeName || 'unknown');
267
- return { pushed: 0, skipped: 0, failed: 1 };
268
- }
269
- const result = await pushToBlossomWithProgress(treeName || 'unknown', hash, key);
270
- // Check for encryption errors
271
- if (result.failed > 0 && result.errors.some(e => e.includes('not encrypted') || e.includes('Unique:'))) {
272
- encryptionErrors.push(treeName || 'unknown');
273
- }
274
- return result;
275
- };
276
- try {
277
- const count = await republishTrees(_config.pubkey, signEvent, pushToBlossom, prefix);
278
- respond({
279
- type: 'republishResult',
280
- id,
281
- count,
282
- encryptionErrors: encryptionErrors.length > 0 ? encryptionErrors : undefined,
283
- });
284
- }
285
- catch (err) {
286
- const error = getErrorMessage(err);
287
- console.error('[Worker] Republish failed:', error);
288
- respond({ type: 'republishResult', id, count: 0, error });
289
- }
290
- }
291
- async function handleRepublishTree(id, pubkey, treeName) {
292
- try {
293
- const success = await republishTree(pubkey, treeName);
294
- respond({ type: 'bool', id, value: success });
295
- }
296
- catch (err) {
297
- const error = getErrorMessage(err);
298
- console.error('[Worker] Republish tree failed:', error);
299
- respond({ type: 'bool', id, value: false, error });
300
- }
301
- }
302
- // Follows set for WebRTC peer classification
303
- let followsSet = new Set();
304
- function getFollows() {
305
- return followsSet;
306
- }
307
- // SocialGraph state
308
- const KIND_CONTACTS = 3; // kind:3 = contact list
309
- let socialGraph = new SocialGraph(DEFAULT_BOOTSTRAP_PUBKEY);
310
- let socialGraphVersion = 0;
311
- let socialGraphSaveTimeout = null;
312
- let socialGraphDirty = false;
313
- function notifySocialGraphVersionUpdate() {
314
- socialGraphVersion++;
315
- socialGraphDirty = true;
316
- self.postMessage({ type: 'socialGraphVersion', version: socialGraphVersion });
317
- // Debounce save - save 5 seconds after last update
318
- if (socialGraphSaveTimeout) {
319
- clearTimeout(socialGraphSaveTimeout);
320
- }
321
- socialGraphSaveTimeout = setTimeout(() => {
322
- saveSocialGraph();
323
- }, 5000);
324
- }
325
- /**
326
- * Save social graph to IndexedDB
327
- */
328
- async function saveSocialGraph() {
329
- if (!socialGraphDirty)
330
- return;
331
- try {
332
- const data = await socialGraph.toBinary();
333
- await socialGraphDB.socialGraph.put({
334
- id: 'main',
335
- data,
336
- updatedAt: Date.now(),
337
- });
338
- socialGraphDirty = false;
339
- console.log('[Worker] Social graph saved to IndexedDB:', data.byteLength, 'bytes');
340
- }
341
- catch (err) {
342
- console.error('[Worker] Failed to save social graph:', err);
343
- }
344
- }
345
- // Track if social graph is still loading - don't subscribe until done
346
- let socialGraphLoading = false;
347
- /**
348
- * Load social graph from IndexedDB
349
- * Waits for completion (no timeout race condition)
350
- */
351
- async function loadSocialGraph(rootPubkey) {
352
- socialGraphLoading = true;
353
- try {
354
- const row = await socialGraphDB.socialGraph.get('main');
355
- if (row?.data) {
356
- const loaded = await SocialGraph.fromBinary(rootPubkey, row.data);
357
- await socialGraph.merge(loaded);
358
- console.log('[Worker] Loaded social graph from IndexedDB, age:', Math.round((Date.now() - row.updatedAt) / 1000), 'seconds, size:', socialGraph.size());
359
- return true;
360
- }
361
- console.log('[Worker] No saved social graph found, starting fresh');
362
- return false;
363
- }
364
- catch (err) {
365
- console.error('[Worker] Failed to load social graph:', err);
366
- return false;
367
- }
368
- finally {
369
- socialGraphLoading = false;
370
- }
371
- }
372
- // Set up response sender for signing module
373
- setResponseSender((msg) => self.postMessage(msg));
374
- // ============================================================================
375
- // Message Handler
376
- // ============================================================================
377
- self.onmessage = async (e) => {
378
- const msg = e.data;
379
- try {
380
- switch (msg.type) {
381
- // Lifecycle
382
- case 'init':
383
- await handleInit(msg.id, msg.config);
384
- break;
385
- case 'close':
386
- await handleClose(msg.id);
387
- break;
388
- case 'setIdentity':
389
- handleSetIdentity(msg.id, msg.pubkey, msg.nsec);
390
- break;
391
- // Heartbeat
392
- case 'ping':
393
- respond({ type: 'pong', id: msg.id });
394
- break;
395
- // Store operations
396
- case 'get':
397
- await handleGet(msg.id, msg.hash);
398
- break;
399
- case 'put':
400
- await handlePut(msg.id, msg.hash, msg.data);
401
- break;
402
- case 'has':
403
- await handleHas(msg.id, msg.hash);
404
- break;
405
- case 'delete':
406
- await handleDelete(msg.id, msg.hash);
407
- break;
408
- // Tree operations
409
- case 'readFile':
410
- await handleReadFile(msg.id, msg.cid);
411
- break;
412
- case 'readFileRange':
413
- await handleReadFileRange(msg.id, msg.cid, msg.start, msg.end);
414
- break;
415
- case 'readFileStream':
416
- await handleReadFileStream(msg.id, msg.cid);
417
- break;
418
- case 'writeFile':
419
- await handleWriteFile(msg.id, msg.parentCid, msg.path, msg.data);
420
- break;
421
- case 'deleteFile':
422
- await handleDeleteFile(msg.id, msg.parentCid, msg.path);
423
- break;
424
- case 'listDir':
425
- await handleListDir(msg.id, msg.cid);
426
- break;
427
- case 'resolveRoot':
428
- await handleResolveRoot(msg.id, msg.npub, msg.path);
429
- break;
430
- case 'setTreeRootCache':
431
- await handleSetTreeRootCache(msg.id, msg.npub, msg.treeName, msg.hash, msg.key, msg.visibility, msg.labels, {
432
- encryptedKey: msg.encryptedKey,
433
- keyId: msg.keyId,
434
- selfEncryptedKey: msg.selfEncryptedKey,
435
- selfEncryptedLinkKey: msg.selfEncryptedLinkKey,
436
- });
437
- break;
438
- case 'getTreeRootInfo':
439
- await handleGetTreeRootInfo(msg.id, msg.npub, msg.treeName);
440
- break;
441
- case 'mergeTreeRootKey':
442
- await handleMergeTreeRootKey(msg.id, msg.npub, msg.treeName, msg.hash, msg.key);
443
- break;
444
- case 'subscribeTreeRoots':
445
- await handleSubscribeTreeRoots(msg.id, msg.pubkey);
446
- break;
447
- case 'unsubscribeTreeRoots':
448
- await handleUnsubscribeTreeRoots(msg.id, msg.pubkey);
449
- break;
450
- // Nostr (TODO: Phase 2)
451
- case 'subscribe':
452
- await handleSubscribe(msg.id, msg.filters);
453
- break;
454
- case 'unsubscribe':
455
- await handleUnsubscribe(msg.id, msg.subId);
456
- break;
457
- case 'publish':
458
- await handlePublish(msg.id, msg.event);
459
- break;
460
- // Media streaming
461
- case 'registerMediaPort':
462
- registerMediaPort(msg.port, msg.debug);
463
- break;
464
- // Stats
465
- case 'getPeerStats':
466
- await handleGetPeerStats(msg.id);
467
- break;
468
- case 'getRelayStats':
469
- await handleGetRelayStats(msg.id);
470
- break;
471
- case 'getStorageStats':
472
- await handleGetStorageStats(msg.id);
473
- break;
474
- // WebRTC pool configuration
475
- case 'setWebRTCPools':
476
- if (webrtc) {
477
- webrtc.setPoolConfig(msg.pools);
478
- // Start WebRTC on first pool config (waits for settings to load)
479
- if (!webrtcStarted) {
480
- webrtc.start();
481
- webrtcStarted = true;
482
- console.log('[Worker] WebRTC controller started (after pool config)');
483
- }
484
- }
485
- respond({ type: 'void', id: msg.id });
486
- break;
487
- case 'sendWebRTCHello':
488
- webrtc?.broadcastHello();
489
- respond({ type: 'void', id: msg.id });
490
- break;
491
- case 'setFollows':
492
- followsSet = new Set(msg.follows);
493
- console.log('[Worker] Follows updated:', followsSet.size, 'pubkeys:', Array.from(followsSet).map(p => p.slice(0, 16)));
494
- respond({ type: 'void', id: msg.id });
495
- break;
496
- // Blossom configuration
497
- case 'setBlossomServers':
498
- if (_config) {
499
- _config.blossomServers = msg.servers;
500
- }
501
- if (msg.servers && msg.servers.length > 0) {
502
- blossomStore = createTrackedBlossomStore(msg.servers);
503
- console.log('[Worker] BlossomStore updated with', msg.servers.length, 'servers');
504
- }
505
- else {
506
- blossomStore = null;
507
- console.log('[Worker] BlossomStore disabled (no servers configured)');
508
- }
509
- emitBlossomBandwidthSnapshot();
510
- respond({ type: 'void', id: msg.id });
511
- break;
512
- // Storage quota configuration
513
- case 'setStorageMaxBytes':
514
- storageMaxBytes = msg.maxBytes;
515
- console.log('[Worker] Storage limit set to', Math.round(storageMaxBytes / 1024 / 1024), 'MB');
516
- // Run eviction check immediately when limit changes
517
- runEvictionCheck();
518
- respond({ type: 'void', id: msg.id });
519
- break;
520
- // Relay configuration
521
- case 'setRelays':
522
- await ndkSetRelays(msg.relays);
523
- // Re-subscribe to WebRTC signaling on new relays
524
- resubscribeWebRTCSignaling();
525
- console.log('[Worker] Relays updated to', msg.relays.length, 'relays');
526
- respond({ type: 'void', id: msg.id });
527
- break;
528
- // Blossom upload
529
- case 'pushToBlossom':
530
- handlePushToBlossom(msg.id, msg.cidHash, msg.cidKey, msg.treeName);
531
- break;
532
- case 'startBlossomSession':
533
- {
534
- const serverUrls = blossomStore?.getWriteServers() || [];
535
- initBlossomProgress(msg.sessionId, msg.totalChunks, serverUrls);
536
- console.log('[Worker] Blossom session started:', msg.sessionId, 'chunks:', msg.totalChunks, 'servers:', serverUrls.length);
537
- respond({ type: 'void', id: msg.id });
538
- }
539
- break;
540
- case 'endBlossomSession':
541
- clearBlossomProgress();
542
- console.log('[Worker] Blossom session ended');
543
- respond({ type: 'void', id: msg.id });
544
- break;
545
- // Republish cached tree events
546
- case 'republishTrees':
547
- handleRepublishTrees(msg.id, msg.prefix);
548
- break;
549
- case 'republishTree':
550
- handleRepublishTree(msg.id, msg.pubkey, msg.treeName);
551
- break;
552
- // SocialGraph operations
553
- case 'initSocialGraph':
554
- handleInitSocialGraph(msg.id, msg.rootPubkey);
555
- break;
556
- case 'setSocialGraphRoot':
557
- handleSetSocialGraphRoot(msg.id, msg.pubkey);
558
- break;
559
- case 'handleSocialGraphEvents':
560
- handleSocialGraphEvents(msg.id, msg.events);
561
- break;
562
- case 'getFollowDistance':
563
- handleGetFollowDistance(msg.id, msg.pubkey);
564
- break;
565
- case 'isFollowing':
566
- handleIsFollowing(msg.id, msg.follower, msg.followed);
567
- break;
568
- case 'getFollows':
569
- handleGetFollowsList(msg.id, msg.pubkey);
570
- break;
571
- case 'getFollowers':
572
- handleGetFollowers(msg.id, msg.pubkey);
573
- break;
574
- case 'getFollowedByFriends':
575
- handleGetFollowedByFriends(msg.id, msg.pubkey);
576
- break;
577
- case 'fetchUserFollows':
578
- handleFetchUserFollows(msg.id, msg.pubkey);
579
- break;
580
- case 'fetchUserFollowers':
581
- handleFetchUserFollowers(msg.id, msg.pubkey);
582
- break;
583
- case 'getSocialGraphSize':
584
- handleGetSocialGraphSize(msg.id);
585
- break;
586
- // NIP-07 responses from main thread
587
- case 'signed':
588
- handleSignedResponse(msg.id, msg.event, msg.error);
589
- break;
590
- case 'encrypted':
591
- handleEncryptedResponse(msg.id, msg.ciphertext, msg.error);
592
- break;
593
- case 'decrypted':
594
- handleDecryptedResponse(msg.id, msg.plaintext, msg.error);
595
- break;
596
- // WebRTC proxy events from main thread
597
- case 'rtc:peerCreated':
598
- case 'rtc:peerStateChange':
599
- case 'rtc:peerClosed':
600
- case 'rtc:offerCreated':
601
- case 'rtc:answerCreated':
602
- case 'rtc:descriptionSet':
603
- case 'rtc:iceCandidate':
604
- case 'rtc:iceGatheringComplete':
605
- case 'rtc:dataChannelOpen':
606
- case 'rtc:dataChannelMessage':
607
- case 'rtc:dataChannelClose':
608
- case 'rtc:dataChannelError':
609
- webrtc?.handleProxyEvent(msg);
610
- break;
611
- default:
612
- console.warn('[Worker] Unknown message type:', msg.type);
613
- }
614
- }
615
- catch (err) {
616
- const error = getErrorMessage(err);
617
- console.error('[Worker] Error handling message:', error);
618
- respond({ type: 'error', id: msg.id, error });
619
- }
620
- };
621
- // ============================================================================
622
- // Response Helper
623
- // ============================================================================
624
- function respond(msg) {
625
- self.postMessage(msg);
626
- }
627
- function respondWithTransfer(msg, transfer) {
628
- // Worker scope postMessage takes options object with transfer property
629
- self.postMessage(msg, { transfer });
630
- }
631
- // ============================================================================
632
- // Lifecycle Handlers
633
- // ============================================================================
634
- async function handleInit(id, cfg) {
635
- try {
636
- _config = cfg;
637
- blossomBandwidthTracker.reset();
638
- emitBlossomBandwidthSnapshot();
639
- // Initialize Dexie/IndexedDB store
640
- const storeName = cfg.storeName || 'hashtree-worker';
641
- store = new DexieStore(storeName);
642
- meshStore = createMeshStore(store);
643
- // Initialize HashTree with the adaptive mesh router store.
644
- tree = new HashTree({ store: meshStore });
645
- // Initialize tree root cache
646
- initTreeRootCache(store);
647
- console.log('[Worker] Initialized with DexieStore:', storeName);
648
- // Initialize identity
649
- initIdentity(cfg.pubkey, cfg.nsec);
650
- console.log('[Worker] User pubkey:', cfg.pubkey.slice(0, 16) + '...');
651
- // Initialize Blossom store with signer for uploads and progress callback
652
- if (cfg.blossomServers && cfg.blossomServers.length > 0) {
653
- blossomStore = createTrackedBlossomStore(cfg.blossomServers);
654
- console.log('[Worker] Initialized BlossomStore with', cfg.blossomServers.length, 'servers');
655
- }
656
- // Initialize NDK with relays, cache, and nostr-wasm verification
657
- await initNdk(cfg.relays, {
658
- pubkey: cfg.pubkey,
659
- nsec: cfg.nsec,
660
- });
661
- console.log('[Worker] NDK initialized with', cfg.relays.length, 'relays');
662
- // Set up unified event handler for all subscriptions
663
- setOnEvent(async (subId, event) => {
664
- const isTreeRoot = isTreeRootEvent(event);
665
- if (isTreeRoot) {
666
- try {
667
- await handleTreeRootEvent(event);
668
- }
669
- catch (err) {
670
- console.warn('[Worker] Failed to handle tree root event:', err);
671
- }
672
- }
673
- // Forward to main thread
674
- respond({ type: 'event', subId, event });
675
- // Route to WebRTC handler
676
- if (subId.startsWith('webrtc-')) {
677
- handleWebRTCSignalingEvent(event);
678
- }
679
- // Route to SocialGraph handler (all socialgraph-* subscriptions)
680
- if (subId.startsWith('socialgraph-') && event.kind === KIND_CONTACTS) {
681
- handleSocialGraphEvent(event);
682
- }
683
- });
684
- // Set up tree root notification callback to notify main thread
685
- setTreeRootNotifyCallback((npub, treeName, record) => {
686
- respond({
687
- type: 'treeRootUpdate',
688
- npub,
689
- treeName,
690
- hash: record.hash,
691
- key: record.key,
692
- visibility: record.visibility,
693
- labels: record.labels,
694
- updatedAt: record.updatedAt,
695
- snapshotNhash: record.snapshotNhash,
696
- encryptedKey: record.encryptedKey,
697
- keyId: record.keyId,
698
- selfEncryptedKey: record.selfEncryptedKey,
699
- selfEncryptedLinkKey: record.selfEncryptedLinkKey,
700
- });
701
- });
702
- // Set up EOSE handler
703
- setOnEose((subId) => {
704
- respond({ type: 'eose', subId });
705
- });
706
- // Initialize WebRTC controller (RTCPeerConnection runs in main thread proxy)
707
- webrtc = new WebRTCController({
708
- pubkey: cfg.pubkey,
709
- localStore: store,
710
- sendCommand: (cmd) => respond(cmd),
711
- sendSignaling: async (msg, recipientPubkey) => {
712
- await sendWebRTCSignaling(msg, recipientPubkey);
713
- },
714
- getFollows, // Used to classify peers into follows/other pools
715
- debug: false,
716
- requestTimeout: WEBRTC_REQUEST_TIMEOUT_MS,
717
- upstreamFetch: async (hash) => (await meshStore?.getDetailed(hash, {
718
- skipPrimary: true,
719
- sourceIds: ['blossom'],
720
- }))?.data ?? null,
721
- });
722
- // Initialize media handler with the tree
723
- initMediaHandler(tree);
724
- // Initialize WebRTC signaling with the controller
725
- initWebRTCSignaling(webrtc);
726
- // Subscribe to WebRTC signaling events (kind 25050)
727
- setupWebRTCSignalingSubscription(cfg.pubkey);
728
- // WebRTC starts when pool config is received (waits for settings to load)
729
- console.log('[Worker] WebRTC controller ready (waiting for pool config)');
730
- // Initialize SocialGraph with user's pubkey as root
731
- socialGraph = new SocialGraph(cfg.pubkey);
732
- console.log('[Worker] SocialGraph initialized with root:', cfg.pubkey.slice(0, 16) + '...');
733
- // Respond ready immediately so page can render
734
- respond({ type: 'ready' });
735
- // Load persisted social graph from IndexedDB, THEN set up subscriptions
736
- // This ensures we only subscribe for users whose follow lists we don't already have
737
- const loaded = await loadSocialGraph(cfg.pubkey);
738
- if (loaded) {
739
- notifySocialGraphVersionUpdate();
740
- console.log('[Worker] Social graph loaded, setting up subscriptions with existing data');
741
- }
742
- // Subscribe to kind:3 (contact list) events for social graph
743
- // Do this AFTER load so we can skip users we already have
744
- setupSocialGraphSubscription(cfg.pubkey);
745
- }
746
- catch (err) {
747
- const error = getErrorMessage(err);
748
- respond({ type: 'error', id, error });
749
- }
750
- }
751
- // NOTE: WebRTC cannot run in workers - RTCPeerConnection is not available
752
- // See: https://github.com/w3c/webrtc-extensions/issues/77
753
- // WebRTC must run in main thread and proxy to worker for storage
754
- /**
755
- * Handle identity change (account switch)
756
- */
757
- function handleSetIdentity(id, pubkey, nsec) {
758
- setIdentity(pubkey, nsec);
759
- console.log('[Worker] Identity updated:', pubkey.slice(0, 16) + '...');
760
- if (_config) {
761
- _config.pubkey = pubkey;
762
- _config.nsec = nsec;
763
- }
764
- if (webrtc) {
765
- webrtc.setIdentity(pubkey);
766
- setupWebRTCSignalingSubscription(pubkey);
767
- }
768
- // Update SocialGraph root
769
- socialGraph.setRoot(pubkey);
770
- notifySocialGraphVersionUpdate();
771
- console.log('[Worker] SocialGraph root updated:', pubkey.slice(0, 16) + '...');
772
- // Reinitialize Blossom with new signer and progress callback
773
- if (_config?.blossomServers && _config.blossomServers.length > 0) {
774
- blossomStore = createTrackedBlossomStore(_config.blossomServers);
775
- }
776
- else {
777
- blossomStore = null;
778
- }
779
- // NOTE: WebRTC not available in workers
780
- respond({ type: 'void', id });
781
- }
782
- /**
783
- * Create Blossom signer using current identity
784
- */
785
- function createBlossomSigner() {
786
- return async (event) => {
787
- const signed = await signEvent({
788
- kind: event.kind,
789
- created_at: event.created_at,
790
- content: event.content,
791
- tags: event.tags,
792
- });
793
- return signed;
794
- };
795
- }
796
- function createTrackedBlossomStore(servers) {
797
- return new BlossomStore({
798
- servers,
799
- signer: createBlossomSigner(),
800
- onUploadProgress: updateBlossomProgress,
801
- logger: (entry) => {
802
- blossomBandwidthTracker.apply(entry);
803
- },
804
- });
805
- }
806
- function createMeshStore(primary) {
807
- return new MeshRouterStore({
808
- primary,
809
- primarySourceId: 'idb',
810
- requestTimeoutMs: REMOTE_READ_TIMEOUT_MS,
811
- sources: [
812
- {
813
- id: 'webrtc',
814
- isAvailable: () => !!webrtc && webrtc.getConnectedCount() > 0,
815
- get: async (hash) => webrtc ? webrtc.get(hash) : null,
816
- },
817
- {
818
- id: 'blossom',
819
- isAvailable: () => !!blossomStore,
820
- get: async (hash) => blossomStore ? blossomStore.get(hash) : null,
821
- },
822
- ],
823
- });
824
- }
825
- function emitBlossomBandwidthSnapshot() {
826
- respond({ type: 'blossomBandwidth', stats: blossomBandwidthTracker.getStats() });
827
- }
828
- async function handleClose(id) {
829
- // NOTE: WebRTC not available in workers
830
- // Close NDK connections
831
- closeNdk();
832
- // Clear identity
833
- clearIdentity();
834
- // Clear caches
835
- clearMemoryCache();
836
- store = null;
837
- meshStore = null;
838
- tree = null;
839
- blossomStore = null;
840
- blossomBandwidthTracker.reset();
841
- _config = null;
842
- respond({ type: 'void', id });
843
- }
844
- // ============================================================================
845
- // Store Handlers (low-level)
846
- // ============================================================================
847
- async function handleGet(id, hash) {
848
- if (!meshStore) {
849
- respond({ type: 'result', id, error: 'Store not initialized' });
850
- return;
851
- }
852
- const data = await meshStore.get(hash);
853
- if (data) {
854
- // Transfer the ArrayBuffer to avoid copying
855
- respondWithTransfer({ type: 'result', id, data }, [data.buffer]);
856
- }
857
- else {
858
- respond({ type: 'result', id, data: undefined });
859
- }
860
- }
861
- async function handlePut(id, hash, data) {
862
- if (!store) {
863
- respond({ type: 'bool', id, value: false, error: 'Store not initialized' });
864
- return;
865
- }
866
- const success = await store.put(hash, data);
867
- respond({ type: 'bool', id, value: success });
868
- // Trigger eviction check (debounced) after successful put
869
- if (success) {
870
- runEvictionCheck();
871
- }
872
- // Fire-and-forget push to blossom (don't await - optimistic upload)
873
- if (blossomStore && success) {
874
- blossomStore.put(hash, data).catch((err) => {
875
- const hashHex = Array.from(hash.slice(0, 8)).map(b => b.toString(16).padStart(2, '0')).join('');
876
- console.warn(`[Worker] Blossom upload failed for ${hashHex}...:`, err instanceof Error ? err.message : err);
877
- respond({ type: 'blossomUploadError', hash: hashHex, error: getErrorMessage(err) });
878
- });
879
- }
880
- }
881
- async function handleHas(id, hash) {
882
- if (!store) {
883
- respond({ type: 'bool', id, value: false, error: 'Store not initialized' });
884
- return;
885
- }
886
- const exists = await store.has(hash);
887
- respond({ type: 'bool', id, value: exists });
888
- }
889
- async function handleDelete(id, hash) {
890
- if (!store) {
891
- respond({ type: 'bool', id, value: false, error: 'Store not initialized' });
892
- return;
893
- }
894
- const success = await store.delete(hash);
895
- respond({ type: 'bool', id, value: success });
896
- }
897
- // ============================================================================
898
- // Tree Handlers (high-level)
899
- // ============================================================================
900
- async function handleReadFile(id, cid) {
901
- if (!tree) {
902
- respond({ type: 'result', id, error: 'Tree not initialized' });
903
- return;
904
- }
905
- try {
906
- const data = await tree.readFile(cid);
907
- if (data) {
908
- respondWithTransfer({ type: 'result', id, data }, [data.buffer]);
909
- }
910
- else {
911
- respond({ type: 'result', id, error: 'File not found' });
912
- }
913
- }
914
- catch (err) {
915
- respond({ type: 'result', id, error: getErrorMessage(err) });
916
- }
917
- }
918
- async function handleReadFileRange(id, cid, start, end) {
919
- if (!tree) {
920
- respond({ type: 'result', id, error: 'Tree not initialized' });
921
- return;
922
- }
923
- try {
924
- const data = await tree.readFileRange(cid, start, end);
925
- if (data) {
926
- respondWithTransfer({ type: 'result', id, data }, [data.buffer]);
927
- }
928
- else {
929
- respond({ type: 'result', id, error: 'File not found' });
930
- }
931
- }
932
- catch (err) {
933
- respond({ type: 'result', id, error: getErrorMessage(err) });
934
- }
935
- }
936
- async function handleReadFileStream(id, cid) {
937
- if (!tree) {
938
- respond({ type: 'streamChunk', id, chunk: new Uint8Array(0), done: true });
939
- return;
940
- }
941
- try {
942
- for await (const chunk of tree.readFileStream(cid)) {
943
- // Send each chunk, transferring ownership
944
- respondWithTransfer({ type: 'streamChunk', id, chunk, done: false }, [chunk.buffer]);
945
- }
946
- // Signal completion
947
- respond({ type: 'streamChunk', id, chunk: new Uint8Array(0), done: true });
948
- }
949
- catch (err) {
950
- respond({ type: 'error', id, error: getErrorMessage(err) });
951
- }
952
- }
953
- async function handleWriteFile(id, parentCid, path, data) {
954
- if (!tree) {
955
- respond({ type: 'cid', id, error: 'Tree not initialized' });
956
- return;
957
- }
958
- try {
959
- // Parse path to get directory path and filename
960
- const parts = path.split('/').filter(Boolean);
961
- const fileName = parts.pop();
962
- if (!fileName) {
963
- respond({ type: 'cid', id, error: 'Invalid path' });
964
- return;
965
- }
966
- // First, create a file CID from the data
967
- const fileResult = await tree.putFile(data);
968
- const fileCid = fileResult.cid;
969
- // If no parent, just return the file CID (no directory structure)
970
- if (!parentCid) {
971
- respond({ type: 'cid', id, cid: fileCid });
972
- return;
973
- }
974
- // Add the file to the parent directory
975
- const newRootCid = await tree.setEntry(parentCid, parts, fileName, fileCid, data.length, 1 // LinkType.File
976
- );
977
- respond({ type: 'cid', id, cid: newRootCid });
978
- }
979
- catch (err) {
980
- respond({ type: 'cid', id, error: getErrorMessage(err) });
981
- }
982
- }
983
- async function handleDeleteFile(id, parentCid, path) {
984
- if (!tree) {
985
- respond({ type: 'cid', id, error: 'Tree not initialized' });
986
- return;
987
- }
988
- try {
989
- // Parse path to get directory path and filename
990
- const parts = path.split('/').filter(Boolean);
991
- const fileName = parts.pop();
992
- if (!fileName) {
993
- respond({ type: 'cid', id, error: 'Invalid path' });
994
- return;
995
- }
996
- const newCid = await tree.removeEntry(parentCid, parts, fileName);
997
- respond({ type: 'cid', id, cid: newCid });
998
- }
999
- catch (err) {
1000
- respond({ type: 'cid', id, error: getErrorMessage(err) });
1001
- }
1002
- }
1003
- async function handleListDir(id, cidArg) {
1004
- if (!tree) {
1005
- respond({ type: 'dirListing', id, error: 'Tree not initialized' });
1006
- return;
1007
- }
1008
- try {
1009
- const entries = await tree.listDirectory(cidArg);
1010
- const dirEntries = entries.map((entry) => ({
1011
- name: entry.name,
1012
- isDir: entry.type === 2, // LinkType.Dir
1013
- size: entry.size,
1014
- cid: entry.cid,
1015
- }));
1016
- respond({ type: 'dirListing', id, entries: dirEntries });
1017
- }
1018
- catch (err) {
1019
- respond({ type: 'dirListing', id, error: getErrorMessage(err) });
1020
- }
1021
- }
1022
- async function handleResolveRoot(id, npub, path) {
1023
- try {
1024
- const resolved = await resolveRootPath(tree, npub, path);
1025
- respond({ type: 'cid', id, cid: resolved ?? undefined });
1026
- }
1027
- catch (err) {
1028
- respond({ type: 'cid', id, error: getErrorMessage(err) });
1029
- }
1030
- }
1031
- async function handleSetTreeRootCache(id, npub, treeName, hash, key, visibility, labels, metadata) {
1032
- try {
1033
- await setCachedRoot(npub, treeName, { hash, key }, visibility, {
1034
- labels,
1035
- encryptedKey: metadata?.encryptedKey,
1036
- keyId: metadata?.keyId,
1037
- selfEncryptedKey: metadata?.selfEncryptedKey,
1038
- selfEncryptedLinkKey: metadata?.selfEncryptedLinkKey,
1039
- });
1040
- respond({ type: 'void', id });
1041
- }
1042
- catch (err) {
1043
- respond({ type: 'void', id, error: getErrorMessage(err) });
1044
- }
1045
- }
1046
- async function handleGetTreeRootInfo(id, npub, treeName) {
1047
- try {
1048
- const cached = await getCachedRootInfo(npub, treeName);
1049
- if (!cached) {
1050
- respond({ type: 'treeRootInfo', id });
1051
- return;
1052
- }
1053
- respond({
1054
- type: 'treeRootInfo',
1055
- id,
1056
- record: {
1057
- hash: cached.hash,
1058
- key: cached.key,
1059
- visibility: cached.visibility,
1060
- labels: cached.labels,
1061
- updatedAt: cached.updatedAt,
1062
- snapshotNhash: cached.snapshotNhash,
1063
- encryptedKey: cached.encryptedKey,
1064
- keyId: cached.keyId,
1065
- selfEncryptedKey: cached.selfEncryptedKey,
1066
- selfEncryptedLinkKey: cached.selfEncryptedLinkKey,
1067
- },
1068
- });
1069
- }
1070
- catch (err) {
1071
- respond({ type: 'treeRootInfo', id, error: getErrorMessage(err) });
1072
- }
1073
- }
1074
- async function handleMergeTreeRootKey(id, npub, treeName, hash, key) {
1075
- try {
1076
- const merged = await mergeCachedRootKey(npub, treeName, hash, key);
1077
- respond({ type: 'bool', id, value: merged });
1078
- }
1079
- catch (err) {
1080
- respond({ type: 'bool', id, value: false, error: getErrorMessage(err) });
1081
- }
1082
- }
1083
- async function handleSubscribeTreeRoots(id, pubkey) {
1084
- try {
1085
- const normalized = normalizePubkey(pubkey);
1086
- if (!normalized) {
1087
- respond({ type: 'void', id, error: 'Invalid pubkey' });
1088
- return;
1089
- }
1090
- const count = treeRootSubscriptionRefs.get(normalized) ?? 0;
1091
- treeRootSubscriptionRefs.set(normalized, count + 1);
1092
- if (count === 0) {
1093
- subscribeToTreeRoots(normalized);
1094
- }
1095
- respond({ type: 'void', id });
1096
- }
1097
- catch (err) {
1098
- respond({ type: 'void', id, error: getErrorMessage(err) });
1099
- }
1100
- }
1101
- async function handleUnsubscribeTreeRoots(id, pubkey) {
1102
- try {
1103
- const normalized = normalizePubkey(pubkey);
1104
- if (!normalized) {
1105
- respond({ type: 'void', id, error: 'Invalid pubkey' });
1106
- return;
1107
- }
1108
- const count = treeRootSubscriptionRefs.get(normalized);
1109
- if (!count) {
1110
- respond({ type: 'void', id });
1111
- return;
1112
- }
1113
- if (count <= 1) {
1114
- treeRootSubscriptionRefs.delete(normalized);
1115
- unsubscribeFromTreeRoots(normalized);
1116
- }
1117
- else {
1118
- treeRootSubscriptionRefs.set(normalized, count - 1);
1119
- }
1120
- respond({ type: 'void', id });
1121
- }
1122
- catch (err) {
1123
- respond({ type: 'void', id, error: getErrorMessage(err) });
1124
- }
1125
- }
1126
- function normalizePubkey(pubkey) {
1127
- if (pubkey.startsWith('npub1')) {
1128
- try {
1129
- const decoded = nip19.decode(pubkey);
1130
- if (decoded.type !== 'npub')
1131
- return null;
1132
- return decoded.data;
1133
- }
1134
- catch {
1135
- return null;
1136
- }
1137
- }
1138
- if (pubkey.length === 64) {
1139
- return pubkey;
1140
- }
1141
- return null;
1142
- }
1143
- // ============================================================================
1144
- // Nostr Handlers
1145
- // ============================================================================
1146
- async function handleSubscribe(id, filters) {
1147
- try {
1148
- // Use the request id as the subscription id
1149
- ndkSubscribe(id, filters);
1150
- respond({ type: 'void', id });
1151
- }
1152
- catch (err) {
1153
- respond({ type: 'void', id, error: getErrorMessage(err) });
1154
- }
1155
- }
1156
- async function handleUnsubscribe(id, subId) {
1157
- try {
1158
- ndkUnsubscribe(subId);
1159
- respond({ type: 'void', id });
1160
- }
1161
- catch (err) {
1162
- respond({ type: 'void', id, error: getErrorMessage(err) });
1163
- }
1164
- }
1165
- async function handlePublish(id, event) {
1166
- try {
1167
- await ndkPublish(event);
1168
- respond({ type: 'void', id });
1169
- }
1170
- catch (err) {
1171
- respond({ type: 'void', id, error: getErrorMessage(err) });
1172
- }
1173
- }
1174
- // ============================================================================
1175
- // Stats Handlers
1176
- // ============================================================================
1177
- async function handleGetPeerStats(id) {
1178
- if (!webrtc) {
1179
- respond({ type: 'peerStats', id, stats: [] });
1180
- return;
1181
- }
1182
- const controllerStats = webrtc.getPeerStats();
1183
- const stats = controllerStats.map(s => ({
1184
- peerId: s.peerId,
1185
- pubkey: s.pubkey,
1186
- connected: s.connected,
1187
- pool: s.pool,
1188
- requestsSent: s.requestsSent,
1189
- requestsReceived: s.requestsReceived,
1190
- responsesSent: s.responsesSent,
1191
- responsesReceived: s.responsesReceived,
1192
- bytesSent: s.bytesSent,
1193
- bytesReceived: s.bytesReceived,
1194
- forwardedRequests: s.forwardedRequests,
1195
- forwardedResolved: s.forwardedResolved,
1196
- forwardedSuppressed: s.forwardedSuppressed,
1197
- }));
1198
- respond({ type: 'peerStats', id, stats });
1199
- }
1200
- async function handleGetRelayStats(id) {
1201
- try {
1202
- const stats = getNdkRelayStats();
1203
- respond({ type: 'relayStats', id, stats });
1204
- }
1205
- catch {
1206
- respond({ type: 'relayStats', id, stats: [] });
1207
- }
1208
- }
1209
- async function handleGetStorageStats(id) {
1210
- try {
1211
- if (!store) {
1212
- respond({ type: 'storageStats', id, items: 0, bytes: 0 });
1213
- return;
1214
- }
1215
- const items = await store.count();
1216
- const bytes = await store.totalBytes();
1217
- respond({ type: 'storageStats', id, items, bytes });
1218
- }
1219
- catch (e) {
1220
- console.error('[Worker] getStorageStats error:', e);
1221
- respond({ type: 'storageStats', id, items: 0, bytes: 0 });
1222
- }
1223
- }
1224
- // ============================================================================
1225
- // SocialGraph Handlers
1226
- // ============================================================================
1227
- function handleInitSocialGraph(id, rootPubkey) {
1228
- try {
1229
- if (rootPubkey) {
1230
- socialGraph = new SocialGraph(rootPubkey);
1231
- }
1232
- const size = socialGraph.size();
1233
- respond({ type: 'socialGraphInit', id, version: socialGraphVersion, size });
1234
- }
1235
- catch (err) {
1236
- respond({ type: 'socialGraphInit', id, version: 0, size: 0, error: getErrorMessage(err) });
1237
- }
1238
- }
1239
- function handleSetSocialGraphRoot(id, pubkey) {
1240
- try {
1241
- socialGraph.setRoot(pubkey);
1242
- // Update followsSet for WebRTC peer classification
1243
- const follows = socialGraph.getFollowedByUser(pubkey);
1244
- followsSet = new Set(follows);
1245
- notifySocialGraphVersionUpdate();
1246
- respond({ type: 'void', id });
1247
- }
1248
- catch (err) {
1249
- respond({ type: 'void', id, error: getErrorMessage(err) });
1250
- }
1251
- }
1252
- function handleSocialGraphEvents(id, events) {
1253
- try {
1254
- let updated = false;
1255
- for (const event of events) {
1256
- socialGraph.handleEvent(event);
1257
- updated = true;
1258
- }
1259
- if (updated) {
1260
- notifySocialGraphVersionUpdate();
1261
- }
1262
- respond({ type: 'void', id });
1263
- }
1264
- catch (err) {
1265
- respond({ type: 'void', id, error: getErrorMessage(err) });
1266
- }
1267
- }
1268
- function handleGetFollowDistance(id, pubkey) {
1269
- try {
1270
- const distance = socialGraph.getFollowDistance(pubkey);
1271
- respond({ type: 'followDistance', id, distance });
1272
- }
1273
- catch (err) {
1274
- respond({ type: 'followDistance', id, distance: 1000, error: getErrorMessage(err) });
1275
- }
1276
- }
1277
- function handleIsFollowing(id, follower, followed) {
1278
- try {
1279
- const result = socialGraph.isFollowing(follower, followed);
1280
- respond({ type: 'isFollowingResult', id, result });
1281
- }
1282
- catch (err) {
1283
- respond({ type: 'isFollowingResult', id, result: false, error: getErrorMessage(err) });
1284
- }
1285
- }
1286
- function handleGetFollowsList(id, pubkey) {
1287
- try {
1288
- const follows = socialGraph.getFollowedByUser(pubkey);
1289
- respond({ type: 'pubkeyList', id, pubkeys: Array.from(follows) });
1290
- }
1291
- catch (err) {
1292
- respond({ type: 'pubkeyList', id, pubkeys: [], error: getErrorMessage(err) });
1293
- }
1294
- }
1295
- function handleGetFollowers(id, pubkey) {
1296
- try {
1297
- const followers = socialGraph.getFollowersByUser(pubkey);
1298
- respond({ type: 'pubkeyList', id, pubkeys: Array.from(followers) });
1299
- }
1300
- catch (err) {
1301
- respond({ type: 'pubkeyList', id, pubkeys: [], error: getErrorMessage(err) });
1302
- }
1303
- }
1304
- function handleGetFollowedByFriends(id, pubkey) {
1305
- try {
1306
- const friendsFollowing = socialGraph.getFollowedByFriends(pubkey);
1307
- respond({ type: 'pubkeyList', id, pubkeys: Array.from(friendsFollowing) });
1308
- }
1309
- catch (err) {
1310
- respond({ type: 'pubkeyList', id, pubkeys: [], error: getErrorMessage(err) });
1311
- }
1312
- }
1313
- function handleGetSocialGraphSize(id) {
1314
- try {
1315
- const size = socialGraph.size();
1316
- respond({ type: 'socialGraphSize', id, size });
1317
- }
1318
- catch (err) {
1319
- respond({ type: 'socialGraphSize', id, size: 0, error: getErrorMessage(err) });
1320
- }
1321
- }
1322
- /**
1323
- * Fetch a user's follow list when visiting their profile.
1324
- * Only fetches if we don't already have their follow list.
1325
- */
1326
- function handleFetchUserFollows(id, pubkey) {
1327
- try {
1328
- if (!pubkey || pubkey.length !== 64) {
1329
- respond({ type: 'ack', id });
1330
- return;
1331
- }
1332
- // Check if we already have their follow list
1333
- const existingCreatedAt = socialGraph.getFollowListCreatedAt(pubkey);
1334
- if (existingCreatedAt !== undefined) {
1335
- respond({ type: 'ack', id });
1336
- return;
1337
- }
1338
- // Subscribe to their kind:3 event
1339
- if (!subscribedPubkeys.has(pubkey)) {
1340
- subscribedPubkeys.add(pubkey);
1341
- ndkSubscribe(`socialgraph-profile-${pubkey.slice(0, 8)}`, [{
1342
- kinds: [KIND_CONTACTS],
1343
- authors: [pubkey],
1344
- }]);
1345
- }
1346
- respond({ type: 'ack', id });
1347
- }
1348
- catch (err) {
1349
- respond({ type: 'ack', id, error: getErrorMessage(err) });
1350
- }
1351
- }
1352
- // Track which pubkeys we've fetched followers for
1353
- const fetchedFollowersPubkeys = new Set();
1354
- /**
1355
- * Fetch users who follow a given pubkey (for profile views)
1356
- * Subscribes to recent kind:3 events with #p tag mentioning this user
1357
- */
1358
- function handleFetchUserFollowers(id, pubkey) {
1359
- try {
1360
- if (!pubkey || pubkey.length !== 64) {
1361
- respond({ type: 'ack', id });
1362
- return;
1363
- }
1364
- // Only fetch once per pubkey per session
1365
- if (fetchedFollowersPubkeys.has(pubkey)) {
1366
- respond({ type: 'ack', id });
1367
- return;
1368
- }
1369
- // Check if we already have some followers data (>10 means we likely have enough)
1370
- const existingFollowers = socialGraph.followerCount(pubkey);
1371
- if (existingFollowers > 10) {
1372
- respond({ type: 'ack', id });
1373
- return;
1374
- }
1375
- fetchedFollowersPubkeys.add(pubkey);
1376
- // Subscribe to recent kind:3 events that mention this user
1377
- // Limit to 100 to avoid overwhelming the connection
1378
- ndkSubscribe(`socialgraph-followers-${pubkey.slice(0, 8)}`, [{
1379
- kinds: [KIND_CONTACTS],
1380
- '#p': [pubkey],
1381
- limit: 100,
1382
- }]);
1383
- respond({ type: 'ack', id });
1384
- }
1385
- catch (err) {
1386
- respond({ type: 'ack', id, error: getErrorMessage(err) });
1387
- }
1388
- }
1389
- // ============================================================================
1390
- // SocialGraph Subscription
1391
- // ============================================================================
1392
- // Track latest event per pubkey to avoid processing old events (for social graph)
1393
- // Limited to 1000 entries to prevent memory leak from encountering many unique pubkeys
1394
- const socialGraphLatestByPubkey = new LRUCache(1000);
1395
- // Track which pubkeys we've already subscribed to for follow lists
1396
- const subscribedPubkeys = new Set();
1397
- // Maximum total users in social graph before stopping crawl
1398
- const MAX_SOCIAL_GRAPH_SIZE = 10000;
1399
- /**
1400
- * Handle incoming SocialGraph event (kind:3)
1401
- */
1402
- function handleSocialGraphEvent(event) {
1403
- const rootPubkey = socialGraph.getRoot();
1404
- const prevTime = socialGraphLatestByPubkey.get(event.pubkey) || 0;
1405
- if (event.created_at > prevTime) {
1406
- socialGraphLatestByPubkey.set(event.pubkey, event.created_at);
1407
- // allowUnknownAuthors=true lets us track followers of any user, not just those connected to root
1408
- socialGraph.handleEvent(event, true);
1409
- // If this is the root user's contact list, update followsSet for WebRTC
1410
- // and subscribe to kind:3 from their follows
1411
- if (event.pubkey === rootPubkey) {
1412
- try {
1413
- const follows = socialGraph.getFollowedByUser(rootPubkey);
1414
- followsSet = new Set(follows);
1415
- console.log('[Worker] Follows updated:', followsSet.size, 'pubkeys');
1416
- // Subscribe to kind:3 from root's follows (depth 1)
1417
- subscribeToFollowsContactLists(Array.from(follows), 1);
1418
- // If user has few follows, also use bootstrap user's follows
1419
- if (follows.size < 5 && rootPubkey !== DEFAULT_BOOTSTRAP_PUBKEY) {
1420
- const bootstrapFollows = socialGraph.getFollowedByUser(DEFAULT_BOOTSTRAP_PUBKEY);
1421
- if (bootstrapFollows.size > 0) {
1422
- subscribeToFollowsContactLists(Array.from(bootstrapFollows), 1);
1423
- }
1424
- }
1425
- }
1426
- catch (err) {
1427
- console.warn('[Worker] Error getting follows for root:', err);
1428
- }
1429
- // Broadcast hello so peers can re-classify with updated follows
1430
- webrtc?.broadcastHello();
1431
- }
1432
- else {
1433
- // For non-root users at depth 1, subscribe to their follows (depth 2)
1434
- // But only if graph isn't already at max size
1435
- if (socialGraph.size() < MAX_SOCIAL_GRAPH_SIZE) {
1436
- try {
1437
- const distance = socialGraph.getFollowDistance(event.pubkey);
1438
- if (distance === 1) {
1439
- const theirFollows = socialGraph.getFollowedByUser(event.pubkey);
1440
- subscribeToFollowsContactLists(Array.from(theirFollows), 2);
1441
- }
1442
- }
1443
- catch (err) {
1444
- console.warn('[Worker] Error getting follows for user:', event.pubkey.slice(0, 16), err);
1445
- }
1446
- }
1447
- }
1448
- notifySocialGraphVersionUpdate();
1449
- }
1450
- }
1451
- /**
1452
- * Subscribe to kind:3 contact lists from a set of pubkeys
1453
- * Only subscribes for users whose follow list we don't already have (getFollowedByUser returns empty)
1454
- */
1455
- function subscribeToFollowsContactLists(pubkeys, depth) {
1456
- // Don't subscribe while still loading from IndexedDB
1457
- if (socialGraphLoading) {
1458
- return;
1459
- }
1460
- // Stop crawling if graph is already large enough
1461
- const currentSize = socialGraph.size();
1462
- if (currentSize >= MAX_SOCIAL_GRAPH_SIZE) {
1463
- return;
1464
- }
1465
- const remainingCapacity = MAX_SOCIAL_GRAPH_SIZE - currentSize;
1466
- const newPubkeys = [];
1467
- for (const pk of pubkeys) {
1468
- if (subscribedPubkeys.has(pk))
1469
- continue;
1470
- if (newPubkeys.length >= remainingCapacity)
1471
- break;
1472
- // Only fetch if we don't have their follow list (like iris-client does)
1473
- try {
1474
- const existingFollows = socialGraph.getFollowedByUser(pk);
1475
- if (existingFollows.size > 0) {
1476
- subscribedPubkeys.add(pk); // Mark as "done" so we don't check again
1477
- continue;
1478
- }
1479
- }
1480
- catch {
1481
- // Invalid pubkey in graph, skip
1482
- continue;
1483
- }
1484
- subscribedPubkeys.add(pk);
1485
- newPubkeys.push(pk);
1486
- }
1487
- if (newPubkeys.length > 0) {
1488
- console.log(`[Worker] Subscribing to kind:3 from ${newPubkeys.length} missing pubkeys at depth ${depth} (graph size: ${currentSize})`);
1489
- // Subscribe in batches to avoid overwhelming relays
1490
- const batchSize = 50;
1491
- for (let i = 0; i < newPubkeys.length; i += batchSize) {
1492
- const batch = newPubkeys.slice(i, i + batchSize);
1493
- ndkSubscribe(`socialgraph-depth${depth}-${i}`, [{
1494
- kinds: [KIND_CONTACTS],
1495
- authors: batch,
1496
- }]);
1497
- }
1498
- }
1499
- }
1500
- /**
1501
- * Subscribe to kind:3 contact list events for social graph
1502
- */
1503
- function setupSocialGraphSubscription(rootPubkey) {
1504
- if (!rootPubkey || rootPubkey.length !== 64) {
1505
- console.warn('[Worker] Invalid pubkey for social graph subscription:', rootPubkey);
1506
- return;
1507
- }
1508
- // Clear previous subscriptions tracking when root changes
1509
- subscribedPubkeys.clear();
1510
- subscribedPubkeys.add(rootPubkey);
1511
- // NOTE: Don't call setOnEvent here - use the unified handler set up in handleInit
1512
- // Check if we already have the root user's follows
1513
- const rootFollows = socialGraph.getFollowedByUser(rootPubkey);
1514
- const hasRootFollows = rootFollows.size > 0;
1515
- // Subscribe to root user's contact list if we don't have it
1516
- if (!hasRootFollows) {
1517
- const authors = [rootPubkey];
1518
- if (rootPubkey !== DEFAULT_BOOTSTRAP_PUBKEY) {
1519
- authors.push(DEFAULT_BOOTSTRAP_PUBKEY);
1520
- subscribedPubkeys.add(DEFAULT_BOOTSTRAP_PUBKEY);
1521
- }
1522
- ndkSubscribe('socialgraph-contacts', [{
1523
- kinds: [KIND_CONTACTS],
1524
- authors,
1525
- }]);
1526
- }
1527
- console.log('[Worker] Subscribed to kind:3 events for social graph');
1528
- }
1529
- //# sourceMappingURL=worker.js.map