mixFetch
Not started
Concurrent Requests
Open your browser's console to see the connection and send/receive logging for this example.
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 };
const logColors: Record<LogLevel, string> = {
info: "gray",
error: "red",
send: "blue",
receive: "green",
};
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[]>([]);
const logEndRef = useRef<HTMLDivElement>(null);
// 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 log panel to bottom
useEffect(() => {
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [logs]);
// Helper to add a timestamped log entry
const addLog = (message: string, level: LogLevel) => {
const timestamp = new Date().toISOString().substring(11, 23);
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 — reuses the existing MixFetch 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);
}
};
const isReady = status === "ready";
const statusText = {
idle: "Not started",
starting: "Starting...",
ready: "Ready",
error: `Error: ${errorMsg}`,
};
const statusColor = {
idle: "gray",
starting: "orange",
ready: "green",
error: "red",
};
return (
<div style={{ marginTop: "1rem" }}>
{/* Start MixFetch */}
<Paper sx={{ p: 2, mb: 2 }} variant="outlined">
<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>
</Paper>
{/* Fetch controls — disabled until MixFetch is ready */}
<Box
sx={{
opacity: isReady ? 1 : 0.5,
pointerEvents: isReady ? "auto" : "none",
}}
>
{/* Single fetch */}
<Stack direction="row">
<TextField
disabled={busy}
fullWidth
label="URL"
type="text"
variant="outlined"
defaultValue={defaultUrl}
onChange={(e) => setUrl(e.target.value)}
/>
<Button
variant="outlined"
disabled={busy}
sx={{ marginLeft: "1rem" }}
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 */}
<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>
{/* Log Panel */}
{logs.length > 0 && (
<Paper
sx={{ p: 2, mt: 3, maxHeight: 200, overflow: "auto" }}
variant="outlined"
>
<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>
))}
<div ref={logEndRef} />
</Paper>
)}
</div>
);
};