Update MixFetch docs playground + components (#6479)

This commit is contained in:
mfahampshire
2026-02-24 09:29:15 +00:00
committed by GitHub
parent 630c4922ac
commit 77a34fe3bf
10 changed files with 1255 additions and 466 deletions
@@ -1,86 +1,273 @@
```tsx copy filename="mixFetchExample.tsx"
import React, { useState } from "react";
```tsx
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 } from "@nymproject/mix-fetch-full-fat";
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 defaultUrl =
"https://nymtech.net/.wellknown/network-requester/exit-policy.txt";
const args = { mode: "unsafe-ignore-cors" };
const mixFetchOptions: SetupMixFetchOps = {
preferredGateway: "2xU4CBE6QiiYt6EyBXSALwxkNvM7gqJfjHXaMkjiFmYW", // with WSS
// preferredNetworkRequester:
// "CTDxrcXgrZHWyCWnuCgjpJPghQUcEVz1HkhUr5mGdFnT.3UAww1YWNyVNYNWFQL1LaHYouQtDiXBGK5GiDZgpXkTK@2RFtU5BwxvJJXagAWAEuaPgb5ZVPRoy2542TT93Edw6v",
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);
console.log(response);
const resHtml = await response.text();
setHtml(resHtml);
addLog(`Response received (${resHtml.length} bytes)`, "receive");
} catch (err) {
console.log(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" }}>
<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>
{/* 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>
{busy && (
<Box mt={4}>
<CircularProgress />
</Box>
)}
{html && (
<>
<Box mt={4}>
<strong>Response</strong>
{/* 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>
<Paper sx={{ p: 2, mt: 1 }} elevation={4}>
<Typography fontFamily="monospace" fontSize="small">
{html}
</Typography>
)}
{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>
);
+234 -38
View File
@@ -1,10 +1,10 @@
import React, { useState } from "react";
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 } from "@nymproject/mix-fetch-full-fat";
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";
@@ -12,8 +12,8 @@ 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,
@@ -21,64 +21,260 @@ const mixFetchOptions: SetupMixFetchOps = {
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);
console.log(response);
const resHtml = await response.text();
setHtml(resHtml);
addLog(`Response received (${resHtml.length} bytes)`, "receive");
} catch (err) {
console.log(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: "gray",
starting: "orange",
ready: "green",
error: "red",
};
return (
<div style={{ marginTop: "1rem" }}>
<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>
{/* --- Start MixFetch Section --- */}
<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>
{busy && (
<Box mt={4}>
<CircularProgress />
</Box>
)}
{html && (
<>
<Box mt={4}>
<strong>Response</strong>
{/* --- Fetch Controls (disabled until 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>
<Paper sx={{ p: 2, mt: 1 }} elevation={4}>
<Typography fontFamily="monospace" fontSize="small">
{html}
</Typography>
)}
{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>
{/* --- Log Panel --- */}
{logs.length > 0 && (
<Paper
ref={logContainerRef}
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>
))}
</Paper>
)}
</div>
);
@@ -1 +1 @@
Friday, February 20th 2026, 13:42:15 UTC
Monday, February 23rd 2026, 22:45:55 UTC
@@ -11,7 +11,7 @@ options:
--no_routing_history Display node stats without routing history
--no_verloc_metrics Display node stats without verloc metrics
-m, --markdown Display results in markdown format
-o [OUTPUT], --output [OUTPUT]
-o, --output [OUTPUT]
Save results to file (in current dir or supply with
path without filename)
```
@@ -12,8 +12,7 @@ usage: nym-node-cli install [-h] [-V] [-d BRANCH] [-v]
options:
-h, --help show this help message and exit
-V, --version show program's version number and exit
-d BRANCH, --dev BRANCH
Define github branch (default: develop)
-d, --dev BRANCH Define github branch (default: develop)
-v, --verbose Show full error tracebacks
--mode {mixnode,entry-gateway,exit-gateway}
Node mode: 'mixnode', 'entry-gateway', or 'exit-
@@ -62,6 +62,8 @@ Options:
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
--wireguard-userspace <WIREGUARD_USERSPACE>
Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] [possible values: true, false]
--verloc-bind-address <VERLOC_BIND_ADDRESS>
Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
--verloc-announce-port <VERLOC_ANNOUNCE_PORT>
@@ -80,6 +82,8 @@ Options:
Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY>
Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
--lp-use-mock-ecash <LP_USE_MOCK_ECASH>
Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP listener will accept any credential without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL>
Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
--open-proxy <OPEN_PROXY>
+1 -1
View File
@@ -38,7 +38,7 @@
"@nextui-org/accordion": "^2.0.40",
"@nextui-org/react": "^2.4.8",
"@nymproject/contract-clients": ">=1.2.4-rc.2 || ^1",
"@nymproject/mix-fetch-full-fat": ">=1.5.1-rc.0 || ^1.4.1",
"@nymproject/mix-fetch-full-fat": "^1.4.2",
"@nymproject/sdk-full-fat": ">=1.5.1-rc.0 || ^1.4.1",
"@redocly/cli": "^1.25.15",
"@types/mdx": "^2.0.13",
@@ -13,7 +13,7 @@ Sounds great, are there any catches? Well, there are a few (for now):
- For now, `mixfetch` doesn't work with SURBS, although this will change in the future.
- For now, `mixFetch` cannot deal with concurrent requests with the same base URL.
- `mixFetch` supports concurrent requests (up to 10) to either different URLs on the same domain or different domains. Duplicate requests to the exact same URL will be deduplicated.
<Callout type="info" emoji="️">
Right now Gateways are not required to run a Secure Websocket (WSS) listener, so only a subset of nodes running in Gateway mode have configured their nodes to do so.
@@ -33,6 +33,7 @@ curl -X 'GET' \
import type { SetupMixFetchOps } from '@nymproject/mix-fetch';
const mixFetchOptions: SetupMixFetchOps = {
clientId: "my-mixfetch-client", // explicit ID to avoid stale default IndexedDB storage
preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1", // with WSS
mixFetchOverride: {
requestTimeoutMs: 60_000,
@@ -78,7 +79,7 @@ import { mixFetch } from '@nymproject/mix-fetch-full-fat';
##### Example: using the `mixFetch` client:
`Get` and `Post` outputs will be observable from your console.
`Get`, `Post`, and `Concurrent` outputs will be observable from your console. MixFetch auto-initializes on the first request. Individual concurrent results are logged as they arrive.
```ts
import './App.css';
@@ -86,6 +87,7 @@ import { mixFetch, SetupMixFetchOps } from '@nymproject/mix-fetch-full-fat';
import React from 'react';
const mixFetchOptions: SetupMixFetchOps = {
clientId: "my-mixfetch-client", // explicit ID to avoid stale default IndexedDB storage
preferredGateway: '23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb', // with WSS
preferredNetworkRequester:
'HuNL1pFprNSKW6jdqppibXP5KNKCNJxDh7ivpYcoULN9.C62NahRTUf6kqpNtDVHXoVriQr6yyaU5LtxdgpbsGrtA@23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb',
@@ -103,7 +105,7 @@ export function HttpGET() {
const response = await mixFetch('https://nym.com/favicon.svg', { mode: 'unsafe-ignore-cors' }, mixFetchOptions);
const text = await response.text();
console.log('response was', text);
setHtml(html);
setHtml(text);
}
return (
@@ -146,11 +148,63 @@ export function HttpPOST() {
);
}
// Send 5 concurrent requests to different URLs on the same domain using Promise.all
export function HttpConcurrent() {
const [results, setResults] = React.useState<string[]>([]);
const [busy, setBusy] = React.useState(false);
async function fetchConcurrent() {
const baseUrl = 'https://jsonplaceholder.typicode.com/posts/';
const count = 5;
setBusy(true);
setResults([]);
console.log(`Starting ${count} concurrent requests to ${baseUrl}1-${count}...`);
try {
// Fire off all requests at once with Promise.all
const requests = Array.from({ length: count }, (_, i) => {
const url = `${baseUrl}${i + 1}`;
return mixFetch(url, { mode: 'unsafe-ignore-cors' }, mixFetchOptions)
.then((res) => res.json())
.then((json: { id: number; title: string }) => {
const entry = `[${json.id}] ${json.title}`;
console.log(entry);
return entry;
});
});
const allResults = await Promise.all(requests);
setResults(allResults);
console.log('All concurrent requests completed!', allResults);
} catch (err) {
console.error('Concurrent fetch error:', err);
} finally {
setBusy(false);
}
}
return (
<>
<button onClick={fetchConcurrent} disabled={busy}>
{busy ? 'Fetching...' : 'Send 5 Concurrent Requests'}
</button>
{results.length > 0 && (
<ul>
{results.map((r, i) => (
<li key={i}>{r}</li>
))}
</ul>
)}
</>
);
}
export default function App() {
return (
<>
<HttpGET />
<HttpPOST />
<HttpConcurrent />
</>
);
}
+50 -38
View File
@@ -78,14 +78,14 @@ importers:
specifier: '>=1.2.4-rc.2 || ^1'
version: 1.4.1
'@nymproject/mix-fetch-full-fat':
specifier: '>=1.5.1-rc.0 || ^1.4.1'
version: 1.4.1
specifier: ^1.4.2
version: 1.4.2
'@nymproject/sdk-full-fat':
specifier: '>=1.5.1-rc.0 || ^1.4.1'
version: 1.4.1
'@redocly/cli':
specifier: ^1.25.15
version: 1.34.5(ajv@8.17.1)
version: 1.34.5(ajv@6.12.6)
'@types/mdx':
specifier: ^2.0.13
version: 2.0.13
@@ -1062,7 +1062,6 @@ packages:
'@next/swc-linux-x64-gnu@15.5.0':
resolution: {integrity: sha512-zPisT+obYypM/l6EZ0yRkK3LEuoZqHaSoYKj+5jiD9ESHwdr6QhnabnNxYkdy34uCigNlWIaCbjFmQ8FY5AlxA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-gnu@15.5.7':
@@ -1705,8 +1704,8 @@ packages:
'@nymproject/contract-clients@1.4.1':
resolution: {integrity: sha512-HuJZ4Hv+Rl6ZZEtCHKgurNLJapM+QQRJlGkevFH2a4UdqUqF9omUkUi3AVes4679dPoSFgvA7plyVSDBdbgV6w==}
'@nymproject/mix-fetch-full-fat@1.4.1':
resolution: {integrity: sha512-AMa21sEd9FELqAJe1lCyHPqxxbc13ApiJ1P/exAslQjiFPb/de/3Ow0FHqKGNPrwyVRS/T2pSzjQ3l8TddiEBA==}
'@nymproject/mix-fetch-full-fat@1.4.2':
resolution: {integrity: sha512-QHPwa7A+c/2VUm4Imq2I21toFiZhbZxcjHud1sFsE9hN5BWxZ+QJKV2bg9oBUzulzoQabsk48RA13/hqU7c4KA==}
'@nymproject/sdk-full-fat@1.4.1':
resolution: {integrity: sha512-dh5bvMUj3m8nEssvO8Nl66WpcJAjwRZrGNwqfczJWLG4nX3Vt95tPLv4v0/Z1W3DQWQFW6WmEPPYHNjl18V/fA==}
@@ -3215,6 +3214,11 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
hasBin: true
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
@@ -3390,8 +3394,9 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.9.19:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
baseline-browser-mapping@2.10.0:
resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==}
engines: {node: '>=6.0.0'}
hasBin: true
bech32@1.1.4:
@@ -3510,6 +3515,9 @@ packages:
caniuse-lite@1.0.30001769:
resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==}
caniuse-lite@1.0.30001774:
resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==}
cardinal@2.1.1:
resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==}
hasBin: true
@@ -4022,8 +4030,8 @@ packages:
duplexify@4.1.3:
resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
electron-to-chromium@1.5.286:
resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
electron-to-chromium@1.5.302:
resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==}
elkjs@0.9.3:
resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==}
@@ -5355,8 +5363,8 @@ packages:
modern-ahocorasick@1.1.0:
resolution: {integrity: sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==}
motion-dom@12.34.0:
resolution: {integrity: sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==}
motion-dom@12.34.3:
resolution: {integrity: sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==}
motion-utils@12.29.2:
resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==}
@@ -6709,8 +6717,8 @@ packages:
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webpack-sources@3.3.3:
resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==}
webpack-sources@3.3.4:
resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==}
engines: {node: '>=10.13.0'}
webpack@5.101.3:
@@ -9307,7 +9315,7 @@ snapshots:
'@nymproject/contract-clients@1.4.1': {}
'@nymproject/mix-fetch-full-fat@1.4.1': {}
'@nymproject/mix-fetch-full-fat@1.4.2': {}
'@nymproject/sdk-full-fat@1.4.1': {}
@@ -11046,7 +11054,7 @@ snapshots:
require-from-string: 2.0.2
uri-js-replace: 1.0.1
'@redocly/cli@1.34.5(ajv@8.17.1)':
'@redocly/cli@1.34.5(ajv@6.12.6)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/exporter-trace-otlp-http': 0.53.0(@opentelemetry/api@1.9.0)
@@ -11055,7 +11063,7 @@ snapshots:
'@opentelemetry/semantic-conventions': 1.27.0
'@redocly/config': 0.22.2
'@redocly/openapi-core': 1.34.5
'@redocly/respect-core': 1.34.5(ajv@8.17.1)
'@redocly/respect-core': 1.34.5(ajv@6.12.6)
abort-controller: 3.0.0
chokidar: 3.6.0
colorette: 1.4.0
@@ -11098,12 +11106,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@redocly/respect-core@1.34.5(ajv@8.17.1)':
'@redocly/respect-core@1.34.5(ajv@6.12.6)':
dependencies:
'@faker-js/faker': 7.6.0
'@redocly/ajv': 8.11.2
'@redocly/openapi-core': 1.34.5
better-ajv-errors: 1.2.0(ajv@8.17.1)
better-ajv-errors: 1.2.0(ajv@6.12.6)
colorette: 2.0.20
concat-stream: 2.0.0
cookie: 0.7.2
@@ -11810,9 +11818,9 @@ snapshots:
dependencies:
event-target-shim: 5.0.1
acorn-import-phases@1.0.4(acorn@8.15.0):
acorn-import-phases@1.0.4(acorn@8.16.0):
dependencies:
acorn: 8.15.0
acorn: 8.16.0
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
@@ -11820,6 +11828,8 @@ snapshots:
acorn@8.15.0: {}
acorn@8.16.0: {}
agent-base@7.1.4: {}
ajv-draft-04@1.0.0(ajv@8.17.1):
@@ -12009,15 +12019,15 @@ snapshots:
base64-js@1.5.1: {}
baseline-browser-mapping@2.9.19: {}
baseline-browser-mapping@2.10.0: {}
bech32@1.1.4: {}
better-ajv-errors@1.2.0(ajv@8.17.1):
better-ajv-errors@1.2.0(ajv@6.12.6):
dependencies:
'@babel/code-frame': 7.27.1
'@humanwhocodes/momoa': 2.0.4
ajv: 8.17.1
ajv: 6.12.6
chalk: 4.1.2
jsonpointer: 5.0.1
leven: 3.1.0
@@ -12079,9 +12089,9 @@ snapshots:
browserslist@4.28.1:
dependencies:
baseline-browser-mapping: 2.9.19
caniuse-lite: 1.0.30001769
electron-to-chromium: 1.5.286
baseline-browser-mapping: 2.10.0
caniuse-lite: 1.0.30001774
electron-to-chromium: 1.5.302
node-releases: 2.0.27
update-browserslist-db: 1.2.3(browserslist@4.28.1)
@@ -12135,6 +12145,8 @@ snapshots:
caniuse-lite@1.0.30001769: {}
caniuse-lite@1.0.30001774: {}
cardinal@2.1.1:
dependencies:
ansicolors: 0.3.2
@@ -12672,7 +12684,7 @@ snapshots:
readable-stream: 3.6.2
stream-shift: 1.0.3
electron-to-chromium@1.5.286: {}
electron-to-chromium@1.5.302: {}
elkjs@0.9.3: {}
@@ -12830,7 +12842,7 @@ snapshots:
eslint: 8.46.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@8.46.0))(eslint@8.46.0)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.46.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.46.0)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.46.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.46.0))(eslint@8.46.0))(eslint@8.46.0)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.46.0)
eslint-plugin-react: 7.37.5(eslint@8.46.0)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.46.0)
@@ -12860,7 +12872,7 @@ snapshots:
tinyglobby: 0.2.14
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.46.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.46.0)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.46.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.46.0))(eslint@8.46.0))(eslint@8.46.0)
transitivePeerDependencies:
- supports-color
@@ -12875,7 +12887,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.46.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.46.0):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.46.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.46.0))(eslint@8.46.0))(eslint@8.46.0):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -13171,7 +13183,7 @@ snapshots:
framer-motion@12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
motion-dom: 12.34.0
motion-dom: 12.34.3
motion-utils: 12.29.2
tslib: 2.8.1
optionalDependencies:
@@ -14491,7 +14503,7 @@ snapshots:
modern-ahocorasick@1.1.0: {}
motion-dom@12.34.0:
motion-dom@12.34.3:
dependencies:
motion-utils: 12.29.2
@@ -15781,7 +15793,7 @@ snapshots:
terser@5.46.0:
dependencies:
'@jridgewell/source-map': 0.3.11
acorn: 8.15.0
acorn: 8.16.0
commander: 2.20.3
source-map-support: 0.5.21
@@ -16171,7 +16183,7 @@ snapshots:
webidl-conversions@3.0.1: {}
webpack-sources@3.3.3: {}
webpack-sources@3.3.4: {}
webpack@5.101.3:
dependencies:
@@ -16181,8 +16193,8 @@ snapshots:
'@webassemblyjs/ast': 1.14.1
'@webassemblyjs/wasm-edit': 1.14.1
'@webassemblyjs/wasm-parser': 1.14.1
acorn: 8.15.0
acorn-import-phases: 1.0.4(acorn@8.15.0)
acorn: 8.16.0
acorn-import-phases: 1.0.4(acorn@8.16.0)
browserslist: 4.28.1
chrome-trace-event: 1.0.4
enhanced-resolve: 5.19.0
@@ -16199,7 +16211,7 @@ snapshots:
tapable: 2.3.0
terser-webpack-plugin: 5.3.16(webpack@5.101.3)
watchpack: 2.5.1
webpack-sources: 3.3.3
webpack-sources: 3.3.4
transitivePeerDependencies:
- '@swc/core'
- esbuild
File diff suppressed because it is too large Load Diff