@hashtree/worker 0.2.1 → 0.2.3
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.
- package/LICENSE +21 -0
- package/dist/app-runtime.d.ts +60 -0
- package/dist/app-runtime.d.ts.map +1 -0
- package/dist/app-runtime.js +271 -0
- package/dist/app-runtime.js.map +1 -0
- package/dist/capabilities/blossomBandwidthTracker.d.ts +26 -0
- package/dist/capabilities/blossomBandwidthTracker.d.ts.map +1 -0
- package/dist/capabilities/blossomBandwidthTracker.js +53 -0
- package/dist/capabilities/blossomBandwidthTracker.js.map +1 -0
- package/dist/capabilities/blossomTransport.d.ts +22 -0
- package/dist/capabilities/blossomTransport.d.ts.map +1 -0
- package/dist/capabilities/blossomTransport.js +139 -0
- package/dist/capabilities/blossomTransport.js.map +1 -0
- package/dist/capabilities/connectivity.d.ts +3 -0
- package/dist/capabilities/connectivity.d.ts.map +1 -0
- package/dist/capabilities/connectivity.js +49 -0
- package/dist/capabilities/connectivity.js.map +1 -0
- package/dist/capabilities/idbStorage.d.ts +25 -0
- package/dist/capabilities/idbStorage.d.ts.map +1 -0
- package/dist/capabilities/idbStorage.js +73 -0
- package/dist/capabilities/idbStorage.js.map +1 -0
- package/dist/capabilities/meshRouterStore.d.ts +71 -0
- package/dist/capabilities/meshRouterStore.d.ts.map +1 -0
- package/dist/capabilities/meshRouterStore.js +316 -0
- package/dist/capabilities/meshRouterStore.js.map +1 -0
- package/dist/capabilities/rootResolver.d.ts +10 -0
- package/dist/capabilities/rootResolver.d.ts.map +1 -0
- package/dist/capabilities/rootResolver.js +392 -0
- package/dist/capabilities/rootResolver.js.map +1 -0
- package/dist/client-id.d.ts +18 -0
- package/dist/client-id.d.ts.map +1 -0
- package/dist/client-id.js +98 -0
- package/dist/client-id.js.map +1 -0
- package/dist/client.d.ts +61 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +417 -0
- package/dist/client.js.map +1 -0
- package/dist/entry.d.ts +2 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +3 -0
- package/dist/entry.js.map +1 -0
- package/dist/htree-path.d.ts +13 -0
- package/dist/htree-path.d.ts.map +1 -0
- package/dist/htree-path.js +38 -0
- package/dist/htree-path.js.map +1 -0
- package/dist/htree-url.d.ts +22 -0
- package/dist/htree-url.d.ts.map +1 -0
- package/dist/htree-url.js +118 -0
- package/dist/htree-url.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/mediaStreaming.d.ts +7 -0
- package/dist/mediaStreaming.d.ts.map +1 -0
- package/dist/mediaStreaming.js +48 -0
- package/dist/mediaStreaming.js.map +1 -0
- package/dist/p2p/boundedQueue.d.ts +79 -0
- package/dist/p2p/boundedQueue.d.ts.map +1 -0
- package/dist/p2p/boundedQueue.js +134 -0
- package/dist/p2p/boundedQueue.js.map +1 -0
- package/dist/p2p/errorMessage.d.ts +5 -0
- package/dist/p2p/errorMessage.d.ts.map +1 -0
- package/dist/p2p/errorMessage.js +7 -0
- package/dist/p2p/errorMessage.js.map +1 -0
- package/dist/p2p/index.d.ts +8 -0
- package/dist/p2p/index.d.ts.map +1 -0
- package/dist/p2p/index.js +6 -0
- package/dist/p2p/index.js.map +1 -0
- package/dist/p2p/lruCache.d.ts +26 -0
- package/dist/p2p/lruCache.d.ts.map +1 -0
- package/dist/p2p/lruCache.js +65 -0
- package/dist/p2p/lruCache.js.map +1 -0
- package/dist/p2p/meshQueryRouter.d.ts +57 -0
- package/dist/p2p/meshQueryRouter.d.ts.map +1 -0
- package/dist/p2p/meshQueryRouter.js +264 -0
- package/dist/p2p/meshQueryRouter.js.map +1 -0
- package/dist/p2p/protocol.d.ts +10 -0
- package/dist/p2p/protocol.d.ts.map +1 -0
- package/dist/p2p/protocol.js +2 -0
- package/dist/p2p/protocol.js.map +1 -0
- package/dist/p2p/queryForwardingMachine.d.ts +46 -0
- package/dist/p2p/queryForwardingMachine.d.ts.map +1 -0
- package/dist/p2p/queryForwardingMachine.js +144 -0
- package/dist/p2p/queryForwardingMachine.js.map +1 -0
- package/dist/p2p/signaling.d.ts +63 -0
- package/dist/p2p/signaling.d.ts.map +1 -0
- package/dist/p2p/signaling.js +185 -0
- package/dist/p2p/signaling.js.map +1 -0
- package/dist/p2p/uploadRateLimiter.d.ts +21 -0
- package/dist/p2p/uploadRateLimiter.d.ts.map +1 -0
- package/dist/p2p/uploadRateLimiter.js +62 -0
- package/dist/p2p/uploadRateLimiter.js.map +1 -0
- package/dist/p2p/webrtcController.d.ts +176 -0
- package/dist/p2p/webrtcController.d.ts.map +1 -0
- package/dist/p2p/webrtcController.js +938 -0
- package/dist/p2p/webrtcController.js.map +1 -0
- package/dist/p2p/webrtcProxy.d.ts +62 -0
- package/dist/p2p/webrtcProxy.d.ts.map +1 -0
- package/dist/p2p/webrtcProxy.js +447 -0
- package/dist/p2p/webrtcProxy.js.map +1 -0
- package/dist/privacyGuards.d.ts +14 -0
- package/dist/privacyGuards.d.ts.map +1 -0
- package/dist/privacyGuards.js +27 -0
- package/dist/privacyGuards.js.map +1 -0
- package/dist/protocol.d.ts +225 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +2 -0
- package/dist/protocol.js.map +1 -0
- package/dist/relay/identity.d.ts +36 -0
- package/dist/relay/identity.d.ts.map +1 -0
- package/dist/relay/identity.js +78 -0
- package/dist/relay/identity.js.map +1 -0
- package/dist/relay/mediaHandler.d.ts +64 -0
- package/dist/relay/mediaHandler.d.ts.map +1 -0
- package/dist/relay/mediaHandler.js +1285 -0
- package/dist/relay/mediaHandler.js.map +1 -0
- package/dist/relay/ndk.d.ts +96 -0
- package/dist/relay/ndk.d.ts.map +1 -0
- package/dist/relay/ndk.js +502 -0
- package/dist/relay/ndk.js.map +1 -0
- package/dist/relay/nostr-wasm.d.ts +14 -0
- package/dist/relay/nostr-wasm.d.ts.map +1 -0
- package/dist/relay/nostr-wasm.js +246 -0
- package/dist/relay/nostr-wasm.js.map +1 -0
- package/dist/relay/nostr.d.ts +60 -0
- package/dist/relay/nostr.d.ts.map +1 -0
- package/dist/relay/nostr.js +207 -0
- package/dist/relay/nostr.js.map +1 -0
- package/dist/relay/protocol.d.ts +592 -0
- package/dist/relay/protocol.d.ts.map +1 -0
- package/dist/relay/protocol.js +16 -0
- package/dist/relay/protocol.js.map +1 -0
- package/dist/relay/publicAssetUrl.d.ts +6 -0
- package/dist/relay/publicAssetUrl.d.ts.map +1 -0
- package/dist/relay/publicAssetUrl.js +14 -0
- package/dist/relay/publicAssetUrl.js.map +1 -0
- package/dist/relay/rootPathResolver.d.ts +9 -0
- package/dist/relay/rootPathResolver.d.ts.map +1 -0
- package/dist/relay/rootPathResolver.js +32 -0
- package/dist/relay/rootPathResolver.js.map +1 -0
- package/dist/relay/signing.d.ts +50 -0
- package/dist/relay/signing.d.ts.map +1 -0
- package/dist/relay/signing.js +299 -0
- package/dist/relay/signing.js.map +1 -0
- package/dist/relay/treeRootCache.d.ts +86 -0
- package/dist/relay/treeRootCache.d.ts.map +1 -0
- package/dist/relay/treeRootCache.js +269 -0
- package/dist/relay/treeRootCache.js.map +1 -0
- package/dist/relay/treeRootSubscription.d.ts +55 -0
- package/dist/relay/treeRootSubscription.d.ts.map +1 -0
- package/dist/relay/treeRootSubscription.js +478 -0
- package/dist/relay/treeRootSubscription.js.map +1 -0
- package/dist/relay/utils/constants.d.ts +76 -0
- package/dist/relay/utils/constants.d.ts.map +1 -0
- package/dist/relay/utils/constants.js +113 -0
- package/dist/relay/utils/constants.js.map +1 -0
- package/dist/relay/utils/errorMessage.d.ts +5 -0
- package/dist/relay/utils/errorMessage.d.ts.map +1 -0
- package/dist/relay/utils/errorMessage.js +8 -0
- package/dist/relay/utils/errorMessage.js.map +1 -0
- package/dist/relay/utils/lruCache.d.ts +26 -0
- package/dist/relay/utils/lruCache.d.ts.map +1 -0
- package/dist/relay/utils/lruCache.js +66 -0
- package/dist/relay/utils/lruCache.js.map +1 -0
- package/dist/relay/webrtc.d.ts +2 -0
- package/dist/relay/webrtc.d.ts.map +1 -0
- package/dist/relay/webrtc.js +3 -0
- package/dist/relay/webrtc.js.map +1 -0
- package/dist/relay/webrtcSignaling.d.ts +37 -0
- package/dist/relay/webrtcSignaling.d.ts.map +1 -0
- package/dist/relay/webrtcSignaling.js +86 -0
- package/dist/relay/webrtcSignaling.js.map +1 -0
- package/dist/relay/worker.d.ts +12 -0
- package/dist/relay/worker.d.ts.map +1 -0
- package/dist/relay/worker.js +1540 -0
- package/dist/relay/worker.js.map +1 -0
- package/dist/relay-client.d.ts +31 -0
- package/dist/relay-client.d.ts.map +1 -0
- package/dist/relay-client.js +197 -0
- package/dist/relay-client.js.map +1 -0
- package/dist/relay-entry.d.ts +2 -0
- package/dist/relay-entry.d.ts.map +1 -0
- package/dist/relay-entry.js +2 -0
- package/dist/relay-entry.js.map +1 -0
- package/dist/runtime-network.d.ts +23 -0
- package/dist/runtime-network.d.ts.map +1 -0
- package/dist/runtime-network.js +105 -0
- package/dist/runtime-network.js.map +1 -0
- package/dist/runtime.d.ts +24 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +126 -0
- package/dist/runtime.js.map +1 -0
- package/dist/transferableBytes.d.ts +2 -0
- package/dist/transferableBytes.d.ts.map +1 -0
- package/dist/transferableBytes.js +6 -0
- package/dist/transferableBytes.js.map +1 -0
- package/dist/tree-root.d.ts +201 -0
- package/dist/tree-root.d.ts.map +1 -0
- package/dist/tree-root.js +632 -0
- package/dist/tree-root.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/worker.d.ts +9 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +793 -0
- package/dist/worker.js.map +1 -0
- package/package.json +14 -14
- package/src/capabilities/blossomTransport.ts +1 -1
- package/src/worker.ts +2 -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
|