diff --git a/src/components/BootSplash.tsx b/src/components/BootSplash.tsx new file mode 100644 index 0000000..3048f81 --- /dev/null +++ b/src/components/BootSplash.tsx @@ -0,0 +1,81 @@ +import { FC, useEffect, useState } from "react" + +/** + * What the page shows while the chain is being reached. + * + * `main.tsx` wraps the entire app in ``, + * and `Subscribe`'s fallback defaults to `null` — "it will render null until the + * subscription exists". `dynamicBuilder$` does not emit until a chain has been + * followed and its metadata decoded, so until then the document body is empty + * and a visitor sees a bare black page with nothing but the tab spinner to + * suggest it is not simply broken. + * + * That is indistinguishable from a crash, and on a chain the console cannot read + * it never resolves at all. So: say what is happening, and say it louder the + * longer it takes. + */ + +const SLOW_MS = 8_000 +const STUCK_MS = 25_000 + +export const BootSplash: FC<{ endpoint?: string; network?: string }> = ({ + endpoint, + network, +}) => { + const [elapsed, setElapsed] = useState(0) + + useEffect(() => { + const started = Date.now() + const id = setInterval(() => setElapsed(Date.now() - started), 500) + + return () => clearInterval(id) + }, []) + + return ( +
+
+ qapi{" "} + console +
+ +
+ +
+ Connecting to {network ?? "the chain"} + {endpoint ? ( + <> + {" "} + at {endpoint} + + ) : null} + … +
+ + {elapsed > SLOW_MS && elapsed <= STUCK_MS ? ( +
+ Still going. The console downloads the chain's metadata and decodes it + before it can render anything, which takes a moment on a first visit. +
+ ) : null} + + {elapsed > STUCK_MS ? ( +
+

+ This is taking much longer than it should. +

+

+ Either the node is unreachable, or the console cannot read this + chain. Check the browser console for the reason, and try another + network — the switcher is in the header once any chain loads, and{" "} + #networkId=polkadot&endpoint=wss://rpc.polkadot.io{" "} + in the URL will force one. +

+
+ ) : null} +
+ ) +} diff --git a/src/main.tsx b/src/main.tsx index be99382..0561f70 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,4 +1,4 @@ -import { dynamicBuilder$ } from "@/state/chains/chain.state" +import { dynamicBuilder$, getDefaultChain } from "@/state/chains/chain.state" import { RemoveSubscribe, Subscribe } from "@react-rxjs/core" import { MultisigExternalSignerModal, PolkaHubProvider } from "polkahub" import { StrictMode } from "react" @@ -8,6 +8,7 @@ import { merge } from "rxjs" import App from "./App.tsx" import { TooltipProvider } from "./components/ui/tooltip.tsx" import "./index.css" +import { BootSplash } from "./components/BootSplash.tsx" import { codeSplit$ } from "./lib/externalState.ts" import { explorer$ } from "./pages/Explorer" import { polkaHub } from "./state/polkahub.ts" @@ -17,8 +18,22 @@ const metrics$ = codeSplit$(() => import("./pages/Metrics/metrics.state.ts").then((r) => r.blockStats$), ) +// Without a fallback, `Subscribe` renders null until `dynamicBuilder$` emits — +// which is after a chain has been followed and its metadata decoded. On a slow +// connection that is seconds of empty black page; on a chain the console cannot +// read, it is forever, and indistinguishable from a crash. +const initial = getDefaultChain() + createRoot(document.getElementById("root")!).render( - + + } + source$={merge(dynamicBuilder$, explorer$, metrics$)} + > diff --git a/src/state/chains/chain.state.ts b/src/state/chains/chain.state.ts index 47c5379..af0ffb0 100644 --- a/src/state/chains/chain.state.ts +++ b/src/state/chains/chain.state.ts @@ -145,10 +145,17 @@ export const isValidUri = (input: string): boolean => { const defaultSelectedChain: SelectedChain = { network: defaultNetwork, - endpoint: LIGHT_CLIENT_ENDPOINT, + // AUTO_RPC, not LIGHT_CLIENT. A light client needs a chain spec, and the + // Quantus networks have none (`hasChainSpecs: false`), so defaulting to + // smoldot asks it to follow a chain it has never heard of — which fails + // before a single byte is fetched, and with `Subscribe`'s null fallback it + // failed as a bare black page. + endpoint: defaultNetwork.lightclient + ? LIGHT_CLIENT_ENDPOINT + : AUTO_RPC_ENDPOINT, forkMethod: "none", } -const getDefaultChain = (): SelectedChain => { +export const getDefaultChain = (): SelectedChain => { const hashParams = getHashParams() if (hashParams.has("networkId") && hashParams.has("endpoint")) { const networkId = hashParams.get("networkId")!