Phase 1:
- S0: Fix open redirect (safeNavigate), delete AuthContext/legacy auth, encrypt API keys, gitignore .env files
- S1: Split client.ts god object into 5 modules, unify toast system, delete unused Sidebar
- S2: Add glass button variant, migrate 32 z-index to SUMI tokens, fix card dark mode
- S3: Skip nav link, aria-hidden on icons, focus-visible ring fixes, alt attrs, aria-live regions
- S4: React.memo on list items, fix key={index}, loading=lazy on images
- S5: Branded loading screen, page transitions respect reduced-motion, LikeButton micro-interaction, i18n sidebar/header
Phase 2 Sprint 6:
- Wire Tailwind shadow utilities to SUMI tokens in @theme block (fixes 50+ files)
- Define shadow-card/shadow-card-hover tokens
- Remove dark:shadow-none workarounds from card.tsx (SUMI handles per-theme shadows)
Co-authored-by: Cursor <cursoragent@cursor.com>
165 lines
4 KiB
TypeScript
165 lines
4 KiB
TypeScript
/**
|
|
* S1.1: API helper utilities
|
|
* Extracted from client.ts — Cancellable requests, deduplication, utilities
|
|
*/
|
|
|
|
import axios, { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
|
|
import { logger } from '@/utils/logger';
|
|
import { requestDeduplication } from '../requestDeduplication';
|
|
import { responseCache } from '../responseCache';
|
|
import { apiClient } from './httpClient';
|
|
|
|
/**
|
|
* Edge 2.2: Create a cancellable request with AbortController support.
|
|
*/
|
|
export function createCancellableRequest<T>(
|
|
requestFn: (signal: AbortSignal) => Promise<T>,
|
|
): { request: Promise<T>; abort: () => void } {
|
|
const abortController = new AbortController();
|
|
const signal = abortController.signal;
|
|
|
|
const request = requestFn(signal).catch((error) => {
|
|
if (
|
|
axios.isCancel(error) ||
|
|
error.name === 'AbortError' ||
|
|
signal.aborted
|
|
) {
|
|
throw error;
|
|
}
|
|
throw error;
|
|
});
|
|
|
|
return {
|
|
request,
|
|
abort: () => {
|
|
if (!signal.aborted) {
|
|
abortController.abort();
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Edge 2.2: Create a request with automatic timeout cancellation.
|
|
*/
|
|
export function createRequestWithTimeout<T>(
|
|
requestFn: (signal: AbortSignal) => Promise<T>,
|
|
timeoutMs: number,
|
|
): { request: Promise<T>; abort: () => void } {
|
|
const abortController = new AbortController();
|
|
const signal = abortController.signal;
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
if (!signal.aborted) {
|
|
abortController.abort();
|
|
}
|
|
}, timeoutMs);
|
|
|
|
const request = requestFn(signal)
|
|
.catch((error) => {
|
|
if (
|
|
axios.isCancel(error) ||
|
|
error.name === 'AbortError' ||
|
|
signal.aborted
|
|
) {
|
|
throw error;
|
|
}
|
|
throw error;
|
|
})
|
|
.finally(() => {
|
|
clearTimeout(timeoutId);
|
|
});
|
|
|
|
return {
|
|
request,
|
|
abort: () => {
|
|
clearTimeout(timeoutId);
|
|
if (!signal.aborted) {
|
|
abortController.abort();
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* FE-API-016/017: Enhanced API client with deduplication and caching
|
|
*/
|
|
export const deduplicatedApiClient = {
|
|
get: <T = any>(url: string, config?: InternalAxiosRequestConfig) => {
|
|
if (!(config as any)?._disableCache) {
|
|
const cachedResponse = responseCache.get({
|
|
...config,
|
|
method: 'GET',
|
|
url,
|
|
});
|
|
if (cachedResponse) {
|
|
logger.debug(`[API] Using cached response for: ${url}`);
|
|
return Promise.resolve(cachedResponse as AxiosResponse<T>);
|
|
}
|
|
}
|
|
return requestDeduplication.getOrCreateRequest(
|
|
{ ...config, method: 'GET', url },
|
|
() => apiClient.get<T>(url, config),
|
|
);
|
|
},
|
|
|
|
post: <T = any>(
|
|
url: string,
|
|
data?: any,
|
|
config?: InternalAxiosRequestConfig,
|
|
) => {
|
|
return requestDeduplication.getOrCreateRequest(
|
|
{ ...config, method: 'POST', url, data },
|
|
() => apiClient.post<T>(url, data, config),
|
|
);
|
|
},
|
|
|
|
put: <T = any>(
|
|
url: string,
|
|
data?: any,
|
|
config?: InternalAxiosRequestConfig,
|
|
) => {
|
|
return requestDeduplication.getOrCreateRequest(
|
|
{ ...config, method: 'PUT', url, data },
|
|
() => apiClient.put<T>(url, data, config),
|
|
);
|
|
},
|
|
|
|
patch: <T = any>(
|
|
url: string,
|
|
data?: any,
|
|
config?: InternalAxiosRequestConfig,
|
|
) => {
|
|
return requestDeduplication.getOrCreateRequest(
|
|
{ ...config, method: 'PATCH', url, data },
|
|
() => apiClient.patch<T>(url, data, config),
|
|
);
|
|
},
|
|
|
|
delete: <T = any>(url: string, config?: InternalAxiosRequestConfig) => {
|
|
return requestDeduplication.getOrCreateRequest(
|
|
{ ...config, method: 'DELETE', url },
|
|
() => apiClient.delete<T>(url, config),
|
|
);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Edge 2.3: Check if a request is slow
|
|
*/
|
|
export function isSlowRequest(
|
|
config?: InternalAxiosRequestConfig,
|
|
): boolean {
|
|
if (!config) return false;
|
|
return (config as any)?._isSlowRequest === true;
|
|
}
|
|
|
|
/**
|
|
* Edge 2.3: Get request duration in milliseconds
|
|
*/
|
|
export function getRequestDuration(
|
|
config?: InternalAxiosRequestConfig,
|
|
): number | undefined {
|
|
if (!config) return undefined;
|
|
return (config as any)?._requestDuration;
|
|
}
|