From 3edd9280a5ac03660a7ed865d7fd1c5c511e82c5 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Oct 2024 11:07:53 +0200 Subject: [PATCH] Create RoboPool --- frontend/src/models/Federation.model.ts | 64 ++++------- frontend/src/services/RoboPool/index.ts | 101 ++++++++++++++++++ .../RobohashGenerator.ts | 2 +- 3 files changed, 125 insertions(+), 42 deletions(-) create mode 100644 frontend/src/services/RoboPool/index.ts diff --git a/frontend/src/models/Federation.model.ts b/frontend/src/models/Federation.model.ts index ee20da45..23a6e67e 100644 --- a/frontend/src/models/Federation.model.ts +++ b/frontend/src/models/Federation.model.ts @@ -14,6 +14,7 @@ import { coordinatorDefaultValues } from './Coordinator.model'; import { updateExchangeInfo } from './Exchange.model'; import eventToPublicOrder from '../utils/nostr'; import { SubCloser } from 'nostr-tools/lib/types/pool'; +import RoboPool from '../services/RoboPool'; type FederationHooks = 'onFederationUpdate'; @@ -60,11 +61,7 @@ export class Federation { if (tesnetHost) settings.network = 'testnet'; this.connection = null; - const relays = [ - 'ws://4t4jxmivv6uqej6xzx2jx3fxh75gtt65v3szjoqmc4ugdlhipzdat6yd.onion/nostr', - // 'ws://ngdk7ocdzmz5kzsysa3om6du7ycj2evxp2f2olfkyq37htx3gllwp2yd.onion/nostr' - ]; - this.relayPool.trustedRelayURLs = new Set(relays); + this.roboPool = new RoboPool(settings, origin); } public coordinators: Record; @@ -75,57 +72,42 @@ export class Federation { public hooks: Record void>>; - public relayPool: SimplePool = new SimplePool(); - public relaySubscriptions: SubCloser[] = []; + public roboPool: RoboPool; setConnection = (settings: Settings): void => { this.connection = settings.connection; if (this.connection === 'nostr') { - this.connectNostr(settings); + this.roboPool.connect(); + this.loadBookNostr(); } else { - this.relayPool.close(Array.from(this.relayPool.trustedRelayURLs)); + this.roboPool.close(); this.loadBook(); } }; - connectNostr = (settings: Settings): void => { + loadBookNostr = (): void => { this.loading = true; this.book = {}; - this.exchange.loadingCache = this.relayPool.trustedRelayURLs.size; + this.exchange.loadingCache = this.roboPool.relays.length; - const authors = Object.values(defaultFederation) - .map((f) => f.nostrHexPubkey) - .filter((item) => item !== undefined); - - const sub = this.relayPool.subscribeMany( - Array.from(this.relayPool.trustedRelayURLs), - [ - { - authors, - kinds: [38383], - '#n': [settings.network], - }, - ], - { - onevent: (event) => { - const { dTag, publicOrder } = eventToPublicOrder(event); - if (publicOrder) { - this.book[dTag] = publicOrder; - } else { - delete this.book[dTag]; - } - }, - oneose: () => { - this.exchange.loadingCache = this.exchange.loadingCache - 1; - this.loading = this.exchange.loadingCache > 0 && this.exchange.loadingCoordinators > 0; - this.updateExchange(); - this.triggerHook('onFederationUpdate'); - }, + this.roboPool.subscribeBook({ + onevent: (event) => { + const { dTag, publicOrder } = eventToPublicOrder(event); + if (publicOrder) { + this.book[dTag] = publicOrder; + } else { + delete this.book[dTag]; + } }, - ); - this.relaySubscriptions.push(sub); + oneose: () => { + this.exchange.loadingCache = this.exchange.loadingCache - 1; + this.loading = this.exchange.loadingCache > 0 && this.exchange.loadingCoordinators > 0; + this.updateExchange(); + this.triggerHook('onFederationUpdate'); + }, + }); }; addCoordinator = ( diff --git a/frontend/src/services/RoboPool/index.ts b/frontend/src/services/RoboPool/index.ts new file mode 100644 index 00000000..761c571b --- /dev/null +++ b/frontend/src/services/RoboPool/index.ts @@ -0,0 +1,101 @@ +import { Event } from 'nostr-tools'; +import { Settings } from '../../models'; +import defaultFederation from '../../../static/federation.json'; +import { Origins } from '../../models/Coordinator.model'; + +interface RoboPoolEvents { + onevent: (event: Event) => void; + oneose: () => void; +} + +class RoboPool { + constructor(settings: Settings, origin: string) { + this.network = settings.network ?? 'mainnet'; + this.relays = Object.values(defaultFederation) + .map((coord) => { + const url = coord[this.network][settings.selfhostedClient ? 'onion' : origin]; + + if (!url) return; + + return `ws://${url.replace(/^https?:\/\//, '')}/nostr`; + }) + .filter((item) => item !== undefined); + } + + public relays: string[]; + public network: string; + + public webSockets: WebSocket[] = []; + private messageHandlers: Array<(url: string, event: MessageEvent) => void> = []; + + connect = () => { + this.relays.forEach((url) => { + if (this.webSockets.find((w: WebSocket) => w.url === url)) return; + + let ws: WebSocket; + + const connect = () => { + ws = new WebSocket(url); + + // Add event listeners for the WebSocket + ws.onopen = () => { + console.log(`Connected to ${url}`); + }; + + ws.onmessage = (event) => { + this.messageHandlers.forEach((handler) => handler(url, event)); + }; + + ws.onerror = (error) => { + console.error(`WebSocket error on ${url}:`, error); + }; + + ws.onclose = () => { + console.log(`Disconnected from ${url}. Attempting to reconnect...`); + setTimeout(connect, 1000); // Reconnect after 1 second + }; + }; + + connect(); + this.webSockets.push(ws); + }); + }; + + close = () => { + this.webSockets.forEach((ws) => ws.close()); + }; + + sendMessage = (message: string) => { + const send = (index: number, message: string) => { + const ws = this.webSockets[index]; + + if (ws.readyState === WebSocket.OPEN) { + ws.send(message); + } else if (ws.readyState === WebSocket.CONNECTING) { + setTimeout(send, 500, index, message); + } + }; + + this.webSockets.forEach((_ws, index) => send(index, message)); + }; + + subscribeBook = (events: RoboPoolEvents) => { + const authors = Object.values(defaultFederation) + .map((f) => f.nostrHexPubkey) + .filter((item) => item !== undefined); + + const request = ['REQ', 'subscribeBook', { authors, kinds: [38383], '#n': [this.network] }]; + + this.messageHandlers.push((_url: string, messageEvent: MessageEvent) => { + const jsonMessage = JSON.parse(messageEvent.data); + if (jsonMessage[0] === 'EVENT') { + events.onevent(jsonMessage[2]); + } else if (jsonMessage[0] === 'EOSE') { + events.oneose(); + } + }); + this.sendMessage(JSON.stringify(request)); + }; +} + +export default RoboPool; diff --git a/frontend/src/services/Roboidentities/RoboidentitiesWebClient/RobohashGenerator.ts b/frontend/src/services/Roboidentities/RoboidentitiesWebClient/RobohashGenerator.ts index 87052125..5929ee79 100644 --- a/frontend/src/services/Roboidentities/RoboidentitiesWebClient/RobohashGenerator.ts +++ b/frontend/src/services/Roboidentities/RoboidentitiesWebClient/RobohashGenerator.ts @@ -32,7 +32,7 @@ class RoboGenerator { setTimeout(() => { this.waitingForLibrary = false; - }, 2500); + }, 3000); } public generate: (hash: string, size: 'small' | 'large') => Promise = async (