1import { useEffect, useRef } from "react";
2
3import { type SlowRpcAckRequest, useSlowRpcAckRequests } from "../rpc/requestLatencyState";
4import { toastManager } from "./ui/toast";
5
6function describeSlowRequests(requests: ReadonlyArray<SlowRpcAckRequest>): string {
7 const count = requests.length;
8 // Thresholds vary per method, so report the smallest one the batch has passed.
9 const thresholdSeconds = Math.round(
10 Math.min(...requests.map((request) => request.thresholdMs)) / 1000,
11 );
12
13 return `${count} request${count === 1 ? "" : "s"} waiting longer than ${thresholdSeconds}s.`;
14}
15
16function SlowRequestDetails({ requests }: { requests: ReadonlyArray<SlowRpcAckRequest> }) {
17 return (
18 <ul className="space-y-2.5 text-xs text-muted-foreground">
19 {requests.map((request) => (
20 <li
21 className="min-w-0 border-border/50 border-b pb-2 last:border-b-0 last:pb-0"
22 key={request.requestId}
23 >
24 <div className="wrap-break-word font-medium text-foreground">{request.tag}</div>
25 <div className="mt-0.5 text-3xs opacity-75">
26 Started {new Date(request.startedAt).toLocaleTimeString()}
27 </div>
28 </li>
29 ))}
30 </ul>
31 );
32}
33
34export function SlowRpcRequestToastCoordinator() {
35 const slowRequests = useSlowRpcAckRequests();
36 const toastIdRef = useRef<ReturnType<typeof toastManager.add> | null>(null);
37
38 useEffect(() => {
39 if (slowRequests.length === 0) {
40 if (toastIdRef.current !== null) {
41 toastManager.close(toastIdRef.current);
42 toastIdRef.current = null;
43 }
44 return;
45 }
46
47 const nextToast = {
48 data: {
49 expandableContent: <SlowRequestDetails requests={slowRequests} />,
50 expandableDescriptionTrigger: true,
51 expandableLabels: { collapse: "Hide requests", expand: "Show requests" },
52 },
53 description: describeSlowRequests(slowRequests),
54 timeout: 0,
55 title: "Some requests are slow",
56 type: "warning" as const,
57 };
58
59 if (toastIdRef.current === null) {
60 toastIdRef.current = toastManager.add(nextToast);
61 } else {
62 toastManager.update(toastIdRef.current, nextToast);
63 }
64 }, [slowRequests]);
65
66 useEffect(
67 () => () => {
68 if (toastIdRef.current !== null) {
69 toastManager.close(toastIdRef.current);
70 }
71 },
72 [],
73 );
74
75 return null;
76}