diff --git a/examples/experimental/railgun-repro/index.ts b/examples/experimental/railgun-repro/index.ts index 4df031d..50333fd 100644 --- a/examples/experimental/railgun-repro/index.ts +++ b/examples/experimental/railgun-repro/index.ts @@ -1,25 +1,25 @@ -import { createDecoder, createEncoder, createLightNode, Protocols, type LightNode, type Decoder, type DecodedMessage } from "@waku/sdk"; -import { determinePubsubTopic } from "@waku/utils"; +import { type DecodedMessage } from "@waku/sdk"; const CONTENT_TOPICS = [ - "/railgun/v2/0-1-fees/json", - "/railgun/v2/0-56-fees/json", - "/railgun/v2/0-137-fees/json", - "/railgun/v2/0-42161-fees/json", - "/railgun/v2/0-421$1-transact-response/json", - "/railgun/v2/encrypted-metrics-pong/json" + "/railgun/v2/0-1-fees/json", + "/railgun/v2/0-56-fees/json", + "/railgun/v2/0-137-fees/json", + "/railgun/v2/0-42161-fees/json", + "/railgun/v2/0-421$1-transact-response/json", + "/railgun/v2/encrypted-metrics-pong/json" ] as const; -const PUBSUB_TOPIC = "/waku/2/rs/0/1"; -const railgunMa = "/dns4/railgun.ivansete.xyz/tcp/8000/wss/p2p/16Uiu2HAmExcXDvdCr2XxfCeQY1jhCoJ1HodgKauRatCngQ9Q1X61"; -const clusterId = 0; -const shard = 1; +interface ServiceWorkerMessage { + type: string; + message?: string; + status?: string; + peerId?: string; + error?: string; + timestamp?: string; + contentTopic?: string; + topics?: readonly string[]; +} -console.log({ - pubsub: determinePubsubTopic(CONTENT_TOPICS[0], PUBSUB_TOPIC), -}); - -// Add timestamp tracking let lastMessageTimestamp: number | null = null; function updateLastMessageTime(): void { @@ -46,13 +46,18 @@ function updateLastMessageTime(): void { } } -// Update the timestamp display every second -setInterval(updateLastMessageTime, 1000); - function updateStatus(message: string): void { const statusElement = document.getElementById('status'); if (!statusElement) return; statusElement.textContent = message; + console.log('📢', message); +} + +function updatePeerId(peerId: string): void { + const peerIdElement = document.getElementById('peerId'); + if (!peerIdElement) return; + peerIdElement.textContent = `Peer ID: ${peerId}`; + console.log('🆔 Peer ID:', peerId); } function addMessageToUI(message: DecodedMessage | string): void { @@ -62,171 +67,148 @@ function addMessageToUI(message: DecodedMessage | string): void { const messageElement = document.createElement('div'); messageElement.className = 'message'; - // Update last message timestamp lastMessageTimestamp = Date.now(); - let content: string; - if (typeof message === 'string') { - content = message; - } else if (message.payload) { - content = new TextDecoder().decode(message.payload); - } else { - content = 'Empty message'; - } + const content = typeof message === 'string' + ? message + : message.payload + ? new TextDecoder().decode(message.payload) + : 'Empty message'; const timestamp = new Date().toLocaleTimeString(); messageElement.textContent = `[${timestamp}] ${content}`; messageContainer.insertBefore(messageElement, messageContainer.firstChild); + console.log('💬 New message:', content); } class Railgun { - private waku: LightNode | null = null; - private subscriptionStartTime: number | null = null; - private messageCount = 0; + private serviceWorkerRegistration: ServiceWorkerRegistration | null = null; - constructor() { - updateStatus('Initializing Railgun...'); - } - - get decoders(): Decoder[] { - console.log('Creating decoders for content topics:', CONTENT_TOPICS); - return CONTENT_TOPICS.map(topic => createDecoder(topic, PUBSUB_TOPIC)); - } - - async start(): Promise { - updateStatus('Starting Waku light node...'); - this.waku = await createLightNode({ - networkConfig: { - shards: [shard], - clusterId: clusterId, - } - }); - - const peerIdElement = document.getElementById('peerId'); - if (peerIdElement) { - peerIdElement.textContent = `Peer ID: ${this.waku.libp2p.peerId.toString()}`; + constructor() { + updateStatus('Initializing Railgun...'); + this.initializeServiceWorker(); } - - updateStatus('Connecting to peer...'); - await this.waku.dial(railgunMa); - await this.waku.waitForPeers([Protocols.Filter]); - updateStatus('Connected successfully'); - this.waku.libp2p.addEventListener("peer:identify", async(peer) => { - updateStatus('Peer connected'); - addMessageToUI(`Peer connected: ${peer.detail.peerId}`); - }); - - this.waku.libp2p.addEventListener("peer:disconnect", (peer) => { - updateStatus('Peer disconnected'); - addMessageToUI(`Peer disconnected: ${peer.detail}`); - }); - } + private async initializeServiceWorker(): Promise { + if (!('serviceWorker' in navigator)) { + updateStatus('Service Workers are not supported in this browser'); + return; + } - async subscribe(): Promise { - if (!this.waku) throw new Error('Waku not initialized'); - - this.subscriptionStartTime = Date.now(); - this.messageCount = 0; - updateStatus('Subscribing to Waku filters...'); + try { + this.serviceWorkerRegistration = await navigator.serviceWorker.register('/service-worker.ts', { + type: 'module' + }); + console.log('🔧 Service Worker registered'); - const {error} = await this.waku.filter.subscribe(this.decoders, (message) => { - this.messageCount++; - if (message.payload) { - const decodedMessage = new TextDecoder().decode(message.payload); - lastMessageTimestamp = Date.now(); - addMessageToUI(decodedMessage); + navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerMessage.bind(this)); + } catch (error) { + console.error('❌ Service Worker registration failed:', error); + updateStatus('Failed to initialize Waku: Service Worker registration failed'); + } + } + + private async handleServiceWorkerMessage(event: MessageEvent): Promise { + const { type, message, status, peerId, error, timestamp, contentTopic } = event.data; - // Log debug information - console.log('Filter message received:', { - messageCount: this.messageCount, - timeSinceSubscription: `${(Date.now() - (this.subscriptionStartTime || 0)) / 1000}s`, - contentTopic: message.contentTopic, - payload: decodedMessage, - timestamp: new Date().toISOString(), - lastMessageTimestamp + switch (type) { + case 'wakuStatus': + this.handleWakuStatus(status, peerId, error); + break; + + case 'peerStatus': + this.handlePeerStatus(status, peerId); + break; + + case 'subscriptionStatus': + this.handleSubscriptionStatus(status, event.data.topics); + break; + + case 'newMessage': + this.handleNewMessage(message, timestamp, contentTopic); + break; + + case 'messageSent': + updateStatus('Message sent successfully'); + break; + + case 'error': + console.error('❌ Service worker error:', error); + updateStatus(`Error: ${error}`); + break; + } + } + + private handleWakuStatus(status?: string, peerId?: string, error?: string): void { + if (status === 'ready') { + updateStatus('Connected to Waku network'); + if (peerId) { + updatePeerId(peerId); + } + this.subscribe(); + } else if (status === 'error') { + updateStatus(`Waku error: ${error}`); + } + } + + private handlePeerStatus(status?: string, peerId?: string): void { + if (!peerId) return; + + if (status === 'connected') { + console.log('🟢 Peer connected:', peerId); + updateStatus('Peer connected'); + } else if (status === 'disconnected') { + console.log('🔴 Peer disconnected:', peerId); + updateStatus('Peer disconnected'); + } + } + + private handleSubscriptionStatus(status?: string, topics?: readonly string[]): void { + if (status === 'subscribed') { + updateStatus('Subscribed to Waku topics'); + console.log('📥 Subscribed to topics:', topics); + } + } + + private handleNewMessage(message?: string, timestamp?: string, contentTopic?: string): void { + if (!message) return; + lastMessageTimestamp = timestamp ? new Date(timestamp).getTime() : Date.now(); + addMessageToUI(`[${contentTopic}] ${message}`); + } + + async subscribe(): Promise { + if (!navigator.serviceWorker.controller) { + console.error('❌ Service Worker not ready'); + return; + } + + navigator.serviceWorker.controller.postMessage({ + type: 'subscribe', + payload: { topics: CONTENT_TOPICS } }); - } - }, {forceUseAllPeers: false}); - - if (error) { - updateStatus(`Subscription error: ${error}`); - } else { - updateStatus('Successfully subscribed to Waku filters'); } - // Add periodic connection check - setInterval(() => this.checkConnection(), 30000); // Check every 30 seconds - } - - private async checkConnection(): Promise { - if (!this.waku) return; - - const connections = this.waku.libp2p.getConnections(); - const filterPeers = this.waku.filter.connectedPeers; - - console.log('Connection status:', { - totalConnections: connections.length, - filterPeers: filterPeers.length, - messageCount: this.messageCount, - timeSinceLastMessage: lastMessageTimestamp ? - `${(Date.now() - lastMessageTimestamp) / 1000}s ago` : - 'No messages received', - uptime: this.subscriptionStartTime ? - `${(Date.now() - this.subscriptionStartTime) / 1000}s` : - 'Not subscribed' - }); - - if (connections.length === 0 || filterPeers.length === 0) { - updateStatus('Connection lost - attempting to reconnect...'); - try { - await this.waku.dial(railgunMa); - await this.waku.waitForPeers([Protocols.Filter]); - updateStatus('Reconnected successfully'); - } catch (error) { - updateStatus(`Reconnection failed: ${error}`); - } + async push(message: string): Promise { + if (!navigator.serviceWorker.controller) { + throw new Error('Service Worker not ready'); + } + + updateStatus('Sending message...'); + navigator.serviceWorker.controller.postMessage({ + type: 'sendMessage', + payload: { message } + }); + + addMessageToUI(`Sent: ${message}`); } - } - - async push(message: string): Promise { - if (!this.waku) throw new Error('Waku not initialized'); - - updateStatus('Pushing message...'); - const encoder = createEncoder({ - contentTopic: CONTENT_TOPICS[0], - pubsubTopic: PUBSUB_TOPIC - }); - - const {failures} = await this.waku.lightPush.send(encoder, { - payload: new TextEncoder().encode(message) - }); - - if (failures.length > 0) { - updateStatus(`Error sending message: ${failures}`); - } else { - updateStatus('Message sent successfully'); - addMessageToUI(`Sent: ${message}`); - } - } - - getWaku(): LightNode | null { - return this.waku; - } } const railgun = new Railgun(); -export default railgun; +setInterval(updateLastMessageTime, 1000); -// Initialize the application -await railgun.start(); -await railgun.subscribe(); - -// Add global functions and objects declare global { interface Window { sendMessage: () => Promise; - waku: LightNode | null; } } @@ -237,6 +219,4 @@ window.sendMessage = async (): Promise => { await railgun.push(message); input.value = ''; } -}; - -window.waku = railgun.getWaku(); \ No newline at end of file +}; \ No newline at end of file diff --git a/examples/experimental/railgun-repro/service-worker.ts b/examples/experimental/railgun-repro/service-worker.ts new file mode 100644 index 0000000..5b2195e --- /dev/null +++ b/examples/experimental/railgun-repro/service-worker.ts @@ -0,0 +1,210 @@ +import { createDecoder, createEncoder, createLightNode, type LightNode, Protocols } from '@waku/sdk'; + +declare const self: ServiceWorkerGlobalScope; + +const WAKU_CONFIG = { + PUBSUB_TOPIC: "/waku/2/rs/0/1", + CONTENT_TOPICS: [ + "/railgun/v2/0-1-fees/json", + "/railgun/v2/0-56-fees/json", + "/railgun/v2/0-137-fees/json", + "/railgun/v2/0-42161-fees/json", + "/railgun/v2/0-421$1-transact-response/json", + "/railgun/v2/encrypted-metrics-pong/json" + ] as const, + NETWORK: { + CLUSTER_ID: 0, + SHARD: 1, + RAILGUN_MA: '/dns4/railgun.ivansete.xyz/tcp/8000/wss/p2p/16Uiu2HAmExcXDvdCr2XxfCeQY1jhCoJ1HodgKauRatCngQ9Q1X61' + } +} as const; + +interface SendMessagePayload { + message: string; +} + +interface SubscribePayload { + topics: readonly string[]; +} + +type MessagePayload = { + type: 'sendMessage'; + payload: SendMessagePayload; +} | { + type: 'subscribe'; + payload: SubscribePayload; +}; + +type ClientMessage = { + type: string; + [key: string]: any; +}; + +let wakuNode: LightNode | null = null; + +async function notifyClients(message: ClientMessage): Promise { + const clients = await self.clients.matchAll(); + clients.forEach(client => client.postMessage(message)); +} + +async function setupPeerListeners(node: LightNode): Promise { + node.libp2p.addEventListener('peer:identify', async (evt) => { + const peerId = evt.detail.peerId.toString(); + console.log('🟢 Peer connected:', peerId); + await notifyClients({ + type: 'peerStatus', + status: 'connected', + peerId + }); + }); + + node.libp2p.addEventListener('peer:disconnect', async (evt) => { + const peerId = evt.detail.toString(); + console.log('🔴 Peer disconnected:', peerId); + await notifyClients({ + type: 'peerStatus', + status: 'disconnected', + peerId + }); + }); +} + +async function initializeWaku(): Promise { + try { + console.log('🚀 Initializing Waku node...'); + wakuNode = await createLightNode({ + networkConfig: { + clusterId: WAKU_CONFIG.NETWORK.CLUSTER_ID, + shards: [WAKU_CONFIG.NETWORK.SHARD], + } + }); + + await setupPeerListeners(wakuNode); + + await wakuNode.start(); + console.log('✅ Waku node started'); + + await wakuNode.dial(WAKU_CONFIG.NETWORK.RAILGUN_MA); + console.log('🔗 Connected to Railgun peer'); + + await wakuNode.waitForPeers([Protocols.Filter]); + console.log('📡 Filter protocol peer ready'); + + await notifyClients({ + type: 'wakuStatus', + status: 'ready', + peerId: wakuNode.libp2p.peerId.toString() + }); + + await subscribeToTopics(); + } catch (error) { + console.error('❌ Error initializing Waku:', error); + await notifyClients({ + type: 'wakuStatus', + status: 'error', + error: error instanceof Error ? error.message : 'Unknown error' + }); + } +} + +async function subscribeToTopics(): Promise { + if (!wakuNode) { + console.error('❌ Waku node not initialized'); + return; + } + + try { + console.log('📥 Creating decoders for topics:', WAKU_CONFIG.CONTENT_TOPICS); + const decoders = WAKU_CONFIG.CONTENT_TOPICS.map( + topic => createDecoder(topic, WAKU_CONFIG.PUBSUB_TOPIC) + ); + + await wakuNode.waitForPeers([Protocols.Filter]); + + const { error } = await wakuNode.filter.subscribe(decoders, handleIncomingMessage); + + if (error) { + throw new Error(`Subscription error: ${error}`); + } + + console.log('✅ Successfully subscribed to topics'); + await notifyClients({ + type: 'subscriptionStatus', + status: 'subscribed', + topics: WAKU_CONFIG.CONTENT_TOPICS + }); + } catch (error) { + console.error('❌ Error subscribing:', error); + await notifyClients({ + type: 'error', + error: error instanceof Error ? error.message : 'Unknown error' + }); + } +} + +async function handleIncomingMessage(message: { payload: Uint8Array; contentTopic: string }): Promise { + console.log('📨 Message received on topic:', message.contentTopic); + const content = new TextDecoder().decode(message.payload); + await notifyClients({ + type: 'newMessage', + message: content, + timestamp: new Date().toISOString(), + contentTopic: message.contentTopic + }); +} + +async function sendMessage(message: string): Promise { + if (!wakuNode) { + throw new Error('Waku node not initialized'); + } + + try { + const encoder = createEncoder({ + contentTopic: WAKU_CONFIG.CONTENT_TOPICS[0], + pubsubTopic: WAKU_CONFIG.PUBSUB_TOPIC, + }); + + await wakuNode.lightPush.send(encoder, { + payload: new TextEncoder().encode(message) + }); + console.log('📤 Message sent:', message); + + await notifyClients({ + type: 'messageSent', + message + }); + } catch (error) { + console.error('❌ Error sending message:', error); + await notifyClients({ + type: 'error', + error: error instanceof Error ? error.message : 'Unknown error' + }); + } +} + +self.addEventListener('install', () => { + console.log('🔧 Service Worker: Installing...'); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event: ExtendableEvent) => { + console.log('🔧 Service Worker: Activating...'); + event.waitUntil(initializeWaku()); +}); + +self.addEventListener('message', async (event: ExtendableMessageEvent) => { + const { type, payload } = event.data as MessagePayload; + + switch (type) { + case 'sendMessage': + await sendMessage(payload.message); + break; + + case 'subscribe': + await subscribeToTopics(); + break; + + default: + console.warn('⚠️ Unknown message type:', type); + } +}); \ No newline at end of file diff --git a/examples/experimental/railgun-repro/tsconfig.json b/examples/experimental/railgun-repro/tsconfig.json index b4f286b..d79224c 100644 --- a/examples/experimental/railgun-repro/tsconfig.json +++ b/examples/experimental/railgun-repro/tsconfig.json @@ -1,25 +1,15 @@ { "compilerOptions": { "target": "ES2020", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - - /* Type Checking */ + "module": "ES2020", + "moduleResolution": "node", "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthrough": true, - "noImplicitAny": true, - "strictNullChecks": true + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2020", "DOM", "WebWorker"], + "types": ["vite/client", "node"] }, - "include": ["src"] + "include": ["*.ts"], + "exclude": ["node_modules"] } \ No newline at end of file