@hashtree/worker 0.2.1 → 0.2.2

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