import React, { useState, useRef, useEffect } from "react"; import CircularProgress from "@mui/material/CircularProgress"; import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; import { mixFetch, createMixFetch } from "@nymproject/mix-fetch-full-fat"; import Stack from "@mui/material/Stack"; import Paper from "@mui/material/Paper"; import type { SetupMixFetchOps } from "@nymproject/mix-fetch-full-fat"; const defaultUrl = "https://nymtech.net/.wellknown/network-requester/exit-policy.txt"; const args = { mode: "unsafe-ignore-cors" }; const mixFetchOptions: SetupMixFetchOps = { clientId: "docs-mixfetch-demo", // explicit ID preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1", mixFetchOverride: { requestTimeoutMs: 60_000, }, forceTls: true, // force WSS }; // Log entry type for the visible log panel type LogLevel = "info" | "error" | "send" | "receive"; type LogEntry = { timestamp: string; message: string; level: LogLevel }; // Color map for log levels const logColors: Record = { info: "gray", error: "red", send: "blue", receive: "green", }; // Label map for log levels const logLabels: Record = { info: "INFO", error: "ERROR", send: "SEND", receive: "RECV", }; export const MixFetch = () => { // MixFetch initialization state const [status, setStatus] = useState<"idle" | "starting" | "ready" | "error">( "idle" ); const [errorMsg, setErrorMsg] = useState(null); // Log panel state const [logs, setLogs] = useState([]); // Single fetch state const [url, setUrl] = useState(defaultUrl); const [html, setHtml] = useState(); const [busy, setBusy] = useState(false); // Concurrent fetch state const [concurrentResults, setConcurrentResults] = useState([]); const [concurrentBusy, setConcurrentBusy] = useState(false); // Auto-scroll within the log panel when new entries are added (without scrolling the page) const logContainerRef = useRef(null); useEffect(() => { if (logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [logs]); // Helper to add a timestamped log entry const addLog = (message: string, level: LogLevel) => { const timestamp = new Date().toISOString().substring(11, 23); // HH:MM:SS.mmm setLogs((prev) => [...prev, { timestamp, message, level }]); }; // Initialize MixFetch explicitly via createMixFetch const handleStart = async () => { try { setStatus("starting"); setErrorMsg(null); addLog("Starting MixFetch...", "info"); await createMixFetch(mixFetchOptions); setStatus("ready"); addLog("MixFetch is ready!", "info"); } catch (err) { const msg = err instanceof Error ? err.message : String(err); setStatus("error"); setErrorMsg(msg); addLog(`Error: ${msg}`, "error"); } }; // Single URL fetch — mixFetch reuses the existing singleton const handleFetch = async () => { try { setBusy(true); setHtml(undefined); addLog(`Sending request to ${url}...`, "send"); const response = await mixFetch(url, args, mixFetchOptions); const resHtml = await response.text(); setHtml(resHtml); addLog(`Response received (${resHtml.length} bytes)`, "receive"); } catch (err) { const msg = err instanceof Error ? err.message : String(err); addLog(`Fetch error: ${msg}`, "error"); } finally { setBusy(false); } }; // Send 5 concurrent requests to different URLs on the same domain const handleConcurrentFetch = async () => { const baseUrl = "https://jsonplaceholder.typicode.com/posts/"; const count = 5; try { setConcurrentBusy(true); setConcurrentResults([]); addLog( `Starting ${count} concurrent requests to ${baseUrl}1-${count}...`, "send" ); // Fire off all requests concurrently using Promise.all const requests = Array.from({ length: count }, (_, i) => { const targetUrl = `${baseUrl}${i + 1}`; return mixFetch(targetUrl, args, mixFetchOptions) .then((res) => res.json()) .then((json: { id: number; title: string }) => { const entry = `[${json.id}] ${json.title}`; addLog(entry, "receive"); return entry; }); }); const results = await Promise.all(requests); setConcurrentResults(results); addLog(`All ${count} concurrent requests completed!`, "info"); } catch (err) { const msg = err instanceof Error ? err.message : String(err); addLog(`Concurrent fetch error: ${msg}`, "error"); } finally { setConcurrentBusy(false); } }; // Are fetch controls enabled? const isReady = status === "ready"; // Status text + color for the startup indicator const statusText: Record = { idle: "Not started", starting: "Starting...", ready: "Ready", error: `Error: ${errorMsg}`, }; const statusColor: Record = { idle: "#9e9e9e", starting: "orange", ready: "#85E89D", error: "#ff6b6b", }; return ( {/* --- Start MixFetch Section --- */} {status === "starting" && } {statusText[status]} {/* --- Fetch Controls (disabled until ready) --- */} {/* Single fetch */} setUrl(e.target.value)} size="small" /> {busy && ( )} {html && ( <> Response {html} )} {/* Concurrent fetch demo */} Concurrent Requests {concurrentBusy && ( )} {concurrentResults.length > 0 && ( {concurrentResults.map((result, i) => ( {result} ))} )} {/* --- Log Panel --- */} {logs.length > 0 && ( Log {logs.map((entry, i) => ( {entry.timestamp} [{logLabels[entry.level]}] {entry.message} ))} )} ); };