Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,26 @@ import logo from "./iloader.svg";
import { GlassCard } from "./components/GlassCard";
import { useTranslation } from "react-i18next";
import { usePlatform } from "./PlatformContext";
import { useStore } from "./StoreContext";
import {
anisetteServers,
AnisetteMeasurement,
measureAnisetteServers,
} from "./anisette";

const anisetteCheckIntervalMs = 30000;

function App() {
const { t } = useTranslation();
const [anisetteServer, setAnisetteServer] = useStore<string>(
"anisetteServer",
"ani.sidestore.io",
);
const [anisetteMeasurements, setAnisetteMeasurements] = useState<
AnisetteMeasurement[]
>([]);
const anisetteServerRef = useRef(anisetteServer);
const setAnisetteServerRef = useRef(setAnisetteServer);

const [operationState, setOperationState] = useState<OperationState | null>(
null,
Expand Down Expand Up @@ -72,6 +89,70 @@ function App() {
checkForUpdates();
}, []);

useEffect(() => {
anisetteServerRef.current = anisetteServer;
}, [anisetteServer]);

useEffect(() => {
setAnisetteServerRef.current = setAnisetteServer;
}, [setAnisetteServer]);

useEffect(() => {
let cancelled = false;
let intervalId: number | undefined;
let isInitialCheck = true;

const checkAnisetteServers = async () => {
if (document.visibilityState !== "visible") return;

const measurements = await measureAnisetteServers();
if (cancelled) return;

setAnisetteMeasurements(measurements);

const currentAnisetteServer = anisetteServerRef.current;

// User is on a custom (non-preset) server: never auto-pick for them.
const isCustomServer = !anisetteServers.some(
(server) => server.value === currentAnisetteServer,
);
if (isCustomServer) {
isInitialCheck = false;
return;
}

const currentMeasurement = measurements.find(
(measurement) => measurement.value === currentAnisetteServer,
);

// First run: silently pick the fastest server outright.
// After that, only switch when the current server stops responding.
if (!isInitialCheck && currentMeasurement?.ttfbMs !== null) return;
isInitialCheck = false;

const fastestMeasurement = measurements
.filter((measurement) => measurement.ttfbMs !== null)
.sort((left, right) => left.ttfbMs! - right.ttfbMs!)[0];

if (!fastestMeasurement || fastestMeasurement.value === currentAnisetteServer) {
return;
}

setAnisetteServerRef.current(fastestMeasurement.value);
};

checkAnisetteServers();
intervalId = window.setInterval(
checkAnisetteServers,
anisetteCheckIntervalMs,
);

return () => {
cancelled = true;
if (intervalId !== undefined) window.clearInterval(intervalId);
};
}, []);

const shortcutLabel = useCallback(
(mac: string, windows: string, linux?: string) => {
if (platform === "mac") return mac;
Expand Down Expand Up @@ -393,6 +474,7 @@ function App() {
platform={platform}
shortcutLabel={shortcutLabel}
checkKeyring={checkKeyring}
anisetteMeasurements={anisetteMeasurements}
/>
</GlassCard>
</section>
Expand Down
85 changes: 85 additions & 0 deletions src/anisette.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
export type AnisetteServer = {
value: string;
label: string;
};

export type AnisetteSpeedGrade =
| "very_fast"
| "fast"
| "good"
| "normal"
| "slow"
| "very_slow"
| "no_response";

export type AnisetteMeasurement = {
value: string;
ttfbMs: number | null;
grade: AnisetteSpeedGrade;
};

export const anisetteServers: AnisetteServer[] = [
{ value: "ani.sidestore.io", label: "SideStore" },
{ value: "ani.stikstore.app", label: "StikStore" },
{ value: "ani.sidestore.app", label: "SideStore" },
{ value: "ani.sidestore.zip", label: "SideStore" },
{ value: "ani.846969.xyz", label: "SideStore" },
{ value: "ani.neoarz.xyz", label: "neoarz" },
{ value: "ani.xu30.top", label: "SteX" },
{ value: "anisette.wedotstud.io", label: "WE. Studio" },
{ value: "ani.waterwave.space", label: "waterwave" },
];

const timeoutMs = 5000;

export const normalizeAnisetteUrl = (server: string) => {
return server.startsWith("http://") || server.startsWith("https://")
? server
: `https://${server}`;
};

export const getAnisetteSpeedGrade = (
ttfbMs: number | null,
): AnisetteSpeedGrade => {
if (ttfbMs === null) return "no_response";
if (ttfbMs <= 50) return "very_fast";
if (ttfbMs <= 120) return "fast";
if (ttfbMs <= 250) return "good";
if (ttfbMs <= 500) return "normal";
if (ttfbMs <= 1000) return "slow";
return "very_slow";
};

export const measureAnisetteServer = async (
server: AnisetteServer,
): Promise<AnisetteMeasurement> => {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), timeoutMs);
const startedAt = performance.now();

try {
await fetch(normalizeAnisetteUrl(server.value), {
cache: "no-store",
mode: "no-cors",
signal: controller.signal,
});
const ttfbMs = Math.round(performance.now() - startedAt);
return {
value: server.value,
ttfbMs,
grade: getAnisetteSpeedGrade(ttfbMs),
};
} catch {
return {
value: server.value,
ttfbMs: null,
grade: "no_response",
};
} finally {
window.clearTimeout(timeout);
}
};

export const measureAnisetteServers = async () => {
return Promise.all(anisetteServers.map(measureAnisetteServer));
};
39 changes: 39 additions & 0 deletions src/components/Dropdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,42 @@
font-weight: 500;
}

.dropdown-selected-text,
.dropdown-option-text {
min-width: 0;
display: flex;
align-items: baseline;
gap: 0.45rem;
}

.dropdown-selected-text {
flex: 1;
}

.dropdown-option-text {
flex: 1;
justify-content: space-between;
}

.dropdown-option-main {
min-width: 0;
display: flex;
align-items: baseline;
gap: 0.45rem;
}

.dropdown-sub-label,
.dropdown-detail {
color: rgba(255, 255, 255, 0.52);
font-size: 0.82rem;
font-weight: 400;
white-space: nowrap;
}

.dropdown-detail {
margin-left: auto;
}

.custom-dropdown.open .dropdown-toggle {
border-color: var(--accent-primary);
background: #3a3a3c;
Expand Down Expand Up @@ -47,6 +83,7 @@
z-index: 5;
max-height: 260px;
overflow-y: auto;
overflow-anchor: none;
}

.custom-dropdown.flip-up .dropdown-menu {
Expand All @@ -71,6 +108,7 @@
color: inherit;
font: inherit;
text-align: left;
scroll-margin: 0;
}

.dropdown-option:hover,
Expand All @@ -87,4 +125,5 @@
color: var(--accent-primary);
font-weight: 700;
font-size: 0.95rem;
flex: 0 0 auto;
}
Loading
Loading