Files
nym/documentation/docs/components/mix-fetch.tsx
T
mfahampshire a70e68c7bd Max/smolmix docs (#6716)
* Smolmix documentation

* Add smolmix docs: landing page, tutorials, and developer page links

* Add Exit Gateway services page (NR vs IPR) and link from existing docs

* Update auto-generated command and API outputs

* Reorg of tutorials and architecture pages

* License information + remove TODO from docs.rs visibile comment + reorg
readme

* Add versions file for doc-wide versioning

* Relative -> absolute links

* Relative -> absolute links

* Update license + add old tutorial code as examples

* Streamline smolmix docs

* Clippy

* Clean up doc comments

* Last pass

* Add larger file download to list

* set new versions

* Clippy

* Remove blake pin from docs + add version range to root Cargo.toml

* Format example logging

* Remove crate blocked component

* Loose whitespace

* Add doc verification script for inline mdx

* Formatting

* Components regen

* Reorg + tighten text

* Voicing cohesion pass + remove bloated examples

* Voicing cont.

* Reduce max download size

* Small suggested clarifications

* Max/docs voicing consistency (#6769)

* Reduce max download size

* voicing consistency across docs

* New landing order w smolmix

* Tweaks

* Final tweaks
2026-05-13 11:19:44 +00:00

283 lines
8.8 KiB
TypeScript

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<LogLevel, string> = {
info: "gray",
error: "red",
send: "blue",
receive: "green",
};
// Label map for log levels
const logLabels: Record<LogLevel, string> = {
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<string | null>(null);
// Log panel state
const [logs, setLogs] = useState<LogEntry[]>([]);
// Single fetch state
const [url, setUrl] = useState<string>(defaultUrl);
const [html, setHtml] = useState<string>();
const [busy, setBusy] = useState<boolean>(false);
// Concurrent fetch state
const [concurrentResults, setConcurrentResults] = useState<string[]>([]);
const [concurrentBusy, setConcurrentBusy] = useState<boolean>(false);
// Auto-scroll within the log panel when new entries are added (without scrolling the page)
const logContainerRef = useRef<HTMLDivElement>(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<typeof status, string> = {
idle: "Not started",
starting: "Starting...",
ready: "Ready",
error: `Error: ${errorMsg}`,
};
const statusColor: Record<typeof status, string> = {
idle: "#9e9e9e",
starting: "orange",
ready: "#85E89D",
error: "#ff6b6b",
};
return (
<Box sx={{ mt: 2 }}>
<Paper sx={{ p: 3 }}>
<Stack spacing={3}>
{/* --- Start MixFetch Section --- */}
<Stack direction="row" alignItems="center" spacing={2}>
<Button
variant="contained"
disabled={status === "starting" || status === "ready"}
onClick={handleStart}
>
Start MixFetch
</Button>
{status === "starting" && <CircularProgress size={20} />}
<Typography
fontFamily="monospace"
fontSize="small"
sx={{ color: statusColor[status] }}
>
{statusText[status]}
</Typography>
</Stack>
{/* --- Fetch Controls (disabled until ready) --- */}
<Box
sx={{
opacity: isReady ? 1 : 0.5,
pointerEvents: isReady ? "auto" : "none",
}}
>
{/* Single fetch */}
<Stack direction="row" spacing={2}>
<TextField
disabled={busy}
fullWidth
label="URL"
type="text"
variant="outlined"
defaultValue={defaultUrl}
onChange={(e) => setUrl(e.target.value)}
size="small"
/>
<Button
variant="outlined"
disabled={busy}
onClick={handleFetch}
>
Fetch
</Button>
</Stack>
{busy && (
<Box mt={2}>
<CircularProgress />
</Box>
)}
{html && (
<>
<Box mt={2}>
<strong>Response</strong>
</Box>
<Paper sx={{ p: 2, mt: 1 }} elevation={4}>
<Typography fontFamily="monospace" fontSize="small">
{html}
</Typography>
</Paper>
</>
)}
{/* Concurrent fetch demo */}
<Box mt={3}>
<strong>Concurrent Requests</strong>
<Box mt={1}>
<Button
variant="outlined"
disabled={concurrentBusy}
onClick={handleConcurrentFetch}
>
Send 5 Concurrent Requests (posts/1-5)
</Button>
</Box>
</Box>
{concurrentBusy && (
<Box mt={2}>
<CircularProgress />
</Box>
)}
{concurrentResults.length > 0 && (
<Paper sx={{ p: 2, mt: 2 }} elevation={4}>
{concurrentResults.map((result, i) => (
<Typography key={i} fontFamily="monospace" fontSize="small">
{result}
</Typography>
))}
</Paper>
)}
</Box>
</Stack>
</Paper>
{/* --- Log Panel --- */}
{logs.length > 0 && (
<Paper
ref={logContainerRef}
sx={{ p: 2, mt: 2, maxHeight: 200, overflow: "auto" }}
>
<strong>Log</strong>
{logs.map((entry, i) => (
<Typography
key={i}
fontFamily="monospace"
fontSize="small"
sx={{ color: logColors[entry.level] }}
>
{entry.timestamp} [{logLabels[entry.level]}] {entry.message}
</Typography>
))}
</Paper>
)}
</Box>
);
};