Skip to content

Commit ebc8a6d

Browse files
committed
Added back registry removed by mistake
Signed-off-by: Robert Gogete <gogeterobert@yahoo.com>
1 parent e05595c commit ebc8a6d

File tree

1 file changed

+327
-0
lines changed

1 file changed

+327
-0
lines changed

src/registry/gun-registry.ts

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import Gun from "gun";
2+
import "gun/lib/webrtc.js";
3+
import { HostCapabilities } from "../interfaces";
4+
5+
export interface GunRegistryOptions {
6+
peers?: string[];
7+
namespace?: string;
8+
/**
9+
* When true, registration will aggressively override any existing values
10+
* for the same storeId by first clearing known fields before writing fresh ones.
11+
* This helps when restarting a host with a fixed storeId and avoids stale data lingering.
12+
*/
13+
forceOverride?: boolean;
14+
/**
15+
* Optional delay (ms) between clearing old fields and writing fresh values,
16+
* giving the network a moment to propagate deletes. Defaults to 150ms.
17+
*/
18+
overrideDelayMs?: number;
19+
/**
20+
* WebRTC configuration for peer-to-peer connections
21+
* When enabled, mesh networking is automatic
22+
*/
23+
webrtc?: {
24+
iceServers?: Array<{ urls: string | string[] }>;
25+
};
26+
/**
27+
* Enable local storage for offline capabilities
28+
*/
29+
localStorage?: boolean;
30+
}
31+
32+
interface GunInstance {
33+
get: (key: string) => GunChain;
34+
}
35+
36+
interface GunChain {
37+
get: (key: string) => GunChain;
38+
put: (data: Record<string, unknown> | null) => void;
39+
once: (callback: (data: Record<string, unknown>) => void) => void;
40+
on: (callback: (data: Record<string, unknown>) => void) => void;
41+
}
42+
43+
export class GunRegistry {
44+
private gun: GunInstance | null = null;
45+
private options: GunRegistryOptions;
46+
private isGunAvailable: boolean = false;
47+
48+
constructor(options: GunRegistryOptions = {}) {
49+
this.options = {
50+
peers: options.peers || ["http://nostalgiagame.go.ro:30878/gun"],
51+
namespace: options.namespace || "dig-nat-tools",
52+
forceOverride: options.forceOverride ?? true,
53+
overrideDelayMs: options.overrideDelayMs ?? 150,
54+
// webrtc: options.webrtc || {
55+
// iceServers: [
56+
// { urls: 'stun:stun.l.google.com:19302' },
57+
// { urls: 'stun:stun1.l.google.com:19302' },
58+
// { urls: 'stun:stun2.l.google.com:19302' }
59+
// ]
60+
// },
61+
// localStorage: options.localStorage ?? true,
62+
};
63+
64+
this.initializeGun();
65+
}
66+
67+
private initializeGun(): void {
68+
try {
69+
this.gun = Gun({
70+
peers: this.options.peers,
71+
file: undefined, // Disable local file storage (must be string or undefined)
72+
localStorage: false, // Use option or default to false
73+
radisk: false, // Disable radisk storage
74+
axe: false,
75+
});
76+
this.isGunAvailable = true;
77+
console.log("Gun.js registry initialized with WebRTC and mesh networking");
78+
console.log(`🔧 WebRTC enabled with ${this.options.webrtc?.iceServers?.length || 0} ICE servers`);
79+
console.log(`🔧 Mesh networking: enabled (automatic with WebRTC)`);
80+
console.log(`🔧 Local storage: ${this.options.localStorage ? 'enabled' : 'disabled'}`);
81+
} catch {
82+
console.warn("Gun.js not available, peer discovery will not work");
83+
this.isGunAvailable = false;
84+
}
85+
}
86+
87+
public isAvailable(): boolean {
88+
return this.isGunAvailable;
89+
}
90+
91+
public async register(capabilities: HostCapabilities): Promise<void> {
92+
if (!this.isGunAvailable || !this.gun) {
93+
throw new Error("Gun.js registry not available");
94+
}
95+
96+
if (!capabilities.storeId) {
97+
throw new Error("StoreId is required for registration");
98+
}
99+
100+
console.log(`🔧 [GunRegistry] Starting registration for host: ${capabilities.storeId}`);
101+
console.log(`🔧 [GunRegistry] Using namespace: ${this.options.namespace}`);
102+
console.log(`🔧 [GunRegistry] Peers configured: ${JSON.stringify(this.options.peers)}`);
103+
104+
// Create a flattened structure that Gun.js can handle
105+
const flatEntry = {
106+
storeId: capabilities.storeId,
107+
lastSeen: Date.now(),
108+
directHttp_available: capabilities.directHttp?.available || false,
109+
directHttp_ip: capabilities.directHttp?.ip || "",
110+
directHttp_port: capabilities.directHttp?.port || 0,
111+
webTorrent_available: capabilities.webTorrent?.available || false,
112+
webTorrent_magnetUris: capabilities.webTorrent?.magnetUris ? JSON.stringify(capabilities.webTorrent.magnetUris) : "[]",
113+
};
114+
115+
console.log(`🔧 [GunRegistry] Registration data:`, JSON.stringify(flatEntry, null, 2));
116+
117+
try {
118+
const hostRef = this.gun
119+
.get(this.options.namespace!)
120+
.get(capabilities.storeId);
121+
122+
// Optionally clear known fields to ensure our fresh values win on restart
123+
if (this.options.forceOverride) {
124+
try {
125+
const fieldsToClear = [
126+
"directHttp_available",
127+
"directHttp_ip",
128+
"directHttp_port",
129+
"webTorrent_available",
130+
"webTorrent_magnetUris",
131+
"externalIp",
132+
"port",
133+
"lastSeen",
134+
"storeId",
135+
];
136+
fieldsToClear.forEach((k) => hostRef.get(k).put(null));
137+
const delay = Math.max(0, this.options.overrideDelayMs || 0);
138+
if (delay > 0) {
139+
await new Promise((r) => setTimeout(r, delay));
140+
}
141+
} catch (e) {
142+
console.warn("⚠️ [GunRegistry] Failed clearing existing fields before override:", e);
143+
}
144+
}
145+
146+
// Store in Gun.js
147+
hostRef.put(flatEntry);
148+
149+
console.log(`✅ [GunRegistry] Successfully registered host ${capabilities.storeId} in Gun.js registry`);
150+
151+
} catch (error) {
152+
console.error(`❌ [GunRegistry] Registration failed for ${capabilities.storeId}:`, error);
153+
throw error;
154+
}
155+
}
156+
157+
public async findPeer(storeId: string): Promise<HostCapabilities | null> {
158+
if (!this.isGunAvailable || !this.gun) {
159+
throw new Error("Gun.js registry not available");
160+
}
161+
162+
console.log(`🔍 [GunRegistry] Looking for specific peer: ${storeId}`);
163+
164+
return new Promise((resolve) => {
165+
const timeout = setTimeout(() => {
166+
console.log(`⏰ [GunRegistry] Timeout searching for peer ${storeId}`);
167+
resolve(null);
168+
}, 10000); // 10 second timeout
169+
170+
this.gun!.get(this.options.namespace!)
171+
.get(storeId)
172+
.once((data: Record<string, unknown>) => {
173+
clearTimeout(timeout);
174+
console.log(`📊 [GunRegistry] Peer ${storeId} data:`, data);
175+
176+
if (data && data.storeId === storeId) {
177+
// Filter out stale entries (older than 5 minutes)
178+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
179+
const lastSeen = data.lastSeen as number;
180+
181+
console.log(`🕒 [GunRegistry] Peer ${storeId} last seen: ${lastSeen ? new Date(lastSeen).toLocaleString() : 'never'}`);
182+
183+
if (lastSeen && lastSeen > fiveMinutesAgo) {
184+
console.log(`✅ [GunRegistry] Peer ${storeId} is fresh`);
185+
186+
// Reconstruct the capabilities object
187+
const capabilities: HostCapabilities = {
188+
storeId: data.storeId as string,
189+
directHttp: data.directHttp_available ? {
190+
available: data.directHttp_available as boolean,
191+
ip: data.directHttp_ip as string,
192+
port: data.directHttp_port as number
193+
} : undefined,
194+
webTorrent: data.webTorrent_available ? {
195+
available: data.webTorrent_available as boolean,
196+
magnetUris: data.webTorrent_magnetUris ?
197+
JSON.parse(data.webTorrent_magnetUris as string) : []
198+
} : undefined,
199+
// Legacy fields for backward compatibility
200+
externalIp: data.externalIp as string,
201+
port: data.port as number,
202+
lastSeen: lastSeen
203+
};
204+
205+
resolve(capabilities);
206+
} else {
207+
console.log(`⏰ [GunRegistry] Peer ${storeId} is stale`);
208+
resolve(null);
209+
}
210+
} else {
211+
console.log(`❌ [GunRegistry] Peer ${storeId} not found or invalid data`);
212+
resolve(null);
213+
}
214+
});
215+
});
216+
}
217+
218+
public async findAvailablePeers(): Promise<HostCapabilities[]> {
219+
if (!this.isGunAvailable || !this.gun) {
220+
throw new Error("Gun.js registry not available");
221+
}
222+
223+
console.log(`🔍 [GunRegistry] Searching for peers in namespace: ${this.options.namespace}`);
224+
console.log(`🔍 [GunRegistry] Connected to peers: ${JSON.stringify(this.options.peers)}`);
225+
226+
return new Promise((resolve) => {
227+
const peers: HostCapabilities[] = [];
228+
const timeout = setTimeout(() => {
229+
console.log(`⏰ [GunRegistry] Search timeout reached, found ${peers.length} peers`);
230+
resolve(peers);
231+
}, 30000); // Increase timeout to 10 seconds
232+
233+
this.gun!.get(this.options.namespace!)
234+
.once(async (data: Record<string, unknown>) => {
235+
console.log(`📊 [GunRegistry] Raw hosts data received:`, data);
236+
237+
if (data) {
238+
const allKeys = Object.keys(data);
239+
console.log(`🔑 [GunRegistry] All keys in hosts data:`, allKeys);
240+
241+
const hostKeys = allKeys.filter(key => key !== "_");
242+
console.log(`🏠 [GunRegistry] Host keys (excluding Gun.js metadata):`, hostKeys);
243+
244+
// Process each host key by fetching the actual data
245+
let processedHosts = 0;
246+
const totalHosts = hostKeys.length;
247+
248+
if (totalHosts === 0) {
249+
console.log(`❌ [GunRegistry] No hosts found in namespace ${this.options.namespace}`);
250+
clearTimeout(timeout);
251+
resolve(peers);
252+
return;
253+
}
254+
255+
for (const hostKey of hostKeys) {
256+
console.log(`🔍 [GunRegistry] Fetching detailed data for host: ${hostKey}`);
257+
258+
// Fetch the actual host data by following the reference
259+
this.gun!.get(this.options.namespace!)
260+
.get(hostKey)
261+
.once((hostData: Record<string, unknown>) => {
262+
processedHosts++;
263+
console.log(`� [GunRegistry] Host ${hostKey} detailed data:`, hostData);
264+
265+
if (hostData && hostData.storeId && typeof hostData.storeId === 'string') {
266+
// Filter out stale entries (older than 5 minutes)
267+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
268+
const lastSeen = hostData.lastSeen as number;
269+
270+
console.log(`🕒 [GunRegistry] Host ${hostKey} last seen: ${lastSeen ? new Date(lastSeen).toLocaleString() : 'never'}`);
271+
console.log(`🕒 [GunRegistry] Five minutes ago: ${new Date(fiveMinutesAgo).toLocaleString()}`);
272+
273+
if (lastSeen && lastSeen > fiveMinutesAgo) {
274+
console.log(`✅ [GunRegistry] Host ${hostKey} is fresh, adding to results`);
275+
276+
// Reconstruct the capabilities object
277+
const capabilities: HostCapabilities = {
278+
storeId: hostData.storeId as string,
279+
directHttp: hostData.directHttp_available ? {
280+
available: hostData.directHttp_available as boolean,
281+
ip: hostData.directHttp_ip as string,
282+
port: hostData.directHttp_port as number
283+
} : undefined,
284+
webTorrent: hostData.webTorrent_available ? {
285+
available: hostData.webTorrent_available as boolean,
286+
magnetUris: hostData.webTorrent_magnetUris ?
287+
JSON.parse(hostData.webTorrent_magnetUris as string) : []
288+
} : undefined,
289+
// Legacy fields for backward compatibility
290+
externalIp: hostData.externalIp as string,
291+
port: hostData.port as number,
292+
lastSeen: lastSeen
293+
};
294+
295+
peers.push(capabilities);
296+
console.log(`✅ [GunRegistry] Added peer: ${capabilities.storeId}`);
297+
} else {
298+
console.log(`⏰ [GunRegistry] Host ${hostKey} is stale, skipping`);
299+
}
300+
} else {
301+
console.log(`❌ [GunRegistry] Host ${hostKey} has invalid data structure:`, {
302+
hasData: !!hostData,
303+
hasStoreId: !!(hostData && hostData.storeId),
304+
storeIdType: hostData && hostData.storeId ? typeof hostData.storeId : 'undefined'
305+
});
306+
}
307+
308+
// Check if we've processed all hosts
309+
if (processedHosts >= totalHosts) {
310+
clearTimeout(timeout);
311+
console.log(`📋 [GunRegistry] Final peer list: ${peers.length} peers found`);
312+
peers.forEach((peer, index) => {
313+
console.log(` ${index + 1}. ${peer.storeId} - HTTP: ${peer.directHttp?.available || false}, WebTorrent: ${peer.webTorrent?.available || false}`);
314+
});
315+
resolve(peers);
316+
}
317+
});
318+
}
319+
} else {
320+
console.log(`❌ [GunRegistry] No hosts data found in namespace ${this.options.namespace}`);
321+
clearTimeout(timeout);
322+
resolve(peers);
323+
}
324+
});
325+
});
326+
}
327+
}

0 commit comments

Comments
 (0)