refact(frontend): simplify hooks, remove wrappers and over-defensiveness

- Remove tunnelCommand.ts pass-throughs, import exposeName directly - Replace dedupeStrings/dedupeNumbers with [...new Set()] - Remove over-defensive array equality checks in useList effects - Inline cheap useMemo, change constants from useState to useMemo - Simplify ensureJsonEnvelope validation - Parameterize UDP/TCP settings handlers - Replace verbose promise duck-typing with instanceof Promise

cognitive committed Apr 4, 2026 at 06:43 UTC 8468063ee290e3a50e991a5418a1be2c70962c7d
6 files changed +38 -144
frontend/src/components/ServerCard.tsx
+1 -1
@@ -102,7 +102,7 @@ export function ServerCard({
102
103 try {
104 const result = action();
105 - if (result && typeof (result as Promise<void>).then === "function") {
105 + if (result instanceof Promise) {
106 void result.catch((error) => {
107 console.error("Failed admin action", error);
108 });
frontend/src/hooks/useAdmin.ts
+16 -40
@@ -142,21 +142,6 @@ function normalizeApprovalMode(value: string | undefined): ApprovalMode {
142 return value === "manual" ? "manual" : "auto";
143 }
144
145 -function dedupeStrings(values: string[]): string[] {
146 - const seen = new Set<string>();
147 - const output: string[] = [];
148 -
149 - values.forEach((value) => {
150 - if (seen.has(value)) {
151 - return;
152 - }
153 - seen.add(value);
154 - output.push(value);
155 - });
156 -
157 - return output;
158 -}
159 -
145 interface AdminSnapshot {
146 serverData: AdminLeaseData[];
147 approvalMode: ApprovalMode;
@@ -349,31 +334,22 @@ export function useAdmin() {
334 });
335 };
336
352 - const handleUDPSettingsChange = async (settings: UDPSettings) => {
353 - await runAdminAction(async () => {
354 - const response = await apiClient.post<{ enabled: boolean; max_leases: number }>(
355 - API_PATHS.admin.udpSettings,
356 - { enabled: settings.enabled, max_leases: settings.maxLeases }
357 - );
358 - setUDPSettings({
359 - enabled: response?.enabled ?? settings.enabled,
360 - maxLeases: response?.max_leases ?? settings.maxLeases,
337 + const handleSettingsChange = (path: string, setter: (s: { enabled: boolean; maxLeases: number }) => void) =>
338 + async (settings: { enabled: boolean; maxLeases: number }) => {
339 + await runAdminAction(async () => {
340 + const response = await apiClient.post<{ enabled: boolean; max_leases: number }>(path, {
341 + enabled: settings.enabled,
342 + max_leases: settings.maxLeases,
343 + });
344 + setter({
345 + enabled: response?.enabled ?? settings.enabled,
346 + maxLeases: response?.max_leases ?? settings.maxLeases,
347 + });
348 });
362 - });
363 - };
349 + };
350
365 - const handleTCPPortSettingsChange = async (settings: TCPPortSettings) => {
366 - await runAdminAction(async () => {
367 - const response = await apiClient.post<{ enabled: boolean; max_leases: number }>(
368 - API_PATHS.admin.tcpPortSettings,
369 - { enabled: settings.enabled, max_leases: settings.maxLeases }
370 - );
371 - setTCPPortSettings({
372 - enabled: response?.enabled ?? settings.enabled,
373 - maxLeases: response?.max_leases ?? settings.maxLeases,
374 - });
375 - });
376 - };
351 + const handleUDPSettingsChange = handleSettingsChange(API_PATHS.admin.udpSettings, setUDPSettings);
352 + const handleTCPPortSettingsChange = handleSettingsChange(API_PATHS.admin.tcpPortSettings, setTCPPortSettings);
353
354 const handleLandingPageEnabledChange = async (enabled: boolean) => {
355 await runAdminAction(async () => {
@@ -405,9 +381,9 @@ export function useAdmin() {
381 });
382
383 const runBulkLeaseAction = async (identityKeys: string[], action: LeaseAction) => {
408 - const normalizedIdentityKeys = dedupeStrings(
384 + const normalizedIdentityKeys = [...new Set(
385 identityKeys.filter((identityKey) => identityKey.length > 0)
410 - );
386 + )];
387 if (normalizedIdentityKeys.length === 0) {
388 throw new Error("No valid leases selected");
389 }
frontend/src/hooks/useList.ts
+4 -35
@@ -36,19 +36,6 @@ export interface UseListReturn<T extends BaseServer> {
36 handleToggleFavorite: (serverId: number) => void;
37 }
38
39 -function dedupeNumbers(values: number[]): number[] {
40 - const seen = new Set<number>();
41 - const next: number[] = [];
42 - values.forEach((value) => {
43 - if (seen.has(value)) {
44 - return;
45 - }
46 - seen.add(value);
47 - next.push(value);
48 - });
49 - return next;
50 -}
51 -
39 function readStoredFavorites(storageKey: string): number[] {
40 let raw: string | null = null;
41 try {
@@ -66,11 +53,11 @@ function readStoredFavorites(storageKey: string): number[] {
53 if (!Array.isArray(parsed)) {
54 return [];
55 }
69 - return dedupeNumbers(
56 + return [...new Set(
57 parsed.filter(
58 (value): value is number => Number.isInteger(value) && value > 0
59 )
73 - );
60 + )];
61 } catch {
62 return [];
63 }
@@ -140,30 +127,12 @@ export function useList<T extends BaseServer>({
127
128 useEffect(() => {
129 const validIDs = new Set(servers.map((server) => server.id));
143 - setFavorites((prev) => {
144 - const next = dedupeNumbers(prev.filter((id) => validIDs.has(id)));
145 - if (
146 - next.length === prev.length &&
147 - next.every((value, index) => value === prev[index])
148 - ) {
149 - return prev;
150 - }
151 - return next;
152 - });
130 + setFavorites((prev) => prev.filter((id) => validIDs.has(id)));
131 }, [servers]);
132
133 useEffect(() => {
134 const availableTagSet = new Set(availableTags);
157 - setSelectedTags((prev) => {
158 - const next = prev.filter((tag) => availableTagSet.has(tag));
159 - if (
160 - next.length === prev.length &&
161 - next.every((value, index) => value === prev[index])
162 - ) {
163 - return prev;
164 - }
165 - return next;
166 - });
135 + setSelectedTags((prev) => prev.filter((tag) => availableTagSet.has(tag)));
136 }, [availableTags]);
137
138 const filteredServers = useMemo(() => {
frontend/src/hooks/useTunnelCommand.ts
+9 -16
@@ -1,9 +1,11 @@
1 import { useEffect, useMemo, useState, type ChangeEvent } from "react";
2 import {
3 - buildDefaultTunnelName,
3 + buildDefaultExposeName,
4 + normalizeExposeName,
5 +} from "@/lib/exposeName";
6 +import {
7 buildTunnelCommand,
8 buildTunnelDisplayCommand,
6 - normalizeTunnelCommandName,
9 type TunnelCommandOS,
10 } from "@/lib/tunnelCommand";
11
@@ -73,8 +75,8 @@ interface TunnelCommandExtras {
75 }
76
77 export function useTunnelCommand(extras: TunnelCommandExtras = {}) {
76 - const [currentOrigin] = useState(readCurrentOrigin);
77 - const [nameSeed] = useState(readTunnelNameSeed);
78 + const currentOrigin = useMemo(() => readCurrentOrigin(), []);
79 + const nameSeed = useMemo(() => readTunnelNameSeed(), []);
80
81 const [target, setTarget] = useState(DEFAULT_HOST);
82 const [name, setName] = useState("");
@@ -82,18 +84,9 @@ export function useTunnelCommand(extras: TunnelCommandExtras = {}) {
84 const [copied, setCopied] = useState(false);
85 const [os, setOs] = useState<TunnelCommandOS>("unix");
86
85 - const resolvedNameSeed = useMemo(
86 - () => `${nameSeed}:${nameShuffleKey}`,
87 - [nameSeed, nameShuffleKey]
88 - );
89 - const generatedName = useMemo(
90 - () => buildDefaultTunnelName(target, resolvedNameSeed),
91 - [resolvedNameSeed, target]
92 - );
93 - const normalizedName = useMemo(
94 - () => normalizeTunnelCommandName(name),
95 - [name]
96 - );
87 + const resolvedNameSeed = `${nameSeed}:${nameShuffleKey}`;
88 + const generatedName = buildDefaultExposeName(target, resolvedNameSeed);
89 + const normalizedName = normalizeExposeName(name);
90 const effectiveName = normalizedName === "" ? generatedName : normalizedName;
91 const commandOptions = useMemo(
92 () => ({
frontend/src/lib/apiClient.ts
+6 -31
@@ -41,45 +41,20 @@ function headersToObject(headers?: HeadersInit): Record<string, string> {
41 }
42
43 function ensureJsonEnvelope<T>(raw: unknown, path: string, status: number): APIEnvelope<T> {
44 - if (!isRecord(raw)) {
45 - throw new APIClientError(
46 - `Unexpected API response for ${path}: envelope is not an object`,
47 - status,
48 - "invalid_envelope",
49 - raw
50 - );
44 + if (!isRecord(raw) || typeof raw.ok !== "boolean") {
45 + throw new APIClientError(`Invalid API response for ${path}`, status, "invalid_envelope", raw);
46 }
52 -
53 - const okValue = raw.ok;
54 - if (typeof okValue !== "boolean") {
55 - throw new APIClientError(
56 - `Unexpected API response for ${path}: missing ok flag`,
57 - status,
58 - "invalid_envelope",
59 - raw
60 - );
47 + if (raw.error !== undefined && !isRecord(raw.error)) {
48 + throw new APIClientError(`Invalid error payload for ${path}`, status, "invalid_envelope", raw.error);
49 }
62 -
50 const errorValue = raw.error;
64 - if (errorValue !== undefined && !isRecord(errorValue)) {
65 - throw new APIClientError(
66 - `Unexpected API response for ${path}: invalid error payload`,
67 - status,
68 - "invalid_envelope",
69 - errorValue
70 - );
71 - }
72 -
51 return {
74 - ok: okValue,
52 + ok: raw.ok,
53 data: (raw as { data?: T }).data,
54 error: errorValue
55 ? {
56 code: typeof errorValue.code === "string" ? errorValue.code : "request_failed",
79 - message:
80 - typeof errorValue.message === "string"
81 - ? errorValue.message
82 - : "Request failed",
57 + message: typeof errorValue.message === "string" ? errorValue.message : "Request failed",
58 }
59 : undefined,
60 };
frontend/src/lib/tunnelCommand.ts
+2 -21
@@ -1,9 +1,5 @@
1 import { API_PATHS } from "@/lib/apiPaths";
2 -import {
3 - buildDefaultExposeName,
4 - normalizeExposeName,
5 - resolveExposeName,
6 -} from "@/lib/exposeName";
2 +import { resolveExposeName } from "@/lib/exposeName";
3
4 export type TunnelCommandOS = "unix" | "windows";
5
@@ -20,16 +16,6 @@ export interface TunnelCommandOptions {
16 os: TunnelCommandOS;
17 }
18
23 -export function buildDefaultTunnelName(
24 - target: string,
25 - nameSeed: string
26 -): string {
27 - return buildDefaultExposeName(target, nameSeed);
28 -}
29 -
30 -export function normalizeTunnelCommandName(value: string): string {
31 - return normalizeExposeName(value);
32 -}
19
20 export function buildTunnelCommand({
21 currentOrigin,
@@ -163,7 +149,7 @@ export function buildTunnelPreviewURL(
149 target: string,
150 nameSeed: string
151 ): string {
166 - const baseHost = getTunnelBaseHost(origin);
152 + const baseHost = getRelayOriginHost(origin);
153 const subdomain = resolveExposeName(name, target, nameSeed);
154 return `https://${subdomain}.${baseHost}`;
155 }
@@ -199,11 +185,6 @@ export function normalizeAbsoluteHTTPURL(raw: string): string {
185 }
186 }
187
202 -function getTunnelBaseHost(origin: string): string {
203 - const relayHost = getRelayOriginHost(origin);
204 - return relayHost;
205 -}
206 -
188 function getRelayOriginHost(origin: string): string {
189 try {
190 const parsed = new URL(origin);