diff --git a/src/app/components/Header.tsx b/src/app/components/Header.tsx index 75cd167..374c13e 100644 --- a/src/app/components/Header.tsx +++ b/src/app/components/Header.tsx @@ -1,28 +1,24 @@ import { Block, BlockTypes } from "@/components/Block"; import { Title } from "@/components/Title"; -import { Status } from "@/components/Status"; -import { useStore } from "@/hooks"; import { Button } from "@/components/Button"; type HeaderProps = { + children?: React.ReactNode; onWalletConnect?: () => void; } export const Header: React.FunctionComponent = (props) => { - const { appStatus, wallet } = useStore(); - return ( <> - Waku RLN + Waku {props.onWalletConnect && ( )} - - {wallet &&

Wallet connected: {wallet}

} + {props.children} ); }; diff --git a/src/app/components/Waku.tsx b/src/app/components/Waku.tsx index 65c7138..3364ca0 100644 --- a/src/app/components/Waku.tsx +++ b/src/app/components/Waku.tsx @@ -2,15 +2,16 @@ import React from "react"; import { Block } from "@/components/Block"; import { Subtitle } from "@/components/Subtitle"; import { Button } from "@/components/Button"; -import { MessageContent, useWaku } from "@/hooks"; +import { MessageContent } from "@/hooks"; -export const Waku: React.FunctionComponent<{}> = () => { - const { - onSend, - messages, - contentTopic: activeContentTopic, - onContentTopicChange: onActiveContentTopicChange - } = useWaku(); +type WakuProps = { + onSend: (nick: string, text: string) => Promise; + activeContentTopic: string; + messages: MessageContent[]; + onActiveContentTopicChange: (contentTopic: string) => void; +} + +export const Waku: React.FunctionComponent = (props) => { const { nick, text, @@ -21,16 +22,16 @@ export const Waku: React.FunctionComponent<{}> = () => { const { contentTopic, onContentTopicChange, - } = useContentTopic(activeContentTopic); + } = useContentTopic(props.activeContentTopic); const onSendClick = async () => { - await onSend(nick, text); + await props.onSend(nick, text); resetText(); }; const renderedMessages = React.useMemo( - () => messages.map(renderMessage), - [messages] + () => props.messages.map(renderMessage), + [props.messages] ); return ( @@ -53,7 +54,7 @@ export const Waku: React.FunctionComponent<{}> = () => { onChange={onContentTopicChange} className="w-96 mr-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" /> - + diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx index 837dd65..c4b0aa7 100644 --- a/src/app/home/page.tsx +++ b/src/app/home/page.tsx @@ -1,12 +1,58 @@ "use client"; import { Header } from "@/app/components/Header"; import { Waku } from "@/app/components/Waku"; +import { useWaku } from "@/hooks"; +import { DebugInfo } from "@/services/waku"; export default function Home() { + const { + onSend, + messages, + debugInfo, + contentTopic, + onContentTopicChange + } = useWaku(); + return (
-
- +
+ +
+
); } + +type DebugInfoProps = { + value?: DebugInfo; +} + +const DebugInfo: React.FunctionComponent = (props) => { + if (!props.value) { + return; + } + + return ( +
+ + Show node info + +
+

Health: {props.value.health}

+

Version: {props.value.version}

+

ENR URI: {props.value.enrUri}

+

Listen Addresses:

+
    + {props.value.listenAddresses.map((address, index) => ( +
  • {address}
  • + ))} +
+
+
+ ); +} diff --git a/src/app/keystore/page.tsx b/src/app/keystore/page.tsx index 269f232..f0cf2fe 100644 --- a/src/app/keystore/page.tsx +++ b/src/app/keystore/page.tsx @@ -3,12 +3,18 @@ import { Header } from "@/app/components/Header"; import { Keystore } from "@/app/components/Keystore"; import { KeystoreDetails } from "@/app/components/KeystoreDetails"; import { useWallet } from "@/hooks"; +import { Status } from "@/components/Status"; +import { useStore } from "@/hooks"; export default function KeystorePage() { const { onWalletConnect } = useWallet(); + const { appStatus, wallet } = useStore(); return (
-
+
+ + {wallet &&

Wallet connected: {wallet}

} +
diff --git a/src/hooks/useWaku.ts b/src/hooks/useWaku.ts index 361e83a..ac98325 100644 --- a/src/hooks/useWaku.ts +++ b/src/hooks/useWaku.ts @@ -1,6 +1,6 @@ import React from "react"; import { CONTENT_TOPIC } from "@/constants"; -import { Message, waku } from "@/services/waku"; +import { DebugInfo, Message, waku } from "@/services/waku"; export type MessageContent = { nick: string; @@ -11,6 +11,7 @@ export type MessageContent = { export const useWaku = () => { const [contentTopic, setContentTopic] = React.useState(CONTENT_TOPIC); const [messages, setMessages] = React.useState>(new Map()); + const [debugInfo, setDebugInfo] = React.useState(); React.useEffect(() => { const messageListener = (event: CustomEvent) => { @@ -38,6 +39,25 @@ export const useWaku = () => { }; }, [messages, setMessages, contentTopic]); + + React.useEffect(() => { + const debugInfoListener = (event: CustomEvent) => { + const debugInfo = event.detail; + + if (!debugInfo) { + return; + } + + setDebugInfo(debugInfo); + }; + + waku.debug.addEventListener("debug", debugInfoListener); + + return () => { + waku.debug.removeEventListener("debug", debugInfoListener); + }; + }, [debugInfo, setDebugInfo]); + const onSend = React.useCallback( async (nick: string, text: string) => { const timestamp = Date.now(); @@ -73,6 +93,7 @@ export const useWaku = () => { return { onSend, + debugInfo, contentTopic, onContentTopicChange, messages: Array.from(messages.values()) diff --git a/src/services/waku.ts b/src/services/waku.ts index 7ade39b..8c8fff2 100644 --- a/src/services/waku.ts +++ b/src/services/waku.ts @@ -17,6 +17,7 @@ const RELAY = "/relay/v1"; const buildURL = (endpoint: string) => `${LOCAL_NODE}${endpoint}`; class Relay { + private subscribing = false; private readonly subscriptionsEmitter = new EventTarget(); // only one content topic subscriptions is possible now private subscriptionRoutine: undefined | number; @@ -24,7 +25,7 @@ class Relay { constructor() {} public addEventListener(contentTopic: string, fn: EventListener) { - this.subscribe(contentTopic); + this.subscribe(); return this.subscriptionsEmitter.addEventListener(contentTopic, fn as any); } @@ -35,11 +36,12 @@ class Relay { ); } - private async subscribe(contentTopic: string) { - if (this.subscriptionRoutine) { + private async subscribe() { + if (this.subscriptionRoutine || this.subscribing) { return; } + this.subscribing = true; try { await http.post(buildURL(`${RELAY}/subscriptions`), [PUBSUB_TOPIC]); @@ -47,11 +49,12 @@ class Relay { await this.fetchMessages(); }, 5 * SECOND); } catch (error) { - console.error(`Failed to subscribe node ${contentTopic}:`, error); + console.error(`Failed to subscribe node ${PUBSUB_TOPIC}:`, error); } + this.subscribing = false; } - public async unsubscribe(contentTopic: string) { + public async unsubscribe() { if (!this.subscriptionRoutine) { return; } @@ -59,10 +62,11 @@ class Relay { try { await http.delete(buildURL(`${RELAY}/subscriptions`), [PUBSUB_TOPIC]); } catch (error) { - console.error(`Failed to unsubscribe node from ${contentTopic}:`, error); + console.error(`Failed to unsubscribe node from ${PUBSUB_TOPIC}:`, error); } clearInterval(this.subscriptionRoutine); + this.subscriptionRoutine = undefined; } private async fetchMessages(): Promise { @@ -103,6 +107,92 @@ class Relay { } } +type DebugInfoResponse = { + enrUri: string; + listenAddresses: string[]; +} + +export type DebugInfo = { + health: string; + version: string; +} & DebugInfoResponse; + +class Debug { + private subscribing = false; + private readonly subscriptionsEmitter = new EventTarget(); + private subscriptionRoutine: undefined | number; + + constructor() {} + + public addEventListener(event: string, fn: EventListener) { + this.subscribe(); + return this.subscriptionsEmitter.addEventListener(event, fn as any); + } + + public removeEventListener(event: string, fn: EventListener) { + return this.subscriptionsEmitter.removeEventListener( + event, + fn as any + ); + } + + private async subscribe() { + if (this.subscriptionRoutine || this.subscribing) { + return; + } + + this.subscribing = true; + try { + await this.fetchParameters(); + this.subscriptionRoutine = window.setInterval(async () => { + await this.fetchParameters(); + }, 30 * SECOND); + } catch(error) { + console.error("Failed to fetch debug info:", error); + } + this.subscribing = false; + } + + private async unsubscribe() { + if (!this.subscriptionRoutine) { + return; + } + clearInterval(this.subscriptionRoutine); + this.subscriptionRoutine = undefined; + } + + private async fetchParameters(): Promise { + const health = await this.fetchHealth(); + const debug = await this.fetchDebugInfo(); + const version = await this.fetchDebugVersion(); + + this.subscriptionsEmitter.dispatchEvent( + new CustomEvent("debug", { detail: { + health, + version, + ...debug, + } }) + ); + } + + private async fetchHealth(): Promise { + const response = await http.get(buildURL(`/health`)); + return response.text(); + } + + private async fetchDebugInfo(): Promise { + const response = await http.get(buildURL(`/debug/v1/info`)); + const body: DebugInfoResponse = await response.json(); + return body; + } + + private async fetchDebugVersion(): Promise { + const response = await http.get(buildURL(`/debug/v1/version`)); + return response.text(); + } +} + export const waku = { relay: new Relay(), + debug: new Debug(), };